# WIKI UPDATE PLAN - shopdb-flask docs (as of HEAD ecf4ef6, 2026-07-30)

Priority order: items 1-4 are stale-on-shipped-features (fix first), 5-6 are new pages, 7-10 are minor rows/notes, 11 is structural.

---

## 1. docs/geenforce-api-cutover.md - UPDATE (and COMMIT - it is untracked)

**Action: update + `git add`.** This is the most valuable in-flight doc and the only dirty file in the repo. Committing it is step zero of this plan.

**Changes:**

a) Section 5, subsection "The display-type.txt dispatcher pattern" (line ~338): it predates commit b22701a. The dispatcher is no longer file-first; it is server-first. Rewrite the subsection opening to:

> ### The display dispatcher: server-resolved role, file fallback
>
> The inline dispatcher payload (built by `plugins/geenforce/seed_display_scope.py`) resolves what the display should show 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 derived from the hostname plus `DisplayFqdnDomain` (registry) or the built-in default domain. `DetectionMethod = Always`, but the script is idempotent: it rewrites the all-users Startup shortcut only when the resolved target changed.

b) Section 12 (Open items): add a line that the robocopy `prod-patch-geenforce` reconciliation debt now includes this very doc being untracked - remove that line after commit.

c) Add a "See also" block near the top linking `GE-ENFORCE-DISPLAY.md`, `GE-ENFORCE-CLIENT.md`, `GE-ENFORCE-DEPLOY.md`, and (new) `API-REFERENCE.md`.

---

## 2. docs/GE-ENFORCE-DISPLAY.md - UPDATE

**Action: update.** One stale claim plus one missing section.

**Changes:**

a) The "display-type -> target map" section (lines ~30, ~61-75) presents `display-type.txt` as THE role source. Retitle the section to **"Role resolution: server first, display-type.txt fallback"** and insert before the table:

> 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.

Keep the existing table, but relabel its caption "fallback map (local file)".

b) Add a new short section **"Dashboard-defaults FQDN keying"** (this is the home for missing-doc item [D]):

> ### 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`), `displaypath`, `businessunitid`. Two public read endpoints consume it: `/api/dashboarddefaults/display-role` (dispatcher) and `/api/dashboarddefaults/visitor-location` (lobby business-unit lookup). `plugins/geenforce/seed_display_scope.py` builds the client-side lookup (line ~130).

---

## 3. docs/GE-ENFORCE.md - UPDATE

**Action: update lines 288-291.** The sentence "Until that cutover, the client only REPORTS; the manifest still comes from the share via Export to Share (4.2)" is false for the displays cohort. Replace with:

> 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.

---

## 4. docs/PILOT-DEPLOY.md - UPDATE

**Action: update.** Two changes:

a) Line 88 asset-count table predates the servers-to-network reclassify [F]. The writer must re-run the counts on the current prodscratch after the reclassify script; do not hand-edit numbers. Replace the row with re-measured values and a footnote:

> 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.

b) Add a new numbered step to the import/verify flow, immediately after the import stages complete and before the parallel-validation counts:

> ### 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).

---

## 5. NEW: docs/PRINTER-INSTALLER.md - CREATE

**Action: create.** Missing-doc item [B]. This is a consumed client contract (GE-ENFORCE-DISPLAY.md:82 lists "printer map" as a fleet manifest entry) with zero coverage. Model it on COLLECTOR-INTEGRATION.md (contract doc for an external client). Draft:

