docs: wiki update for API docs, printer installer, geenforce cutover, timezone

Execute WIKI-UPDATE-PLAN.md (14 items):
- NEW docs/PRINTER-INSTALLER.md: install-list / pc-default / install-batch
  contract + public installer map page.
- NEW-shape docs/API-REFERENCE.md: index + pointer to the live generated docs
  (/api/docs Redoc, openapi.json, llms.txt, MCP), replacing a stale full dump.
- geenforce cutover + GE-ENFORCE-DISPLAY/CLIENT/DEPLOY: server-first display
  dispatcher (display-role by FQDN, display-type.txt fallback), dashboarddefaults
  FQDN keying, legacy kiosk autostart self-heal (Wow6432Node), per-PC-type
  cutover status.
- PLUGINS: printers/slides rows + plugin-permissions note (slides.manage).
- IMPORT-API: dashboarddefaults FQDN-first keying.
- CONFIG: word-wise search, site_timezone setting.
- PILOT-DEPLOY: servers-to-network reclassify step. IMPORT-ADOPTION: fixup note.
- CLAUDE.md: test count 1077->1159, HTTPS-cutover state. CHANGELOG: timezone +
  kiosk-autostart fixes, site_timezone setting.
This commit is contained in:
cproudlock
2026-07-30 16:05:21 -04:00
parent af6bcd4726
commit 802256f929
14 changed files with 450 additions and 3329 deletions

View File

@@ -10,6 +10,14 @@ ADR-007 and ADR-002.
## [Unreleased]
### Added
- Configurable site timezone: a `site_timezone` site setting (default
`America/New_York`, public-readable), editable in Settings > Site >
Localization. Notification start/end times are entered and displayed in this
zone, and daily-reset notification expiry is computed in it. A shared
`frontend/src/utils/datetime.js` (Intl-based, DST-safe) does the conversion.
### Changed
- Asset detail pages (machines, PCs, printers, network devices, measuring
@@ -24,6 +32,15 @@ ADR-007 and ADR-002.
### Fixed
- Notification start/end times were off by the timezone offset (a 2:34 PM entry
displayed as 6:34 PM). Times are now stored UTC and shown/entered in the site
timezone; the calendar keys all-day events off the site-local day.
- Kiosk displays showed a white screen on login: a legacy 32-bit kiosk
installer's autostart kept relaunching Edge at a now-dead URL. The install's
HKLM Run value was WOW64-redirected into `SOFTWARE\Wow6432Node\...\Run` and
survived earlier cleanup. The `gea-shopfloor-display` dispatcher now purges
the legacy autostarts every enforce cycle across both registry views, all
user hives, Run/RunOnce/policy-Run, and every Startup folder.
- List pages keep the current page (and search term) in the URL query, so
paging to page 9, opening an item, and hitting browser Back returns to page 9
instead of resetting to page 1. Applies to all 18 list views via a shared

View File

@@ -44,7 +44,8 @@ Refactor phases 0-5 landed; phase 6 (multi-site distribution readiness) largely
### Active state
- 1077 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a lean-build job + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8)
- 1159 tests, naming/style check green, Gitea Actions CI (backend + naming + frontend build + a lean-build job + a migrations-mysql job that runs the real fresh upgrade on utf8mb4 MySQL 8)
- GE-Enforce HTTPS cutover: the displays/kiosks cohort now fetches manifest + inline payloads entirely over HTTPS (share-less); the `gea-shopfloor-display` scope is authored in code (`plugins/geenforce/seed_display_scope.py`) and published via `seed_display_scope(publish=True)`. Other fleet PC types still enforce from the SMB share and only report. See `docs/geenforce-api-cutover.md`.
- `__contract_version__` at 0.15.0 (0.12.0 mailer, 0.13.0 User/Role, 0.14.0 send_webhook, 0.15.0 authorized_service_token) (product `__version__` 0.7.0, tags v0.5.0/v0.6.0/v0.7.0 - distinct series, ADR-007)
- 13 bundled plugins all satisfy contract: computers, employees, geenforce, knowledgebase, machines, measuringtools, network, notifications, printedparts, printers, slides, usb, warranty
- Core Alembic chain: baseline `68b3947ae14f` -> head `7d26_settings_description_text` (33 core migrations). Each plugin owns its own chain (ADR-008); deploy runs `flask db upgrade` then `flask plugin upgrade-all`. Reproducible + idempotent from empty (env.py relaxes session sql_mode so the chain runs on strict MySQL 8).

