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.
134 lines
7.0 KiB
Markdown
134 lines
7.0 KiB
Markdown
# 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=<other>` returns 403 and
|
|
`GET /payload/<sha>` 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=<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<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
|
|
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.
|