> # Printer installer map and install endpoints
>
> Replaces the classic 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 (`plugins/printers/api/asset_routes.py`). All three are `@jwt_required(optional=True)`: anonymous fleet clients work, a logged-in browser also works.
>
> ## The public map page
>
> `PrinterInstallerMap` is a public (no-login) frontend page: the floor map with printer hotspots at `mapx`/`mapy`. The user clicks printers, the page requests an install batch. The PC's default printer is preselected via `pc-default`.
>
> ## GET /api/printers/install-list
>
> Flat, unpaginated list of active NETWORK printers (must have a hostname or a non-USB IP; USB-only printers are excluded). Fields per row: `printerid`, `name`, `machinenumber`, `windowsname`, `sharename`, `hostname`, `ipaddress`, `vendorname`, `modelnumber`, `installpath`, `iscsf`, `locationname`, `mapx`, `mapy`.
> `?format=text` returns a pipe-delimited line per printer (fixed field order: printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy) so the Inno/Pascal installer does a split() instead of parsing JSON.
>
> ## GET /api/printers/pc-default?machine=NNNN
>
> The PC's default printer by machine (asset) number, persisted at PXE enrollment. Resolved via the `defaultprinter` asset relationship (PC asset -> printer asset). Returns `{printerid, windowsname}`, or `{}` when unknown/none. `?format=text` returns one pipe line, or an EMPTY body for none.
>
> ## GET /api/printers/install-batch?printerids=1,2,3
>
> Returns a self-deleting Windows .bat installing the selected printers, grouped like classic installprinter.asp:
> - HP / Xerox: one universal `PrinterInstaller.exe /PRINTER="a,b,c"` call
> - printers with a `.exe` installpath: PowerShell Invoke-WebRequest download (caller's Windows creds, site base URL + IIS `/installers` folder) then run `/SILENT`
> - no installpath or `.zip`: listed as manual install
> `printerids` is required (comma-separated, non-numeric tokens ignored).
>
> ## Fleet wiring
>
> The `common` scope's `printer map` manifest entry (see GE-ENFORCE-DISPLAY.md) lays down the signed installer. Install name preference: `windowsname`, else `sharename`, else asset name/number.

Cross-link it from PLUGINS.md printers row (item 7) and GE-ENFORCE-DISPLAY.md:82.

---

## 6. docs/GE-ENFORCE-DEPLOY.md - UPDATE (minor)

**Action: update.** Add one cross-link paragraph near the ShopdbUrl/ApiToken registry contract section:

> 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.

---

## 7. docs/PLUGINS.md - UPDATE (two rows)

**Action: update.**

Line 13, printers row, new text:

> | `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). Optional Zabbix integration for supply tracking. Legacy `PrinterData` retiring per ADR-001. |

Line 22, slides row, new text:

> | `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. |

Also add a short **"Plugin permissions"** note after the table (first place any plugin permission is documented):

> Plugins may register their own permissions (e.g. `slides.manage`). Admins implicitly hold them; grant them to specific roles/users to delegate curation without admin. Each plugin's registered permissions appear in its `plugin.py` `get_permissions()`.

---

## 8. docs/IMPORT-API.md - UPDATE (one row)

**Action: update line 237.** Replace the dashboarddefaults row:

> | `dashboarddefaults` | `POST /api/dashboarddefaults` | `fqdn` (preferred key, stored lowercase), `ipaddress` (fallback key), `displayrole` (`dashboard`/`lobby`/`partskiosk`), `displaypath`, `businessunitid` (remapped), `description` | `fqdn`, else `ipaddress` |

Add a one-line note under the table: "Resolution at runtime is FQDN-first with IP fallback (migration `7d31_dashboarddefault_fqdn`); import both when the legacy source has them."

---

## 9. docs/CONFIG.md - UPDATE (one note)

**Action: update.** In the "search (dynamic)" section (line ~316), append:

> 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.

No full doc for [E]; this note plus a CHANGELOG line covers it.

---

## 10. docs/IMPORT-ADOPTION.md - UPDATE (optional, one line)

**Action: update.** In the per-site loader section, add: "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."

---

## 11. NEW: docs/API-REFERENCE.md - CREATE (proposal)

**Where:** `docs/API-REFERENCE.md`, wiki-bound alongside the rest.

**Scope:** an index, not a spec. One table per audience, each row = endpoint, auth, one-line purpose, link to the owning contract doc. Sections:

1. **Fleet/client contracts (unauthenticated or token)** - geenforce fetch/report + `/payload/<sha>` (-> GE-ENFORCE-CLIENT.md, geenforce-api-cutover.md), collector (-> COLLECTOR-INTEGRATION.md), printers install trio (-> PRINTER-INSTALLER.md), `dashboarddefaults/display-role` + `visitor-location` (-> GE-ENFORCE-DISPLAY.md).
2. **Import API** - pointer to IMPORT-API.md, do not duplicate.
3. **Core UI API** - one line: JWT-authenticated, versioned by plugin contract (0.15.0), see CONTRACT-STABILITY.md; enumerate only the public/optional-auth endpoints since those are the site-exposure surface a firewall reviewer asks about.

**Linked from:** docs/adr/README.md sibling index if one exists, PLUGINS.md header, DEPLOY.md security section (public-endpoint inventory is exactly what a deploy reviewer needs), and each contract doc's "See also". Rule to state at top: detailed request/response shapes live in the contract docs; this page only answers "what endpoints exist, who calls them, what auth".

---

## 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.
- No retirements: every existing page stays. ADR-005 must NOT be edited for [F]; ADRs are immutable and its "reclassification" is equipment/measuringtools only. The reclassify script is operational, documented in PILOT-DEPLOY.md (item 4).

Key source files for the writer: `/home/camp/projects/shopdb-flask/plugins/printers/api/asset_routes.py` (install endpoints, lines 311/432/568), `/home/camp/projects/shopdb-flask/shopdb/core/api/dashboarddefaults.py` (display-role, line 72), `/home/camp/projects/shopdb-flask/plugins/geenforce/seed_display_scope.py` (dispatcher, line ~117-151), `/home/camp/projects/shopdb-flask/scripts/reclassify_servers_to_network.py` (usage in module docstring), `/home/camp/projects/shopdb-flask/shopdb/core/api/search.py` (`_word_match`, lines 27-42), `/home/camp/projects/shopdb-flask/plugins/slides/plugin.py` (slides.manage, line 66).