File diff suppressed because it is too large Load Diff

View File

@@ -145,6 +145,14 @@ read back through the API.
| `printer_hostname_template` | `Printer-{ip}.printer.geaerospace.net` | Printer hostname template. `{ip}` is the dash-separated IP address. |
| `contact_email_domain` | `geaerospace.com` | Email domain appended to a support contact's SSO to build email (`sso@domain`) and Teams-chat links. Blank hides the contact action buttons. |
| `dualpath_single_machine` | `true` | Treat a Dualpath pair (a dual-bay machine with one controller) as a single machine in the machines list, dashboard/report counts, and the floor map (the secondary bay is hidden). The data model always keeps both bay records; detail pages stay per-bay with a sibling banner. `false` lists and counts both bays separately. |
| `site_timezone` | `America/New_York` | IANA timezone for the site. Notification start/end times are entered and displayed in this zone (not the viewer's browser zone), and daily-reset notification expiry (`expirymode=dailytime`) is computed here. Editable in Settings > Site > Localization. Public-readable so kiosks/clients can resolve it. |
Notification times are stored and served in UTC; the frontend converts to
`site_timezone` via `frontend/src/utils/datetime.js` (Intl-based, DST-safe).
Change note: notification times are now timezone-correct (stored UTC, shown in
`site_timezone`); this fixes the prior offset bug where a 2:34 PM entry displayed
as 6:34 PM.
### branding
@@ -319,6 +327,11 @@ One boolean key per search domain, keyed `search_<type>_enabled` (default
`true`). Toggles whether a domain appears in global search results. The set is
generated from `SEARCH_DOMAINS` in `shopdb/core/api/settings.py`.
Search terms are matched word-wise: a multi-word query returns rows containing
EVERY word, each word anywhere in the searched fields, in any order ("CSF Roles"
matches a row with "CSF" and "Roles" in different columns). Quoting does not
force a contiguous phrase.
## Custom fields
Site-defined extra attributes per asset type (Settings > Custom Fields, table

View File

@@ -106,6 +106,12 @@ retrofit it. `-NoTask` provisions identity + kit without registering the task.
Re-running it updates identity/config and re-registers the task in place.
For PC types that have cut over to HTTPS manifest delivery (currently
displays/kiosks), the full server-side setup, auth model (IP allowlist vs
ApiToken), and per-PC-type cutover playbook live in `geenforce-api-cutover.md`.
This doc covers what gets laid on the PC; that doc covers where the manifest
comes from.
---
## 5. The engine boundary

View File

@@ -65,7 +65,17 @@ creates a new version).
re-asserts each cycle, but the script is idempotent (it skips relaunch if a
kiosk process is already serving the target URL).
### display-type -> target map
### Role resolution: server first, display-type.txt fallback
The dispatcher first asks the server: `GET
/api/dashboarddefaults/display-role?fqdn=<fqdn>` (public, unauthenticated). A row
in `dashboarddefaults` keyed by the display's FQDN (IP fallback) wins and returns
the role and frontend path directly. Only when the server is unreachable or has
no mapping does the dispatcher fall back to the local `display-type.txt` map
below. To repurpose a display, edit its `dashboarddefaults` row; the change takes
effect on the next enforce cycle.
Fallback map (local file):
| display-type.txt | kiosk route | notes |
| --- | --- | --- |
@@ -73,6 +83,39 @@ creates a new version).
| `Lobby` | `/tv` | slides plugin TV dashboard (surface `lobby`) |
| `3DPrintRoom` | `/parts-kiosk` | **PLACEHOLDER, TODO-confirm** printedparts parts kiosk route; confirm the real 3D-print-room target with the floor team before publishing to production displays |
### Dashboard-defaults FQDN keying
`dashboarddefaults` rows were historically keyed by IP. Migration
`7d31_dashboarddefault_fqdn` added an `fqdn` column; resolution is now FQDN-first
with IP as fallback (`_resolve_default` in
`shopdb/core/api/dashboarddefaults.py`). FQDNs are stored lowercase. This
survives DHCP churn on kiosk subnets. `POST /api/dashboarddefaults` accepts
`fqdn`, `ipaddress`, `displayrole` (`dashboard`|`lobby`|`partskiosk`),
`businessunitid`, and `description`; `displaypath` is not stored but derived from
the role (`DISPLAY_ROLE_PATHS`). Two public read endpoints consume it:
`/api/dashboarddefaults/display-role` (dispatcher) and
`/api/dashboarddefaults/visitor-location` (lobby business-unit lookup). The
server derives a display's FQDN from its reported BIOS serial as
`F<serial>.<domain>` (`derive_display_fqdn`, domain from the `display_fqdn_domain`
setting); the dispatcher in `plugins/geenforce/seed_display_scope.py` builds the
same FQDN client-side for its lookup.
### Legacy autostart self-heal
The dispatcher also cleans up after the old GE Aerospace Dashboard / Lobby
Display Inno installers, which planted autostarts (a Public-Desktop `.lnk`, an
all-users Startup `.lnk`, and an `HKLM ...\CurrentVersion\Run` value) that
relaunch Edge at now-dead URLs (`/shopfloor-dashboard/`, `/tv-dashboard/`) and
white-screen. The 32-bit installer's Run value was WOW64-redirected into
`Wow6432Node`, which is why it survived earlier cleanup. Every enforce cycle the
dispatcher sweeps both registry views, all loaded user hives, Run/RunOnce/policy
Run keys, and every per-user and common Startup folder, matching by legacy name
and by the old URLs, then kills any old-URL Edge. The kiosk shortcut it writes is
a direct Edge shortcut (no launcher or VBS). The fix ships by re-publishing this
code-authored scope (`seed_display_scope(publish=True)`), not an import-share.
`pxe-images/github/find-legacy-kiosk-autostart.ps1` is a read-only locator for
stragglers.
## Self-sufficient: displays do NOT inherit common
The `gea-shopfloor-display` scope carries everything a display enforces. It does

