# GE-Enforce: the gea-shopfloor-display scope Displays are the share-less corner of the fleet. They are Entra-joined, credential-less kiosk PCs that pull their manifest over HTTPS on port 443 and authenticate with a read-only service PAT scoped `geenforce.fetch`, sent as `X-API-Key`. They have no SMB share mount. The kiosk engine and the kiosk browser are baked into the display image, not shipped over HTTPS, so the display manifest heals POLICY / CONFIG drift only, never EXEs. It is self-sufficient and does not inherit the fleet-wide `common` scope (see below). ## The display fetch token MUST be resource-bound The same read-only key ships to every display (delivered by DSC, or baked into the image), so it must not be a skeleton key for the whole content store. Mint the display token bound to just this scope, so a leak cannot pull any other scope's manifest or any blob by hash: ``` POST /api/apitokens { "name": "display fetch", "scopes": ["geenforce.fetch"], "resourcescopes": ["gea-shopfloor-display"] } ``` With `resourcescopes` set, `GET /manifest?pctype=` returns 403 and `GET /payload/` returns 404 for any blob the display scope does not reference. `resourcescopes` NULL (unset) = unrestricted, for back-compat with existing service tokens. Rotate by minting a new bound token and revoking the old one (deactivate it server-side); DSC re-delivers, or re-image. There are three display subtypes, selected by `C:\Enrollment\display-type.txt`: `Dashboard`, `Lobby`, and `3DPrintRoom`. ## Authoring the scope The scope is authored programmatically by `plugins/geenforce/seed_display_scope.py`, which builds a manifest dict and hands it to `service.replace_scope_draft` (the same call the `import-share` CLI uses), then attaches the inline dispatcher payload. From a Flask app context: ```python from plugins.geenforce.seed_display_scope import seed_display_scope seed_display_scope(publish=True) # publish=False leaves it as a draft ``` `replace_scope_draft` is an idempotent draft rebuild. `publish=True` additionally freezes an immutable published snapshot (that step is not idempotent: it always creates a new version). ### What the scope contains 1. Four `Registry` drift-heal entries that re-assert the Microsoft Edge kiosk relaunch policies set at imaging by `09-Setup-Display.ps1`. Each writes the value and detects drift with `DetectionMethod = ValueMatches` against the same path/name, so a display that loses a policy self-heals on the next enforce cycle with no keyboard or mouse on site: - `RelaunchNotification = 2` (DWord, Required auto-restart) - `RelaunchNotificationPeriod = 3600000` (DWord, 1 hour) - `RelaunchHeadsUpPeriod = 60000` (DWord, 1 minute) - `RelaunchWindow` (String, JSON, 02:00 start, 120 minute duration) 2. One `PS1` dispatcher, delivered inline over HTTPS. It reads `C:\Enrollment\display-type.txt` and launches the kiosk target for the subtype. The subtype -> route map is a data-driven table (`DISPLAY_TYPE_TARGETS`) at the top of both the seed module and the generated script, so targets are easy to edit. `DetectionMethod = Always` so it re-asserts each cycle, but the script is idempotent (it skips relaunch if a kiosk process is already serving the target URL). ### Role resolution: server first, display-type.txt fallback The dispatcher first asks the server: `GET /api/dashboarddefaults/display-role?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 | | --- | --- | --- | | `Dashboard` | `/shopfloor` | core ShopfloorDashboard, standalone full-screen | | `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.` (`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. `/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 NOT inherit the fleet-wide `common` scope. Displays run the enforcer with common-merge off (the client default; common-merge is opt-in via `Invoke-ShopdbEnforce.ps1 -IncludeCommon`), so `common`'s SMB-backed fleet entries (Adobe, Oracle, OpenText, Defect Tracker, EventSaver, printer map, self-update, asset-reporting, ...) never reach a share-less display. This was a deliberate decision: a display needs none of common's software, and inheriting common would have forced repackaging every SMB `common` payload as `http`/`inline` for a share-less box. Keeping the display scope self-sufficient avoids all of that. If a future non-display share-less PC genuinely needs the fleet-wide entries, that is what `-IncludeCommon` plus a per-entry SMB->http payload conversion would be for -- but displays do not use it.