Printer assignment end to end, and a container that keeps a site's files

Assign printers to a MACHINE and let whichever PC controls it install them, with
the observed half kept strictly apart from the assigned half: a collector report
can never write an assignment row. Adoption is explicit. Adds a singular
relationship type so defaultprinter can mean exactly one, client scripts that
converge a bay's queues and correct a drifted one in place, and a wave gate so a
fleet-wide driver rollout cannot stampede its own share.

On the container side: the compose stack never persisted the instance directory,
so a rebuild discarded plugins.json and every upload. It is a volume now, the
image ships instance/ owned by the app user so the mountpoint is writable, and
the database moves to MySQL 8.4 LTS, which the Windows runbook has recommended
since 8.0 went end of life. The image also could not build at all: the frontend
stage never copied the plugin staging script it runs.

Adds a topology migration guide covering IIS to Docker and back.
This commit is contained in:
2026-08-20 07:46:35 -04:00
parent 09e9d5f047
commit d267813999
45 changed files with 6475 additions and 56 deletions

View File

@@ -10,6 +10,154 @@ ADR-007 and ADR-002.
## [Unreleased]
### Added
- **Printer assignment has a form.** `PrinterAssignmentPicker` is the ONE picker
for both ends: MachineForm gained it (the machine is where the assignment
belongs and there was no way to set it except the relationships card), and
PCForm now uses the same component instead of its own copy, so the two sides
of an override cannot drift. PCForm also stopped reconciling row at a time
through the generic relationship endpoints, which left a PC half-assigned on
an HTTP failure; it calls
`PUT /api/printers/assignments/for-asset/<id>` instead.
- **A relationship type can declare itself singular**
(`relationshiptypes.issingular`, migration 7d34), and `defaultprinter` does.
Setting a second default REPLACES the first. The unique constraint is
(source, target, type), so two different targets were two valid rows and the
resolver took the OLDEST: a new default silently lost.
- **Observed printer queues.** `POST /api/collector/printers` (the ADR-006 hook,
no new transport and no new credential) records what a bay ACTUALLY has in a
plugin-owned table, kept strictly apart from what it is assigned. Adoption is
explicit via `POST /api/printers/assignments/seed-from-observed`, which routes
through the same reconcile path as the editor and REFUSES a queue matching no
known printer. New client script `Report-PrintersToShopDB.ps1`.
- **Wave-gated driver rollout.** `Install-ShopdbPrinterDrivers.ps1` takes
`-WaveStart`, `-Waves`, `-WaveUnit` and `-IgnoreWave`. GE-Enforce offsets each
PC by SHA256(hostname) % 5 MINUTES, which was sized for a JSON check, not a
100 MB driver set: ungated, ~300 bays pull ~30 GB inside one five-minute
window on the share the whole floor depends on. Each bay derives its wave from
its own hostname, the gate runs BEFORE the manifest is read (the manifest is
on that share too), and it fails closed on an unparseable date.
### Changed
- **MySQL 8.4 LTS** in both compose files, and as the default in
`build-offline-bundle.ps1`. 8.0 reached end of life in April 2026, which
`INSTALL-WINDOWS-IIS.md` already said while the stack still pinned it; the two
halves of the product disagreed about which server a site should run.
Verified against a real 8.4.11 server rather than by changing a tag: the core
chain and five plugin chains applied clean, 66 tables at a single
`utf8mb4_unicode_ci` collation, and the image's PyMySQL authenticates against
8.4's `caching_sha2_password` (which is why `cryptography` is pinned).
Sites upgrading an EXISTING server should note that 8.4 removes
`mysql_native_password`: an account created on 5.6 or 5.7 must be moved to
`caching_sha2_password` before the upgrade or it cannot log in afterwards.
Air-gapped sites need a fresh offline bundle, since the tarball carries the
MySQL image.
- **The database healthcheck probes over TCP** rather than the unix socket. The
entrypoint's init pass answers on the socket while running the server with
`--skip-networking`, so a socket ping reported healthy DURING init and
`depends_on: service_healthy` released `api` and `migrate` against a server
that was about to restart. Found by having it happen: a probe passed at 8s and
the next query failed because the server was mid-restart.
### Fixed
- **The image did not build.** `npm run build` fires a `prebuild` hook that runs
`node ../scripts/stage-frontend.mjs`, which stages each plugin's frontend into
the Vite tree and codegens `routes.gen.js`. The Dockerfile's frontend stage
copied `frontend/` alone and flattened it to the stage root, so that path
resolved to `/scripts` and every build since the staging script landed died on
`Cannot find module`. The stage now keeps the repo-relative layout and copies
`scripts/stage-frontend.mjs` and `plugins/` in beside it. Verified end to end:
16 plugin frontends staged, `frontend/dist` in the final image.
- **The compose stack did not persist `instance/`.** `db_data` was a volume and
the instance directory was not, so `docker compose build api && up -d api`
recreated the container and discarded `plugins.json` along with every upload:
floor plans, branding, model and application images, employee photos,
warranty proofs, slides, printed-part files and the Dell OAuth token. The
visible symptom was a site coming back with its plugins disabled. Both compose
files now mount an `instance_data` volume (the air-gap `migrate` service too,
since `plugin upgrade-all` writes `plugins.json`), and DEPLOY.md carries the
one-time rescue for a stack that predates it.
- **A drifted print queue is corrected, not just a missing one.** Queues were
matched by NAME alone, so a bay whose printer had moved or whose queue was
built on a replaced driver looked converged and printed to the wrong device.
`Set-ShopdbPrinters.ps1` now repoints a wrong port and swaps a wrong driver
IN PLACE with `Set-Printer`, so the queue keeps its name, sharing, permissions
and whoever holds it as their default. The driver is only swapped when the
wanted one is staged, and there is still no removal path in the script.
- **MachineForm's dropdowns all came up empty.** It read `.data.data` off
`computersApi.listAll()`, which already resolves to the array, so the whole
parallel load threw into the catch and the machine's own values never loaded.
- **The legacy import loader dropped `machines.printerid`**, the classic
system's record of each machine's default printer, so the production import
would have lost every one.
## [0.11.3] - 2026-08-19
Fixes the last of the buildings-and-levels bugs, and brings the shop-floor
clients into the product: the collector reporter, EventSaver and a printer
assignment feature that lets a bay install its own printers.
**Anyone on 0.11.0-0.11.2 should take this.** The hover preview reported "this
asset has a position but no level" for every asset, and the map PDF printed
markers from every floor onto one sheet.
### Fixed
- **Every hover mini-map said "no level".** When 0.11.0 gave LocationMapTooltip a
levelid, none of its seven call sites were taught to pass one - printer,
machine and PC detail, the toner report, enforcement reports, the warranty chip
and the dashboard cards. Two payloads behind them also emitted coordinates with
no level.
- **Map PDF export printed other floors' markers.** It plotted every filtered
asset onto one sheet, which is worse than the on-screen version was: nobody can
correct a sheet once it is printed and carried onto the floor.
- **The legacy import loader created undrawable markers**, sending mapleft/maptop
with no level at three call sites. That loader is the one still to run against
production.
- The build gate that should have caught all of this asked whether a FILE
mentions `levelid`, not whether each position does - one module emitted `mapx`
six times and `levelid` once and passed. It now checks per occurrence, covers
`scripts/`, and fails any Vue file that binds tooltip coordinates without a
level.
### Added
- **Printer assignment.** Printers belong to the MACHINE and reach whichever PC
controls it, so a reimaged or swapped PC comes back with the bay's printers and
nothing had to be saved off the old one. New `usesprinter` relationship type,
propagating through `controls` at read time; `GET /api/printers/for-host/<host>`
for the client; `PUT /api/printers/assignments/for-asset/<id>` to reconcile a
whole set in one call.
- **`printerdrivers.vendorid`**, so one row serves a make. HP and Xerox universal
drivers cover 41 of the reference site's 44 printers; binding a driver to a
single model meant 21 near-duplicate rows. Resolution is model, then vendor,
then the pre-vendorid name convention.
- **Client scripts** in `plugins/printers/client/`: stage a site's driver set
(`Install-ShopdbPrinterDrivers.ps1`, with `-TestOnly` for DSC), converge a
bay's queues (`Set-ShopdbPrinters.ps1`), and apply the per-user default
(`Apply-ShopdbDefaultPrinter.ps1`).
- **The collector reporter is in the repository** at
`plugins/computers/client/`, with no site in it, and the server generates a
copy carrying this site's URL and ranges: Settings > Computers > Asset
reporter, or `GET /api/computers/client-script`. The collector key is
deliberately never stamped into it.
- **EventSaver source** at `plugins/slides/client/`. The site UNC that was
compiled into it is gone; a missing ini now fails visibly rather than pointing
a new site at the reference site's file server.
- Settings: `computers_routableranges` (replaces two hardcoded VLANs) and the
buildings-and-levels admin gains editable width AND height.
### Changed
- The map editor accepts a click. Its handler was bound only if the map was
already a picker when it mounted, and the editor opens with nothing selected,
so placing a marker by hand was impossible for the life of the page.
- Global search orders every query before truncating, so the same search cannot
return a different subset twice.
## [0.11.2] - 2026-08-17
Bug fixes for the buildings-and-levels work in 0.11.0, all found by using it.