View File

@@ -288,8 +288,15 @@ The engine sources the manifest and reports results using the reference kit in
(BaseUrl + a `geenforce.fetch`/`geenforce.report` service token). See
`docs/GE-ENFORCE-CLIENT.md` for the fetch/report contract, the last-known-good
cache, shadow mode, and the staged cutover from share-sourced to shopdb-sourced
manifests. Until that cutover, the client only REPORTS; the manifest still comes
from the share via Export to Share (4.2).
manifests.
The cutover from share-sourced to shopdb-sourced manifests is per PC type. The
**displays/kiosks cohort has cut over**: share-less display PCs fetch their
manifest and payloads entirely over HTTPS (see `docs/geenforce-api-cutover.md`
and `docs/GE-ENFORCE-DISPLAY.md`). All other fleet PC types (cmm, collections,
keyence, genspect, heattreat, partmarker, nocollections, common) still enforce
from the SFLD SMB share via Export to Share (4.2) and only REPORT to shopdb. The
playbook for moving the next PC type is `geenforce-api-cutover.md` section 11.
---

View File

@@ -25,6 +25,11 @@ implementation #1. Read it alongside this guide.
- `run.py` - ordered `stage_*` functions. Each reads a slice of the source,
POSTs it, and records the crosswalk later stages resolve foreign keys against.
Post-import fixups that re-point existing assets (example:
`scripts/reclassify_servers_to_network.py`, servers imported as PCs moved to
network devices in place) belong in the site loader's verify stage, not in the
stable API layer.
### Stage order matters
Reference/lookup tables first (so foreign keys resolve), then the entity hub,

View File

