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.
7.0 KiB
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:
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
- Four
Registrydrift-heal entries that re-assert the Microsoft Edge kiosk relaunch policies set at imaging by09-Setup-Display.ps1. Each writes the value and detects drift withDetectionMethod = ValueMatchesagainst 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)
- One
PS1dispatcher, delivered inline over HTTPS. It readsC:\Enrollment\display-type.txtand 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 = Alwaysso 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.