@@ -234,10 +234,13 @@ upload them after import via `POST /api/models/<modelid>/image` (multipart
| `pctype` | `POST /api/computers/types` | `typename` -> `computertype`, `description` | `computertype` |
| `subnettypes` | (see subnets) | used as `subnettype` string on subnets | - |
| `subnets` | `POST /api/network/subnets` | `cidr`, `description` -> `name`/`description`, `vlan` -> create VLAN first (`POST /api/network/vlans`) then `vlanid`, `subnettypeid` -> `subnettype` name | `cidr` |
| `dashboarddefaults` | `POST /api/dashboarddefaults` | `ipaddress` -> `ipaddress`, `businessunitid` (remapped), `description` | `ipaddress` |
| `dashboarddefaults` | `POST /api/dashboarddefaults` | `fqdn` (preferred key, stored lowercase), `ipaddress` (fallback key), `displayrole` (`dashboard`/`lobby`/`partskiosk`), `businessunitid` (remapped; only the `dashboard` role uses it), `description` | `fqdn`, else `ipaddress` |
| `controllertypes` | remap into `vendors` + `models` | e.g. "Fanuc" -> a Vendor; the controller model -> a Model; then set `controllervendorid`/`controllermodelid` on the machine | - |
| `comstypes` | `communicationtypes` (seeded, no API) | ensure `flask seed reference-data` created IP/Serial/USB/... before importing comms | - |
Resolution at runtime is FQDN-first with IP fallback (migration
`7d31_dashboarddefault_fqdn`); import both when the legacy source has them.
Note on communication types: the classic `comstypes.typename` values
(IP, Serial, Network_Interface, USB, Parallel, VNC, FTP, DNC) correspond to the
seeded `communicationtypes.comtype`. They are created by the reference-data seed,

View File

@@ -80,12 +80,28 @@ import API. It is site glue, not product code.
printers -> dependents -> relationships -> subnets -> usb -> verify). It is
idempotent - a crashed run resumes from `idmap.json`.
3. Reclassify servers into network devices. The classic DB stored servers as
PCs, so the import lands them as `computer` assets. Re-point them in place:
```
DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py # dry run, prints matches
DATABASE_URL=... venv/bin/python scripts/reclassify_servers_to_network.py --commit # apply
# match on an exact computer type instead of the SVR- name prefix:
... --type "Server" --commit
```
The assetid does not change: communications, relationships, map position, and
audit history carry over. Only the extension row is swapped (computers ->
networkdevices) and the asset type flipped; reclassified devices get the
`Server` networkdevicetype. Run the dry run, eyeball the list, then commit.
Re-running is safe (already-moved assets no longer match).
Expected magnitude (from the WJ dumps used in development - your fresh dumps will
differ slightly):
| entity | count |
|---|---|
| assets | ~983 (computer ~663, machine ~76, network ~58, measuring-tool ~136, printer ~50) |
| assets [*] | ~983 (computer ~663, machine ~76, network ~58, measuring-tool ~136, printer ~50) |
| locations | ~24 |
| employees | ~415 |
| installs | ~850 |
@@ -97,6 +113,16 @@ differ slightly):
| subnets | ~37 |
| USB devices / events | ~18 / ~232 |
[*] Counts taken AFTER `scripts/reclassify_servers_to_network.py --commit`.
Servers imported as computers are re-pointed to network devices, so the computer
count drops and network rises by the same amount versus a raw import.
> PLACEHOLDER - re-measure before publishing. The computer/network split shown
> in the assets row above still reflects a RAW import (pre-reclassify). Re-run
> the counts on the current prodscratch AFTER the reclassify step above and drop
> in the actual numbers; do not carry these development figures forward as if
> they already account for the reclassify.
The `verify` stage prints a source-vs-target row-count audit; the gaps are the
documented skips (inactive rows, duplicate machinenumbers, LocationOnly, the
9999 placeholder).

View File

@@ -10,7 +10,7 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
|--------|--------|-------|
| `machines` | Manufacturing machinery: 5-axis mills, lathes, broachers, heat treatment ovens | Manually entered. See [ADR-005](adr/ADR-005-equipment-vs-measuringtools.md). Subtype tables for FOCAS / CLM / MTConnect controller protocols (planned). |
| `computers` | Shop-floor PCs and engineering workstations | Fed by the PXE pipeline collector per [ADR-006](adr/ADR-006-collector-contract.md). |
| `printers` | Network and shop-floor printers | Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. |
| `printers` | Network and shop-floor printers | Public installer map page + fleet install contract (`/api/printers/install-list`, `/pc-default`, `/install-batch`; see [PRINTER-INSTALLER.md](PRINTER-INSTALLER.md)). Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. |
| `network` | Switches, routers, access points, IDFs as locations | Asset-only; cleanest of the bundled set. |
| `usb` | USB devices issued to shop-floor users | Lightweight checkout / check-in. |
| `notifications` | Shop-floor notifications, recognitions, kiosk feed | Used by `ShopfloorDashboard.vue`. |
@@ -19,9 +19,16 @@ These plugins are in `plugins/` in this repo. Enable per site with `flask plugin
| `geenforce` | GE-Enforce manifest store: imaging PC-type scopes and their install manifests (apps, scripts, files, registry, version gates) | Per [ADR-012](adr/ADR-012-geenforce-manifest-ownership.md). Served to the GE-Enforce client as JSON. Requires GE-Enforce lib >= 2.6 on target PCs. Ships `default_enabled: false`. |
| `knowledgebase` | Knowledge Base articles linking to external resources | Lightweight article store. |
| `printedparts` | 3D-printed parts inventory | Kiosk checkout / check-in. Ships `default_enabled: false`. |
| `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. |
| `slides` | Slides for the lobby display and shop-floor screensaver | Upload / reorder / delete per surface. Management gated on the `slides.manage` permission, grantable to non-admin curators; display routes are public. |
| `warranty` | Asset warranty tracking | Manual entry now, Dell / Lenovo / HP provider lookups later. Derived coverage status with report buckets. |
## Plugin permissions
Plugins may register their own permissions (for example `slides.manage`). Admins
implicitly hold them; grant them to specific roles or users to delegate curation
without admin. Each plugin's registered permissions appear in its `plugin.py`
`get_permissions()`.
## Building your own
Guides:

104
docs/PRINTER-INSTALLER.md Normal file
View File

@@ -0,0 +1,104 @@
# Printer installer map and install endpoints
How the shop-floor fleet installs network printers from shopdb-flask, replacing
the classic ASP `apiprinters.asp` / `apipcdefaultprinter.asp` / `installprinter.asp`
contract. Shopfloor 2.0 PCs cannot run unsigned `.bat` maps, so a signed
installer EXE (and the public web map page) drives installs from three endpoints
in the printers plugin.
- Server code: `plugins/printers/api/asset_routes.py`
(`printer_install_list`, `pc_default_printer`, `printer_install_batch`)
- Consumed as a fleet manifest entry: the `common` scope's `printer map`
entry (see `GE-ENFORCE-DISPLAY.md`).
All three endpoints are `@jwt_required(optional=True)`: an anonymous fleet
client works, and a logged-in browser (the public map page) works too.
---
## 1. The public map page
`PrinterInstallerMap` is a public (no-login) frontend page: the floor map with
printer hotspots positioned at each printer's `mapx` / `mapy`. The user clicks
the printers they want, and the page requests an install batch. The PC's default
printer is preselected via `pc-default`.
---
## 2. `GET /api/printers/install-list`
Flat, unpaginated list of active NETWORK printers. A printer counts as network
only if it has a hostname or a non-USB IP; USB-only printers are excluded.
Fields per row:
| Field | Notes |
|---|---|
| `printerid` | Printer id (the token `install-batch` takes). |
| `name` | Asset name, else asset number. |
| `machinenumber` | The asset number. |
| `windowsname` | Standardized Windows printer name. |
| `sharename` | Share / CSF name. |
| `hostname` | Print-queue host. |
| `ipaddress` | Primary IP (falls back to any communication row). |
| `vendorname` | Direct vendor, else the model's vendor. |
| `modelnumber` | Model name. |
| `installpath` | Installer path for this printer (see install-batch). |
| `iscsf` | CSF flag. |
| `locationname` | Location name, if the asset has one. |
| `mapx` / `mapy` | Floor-map hotspot position. |
`?format=text` returns a pipe-delimited line per printer, one printer per line,
with a fixed field order so the Inno / Pascal installer does a `split()` instead
of parsing JSON:
```
printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy
```
Any pipe or newline inside a value is neutralized to a space so the field count
stays fixed. The web map uses the default JSON.
---
## 3. `GET /api/printers/pc-default?machine=NNNN`
The PC's default printer, by machine (asset) number persisted at PXE enrollment.
Parity with classic `apipcdefaultprinter.asp`: the installer preselects a PC's
default-printer hotspot on the site-map wizard. The link is resolved through the
`defaultprinter` asset relationship (PC asset -> printer asset), so it stays
inside the contract surface (no cross-plugin model import).
Returns `{printerid, windowsname}`, or `{}` when the machine is unknown or has no
active default printer set.
`?format=text` returns one pipe-delimited line (`printerid|windowsname`), or an
EMPTY body when there is no default (so the installer's split yields nothing).
---
## 4. `GET /api/printers/install-batch?printerids=1,2,3`
Returns a self-deleting Windows `.bat` that installs the selected printers,
grouped the same way classic `installprinter.asp` grouped them:
- HP / Xerox: one universal `PrinterInstaller.exe /PRINTER="a,b,c"` call.
- Printers with a `.exe` `installpath`: a PowerShell `Invoke-WebRequest` download
(using the caller's Windows credentials, against the site base URL + the
IIS-served `/installers` folder) followed by running it `/SILENT`.
- No `installpath`, or a non-`.exe` payload (e.g. a `.zip`): listed as a manual
install rather than run blindly.
`printerids` is required, comma-separated; non-numeric tokens are ignored. An
empty / missing list is a validation error.
The install name preference is `windowsname`, else `sharename`, else the asset
name / number.
---
## 5. Fleet wiring
The `common` scope's `printer map` manifest entry (see `GE-ENFORCE-DISPLAY.md`)
lays down the signed installer that consumes these endpoints. The web map page
covers the same install flow for a human at a browser.

View File

@@ -183,6 +183,40 @@ No full doc for [E]; this note plus a CHANGELOG line covers it.
---
## 12. docs/CONFIG.md - UPDATE (site timezone) + note in notifications coverage
**Action: update.** New `site_timezone` site setting (default `America/New_York`, public), added in `shopdb/core/api/settings.py` (`build_default_settings`, category `site`) and surfaced in `frontend/src/views/settings/SiteSettings.vue` (Localization group, common-zone dropdown). Add to the site-settings section of CONFIG.md:
> `site_timezone` (site, default `America/New_York`) - IANA timezone for the site. Notification start/end times are entered and displayed in this zone (not the viewer's browser zone), and daily-reset notification expiry (`expirymode=dailytime`) is computed here. Editable in Settings > Site > Localization. Public-readable so kiosks/clients can resolve it. Stored/served UTC; the frontend converts via `frontend/src/utils/datetime.js` (Intl-based, DST-safe).
Also add a CHANGELOG line: notification times are now timezone-correct (stored UTC, shown in `site_timezone`); fixes the prior offset bug where a 2:34 PM entry displayed as 6:34 PM.
---
## 13. docs/geenforce-api-cutover.md + GE-ENFORCE-DISPLAY.md - UPDATE (legacy kiosk autostart purge)
**Action: update both.** The display dispatcher now self-heals the legacy kiosk autostarts the OLD Dashboard/Lobby Inno installers left behind. Add a subsection to the dispatcher coverage in `geenforce-api-cutover.md` (near the dispatcher pattern) and a short note in `GE-ENFORCE-DISPLAY.md`:
> ### Legacy autostart self-heal
>
> The old GE Aerospace Dashboard / Lobby Display Inno installers planted three autostarts each: a Public-Desktop `.lnk`, an all-users Startup `.lnk`, and an `HKLM ...\CurrentVersion\Run` value, all launching Edge at now-dead URLs (`/shopfloor-dashboard/`, `/tv-dashboard/`) which 404 to a white screen. Because those installers were 32-bit, the Run value was WOW64-redirected into `HKLM\SOFTWARE\Wow6432Node\...\Run`, invisible to 64-bit tooling - the reason it survived earlier cleanup. The dispatcher (`build_dispatcher_script` in `seed_display_scope.py`) now sweeps, every enforce cycle: both the native and Wow6432Node registry views, every loaded user hive (HKU), Run + RunOnce + Policies\Explorer\Run, matching by the legacy value names AND by any value pointing at the old URLs; plus every per-user and common Startup folder; then kills any running old-URL Edge. A read-only locator, `pxe-images/github/find-legacy-kiosk-autostart.ps1`, hunts all these locations (and Edge startup-URL policy, scheduled tasks, Assigned Access) when a straggler persists.
Note that the kiosk shortcut is a direct Edge shortcut (no launcher/VBS) and that the fix ships by re-publishing the code-authored `gea-shopfloor-display` scope (`seed_display_scope(publish=True)`), not an import-share.
---
## 14. docs/API-REFERENCE.md - CREATE (revise item 11 scope: link the LIVE docs)
**Action: supersedes item 11's "hand-written index only" framing.** The repo now ships generated, hosted API docs, so API-REFERENCE.md is primarily a pointer to those plus the audience/auth index:
- **Live interactive spec:** `GET /api/docs` (self-hosted Redoc), raw spec at `GET /api/docs/openapi.json` (OpenAPI 3.1, ~238 paths / ~362 ops, generated by `scripts/gen_openapi.py` from `docs/api-inventory.json`).
- **LLM/agent entry point:** `GET /api/docs/llms.txt` (llms.txt convention) and the read-only MCP server (`mcp/shopdb_mcp.py`, `FastMCP.from_openapi`; set up on a work PC via `pxe-images/github/setup-mcp.cmd`).
- Keep item 11's audience tables (fleet/client contracts, import API, core UI API) as the human index, each row linking the owning contract doc. State at top: interactive/full shapes live at `/api/docs`; contract docs hold the deep semantics; this page answers "what exists, who calls it, what auth".
Cross-link from PLUGINS.md header, DEPLOY.md security section, and `docs/llms.txt` provenance. The `docs/` blueprint lives in `shopdb/core/api/docs.py` (vendored Redoc under `staticdocs/`); mention it is a core blueprint always mounted.
---
## Out of scope for the wiki but flagged to the maintainer
- CLAUDE.md drift (test count 1077 vs 1159, phantom "lean-build job", 2026-07-13 "Current state" missing the entire HTTPS-cutover arc) is repo-doc, not wiki, but should ride the same commit.

View File

@@ -335,18 +335,51 @@ entry that got reinstalled). `Always`/no-detection entries install every cycle
by design and are not flagged, so the server-side status derivation
(`service.record_enforcement_report`: failed > selfhealed > ok) stays honest.
### The display-type.txt dispatcher pattern
### The display dispatcher: server-resolved role, file fallback
For kiosks, per-subtype behavior does not fork the scope: ONE scope
(`gea-shopfloor-display`), one inline dispatcher entry that reads
`C:\Enrollment\display-type.txt` at enforce time and acts on it. Subtype
changes are a one-file edit plus the next 15-min cycle, and retargeting a
subtype is a table edit in `seed_display_scope.py` + republish. The dispatcher
writes an all-users Startup shortcut (never Start-Process - see gotchas), and
sweeps stale kiosk launchers first (its own `ShopDB Kiosk*.lnk`, the imaging
installers' `GE Aerospace Dashboard*`/`GE Aerospace Lobby*` shortcuts, any
browser .lnk with `-kiosk`-ish args or shopdb URLs, and .url files pointing at
the kiosk routes).
(`gea-shopfloor-display`), one inline dispatcher entry (built by
`plugins/geenforce/seed_display_scope.py`) that resolves what the display should
show at enforce time, in two steps:
1. **Server (authoritative):** `GET
$KioskBaseUrl/api/dashboarddefaults/display-role?fqdn=<own-fqdn>`. This is a
PUBLIC endpoint (no token). The server matches the FQDN against the
`dashboarddefaults` table (IP fallback) and returns `{role, path,
businessunitid, businessunit}`. Roles: `dashboard`, `lobby`, `partskiosk`.
Changing a display's job is now a server-side edit; no touch on the PC.
2. **Fallback (offline, or unmapped):** the local
`C:\Enrollment\display-type.txt` value against the `DISPLAY_TYPE_TARGETS` map
baked into the script. If neither resolves, the dispatcher logs and
configures nothing.
The FQDN is built as `F<BIOS serial>.<domain>` (GE device naming); the domain
comes from HKLM `DisplayFqdnDomain` or the built-in default
(`device.geaerospace.net`). `DetectionMethod = Always`, but the script is
idempotent: it rewrites the all-users Startup shortcut (never Start-Process -
see gotchas) only when the resolved target changed.
### Legacy autostart self-heal
The old GE Aerospace Dashboard / Lobby Display Inno installers planted three
autostarts each: a Public-Desktop `.lnk`, an all-users Startup `.lnk`, and an
`HKLM ...\CurrentVersion\Run` value, all launching Edge at now-dead URLs
(`/shopfloor-dashboard/`, `/tv-dashboard/`) which 404 to a white screen. Because
those installers were 32-bit, the Run value was WOW64-redirected into
`HKLM\SOFTWARE\Wow6432Node\...\Run`, invisible to 64-bit tooling - the reason it
survived earlier cleanup. The dispatcher (`build_dispatcher_script` in
`seed_display_scope.py`) now sweeps, every enforce cycle: both the native and
Wow6432Node registry views, every loaded user hive (HKU), Run + RunOnce +
Policies\Explorer\Run, matching by the legacy value names AND by any value
pointing at the old URLs; plus every per-user and common Startup folder; then
kills any running old-URL Edge. A read-only locator,
`pxe-images/github/find-legacy-kiosk-autostart.ps1`, hunts all these locations
(and Edge startup-URL policy, scheduled tasks, Assigned Access) when a straggler
persists.
The kiosk shortcut is a direct Edge shortcut (no launcher/VBS); the fix ships by
re-publishing the code-authored `gea-shopfloor-display` scope
(`seed_display_scope(publish=True)`), not an import-share.
---