Files
shopdb-flask/docs/PRINTER-INSTALLER.md
cproudlock 3324dbd91e
Some checks failed
CI / backend (push) Failing after 9s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 7s
Buildings and levels for the floor map, and make every identifier searchable
The map was one picture of one floor. A second floor was added, the blueprint
changed size, and machines moved, so a position now records WHICH DRAWING its
coordinates belong to.

Buildings and levels (ADR-017). Each level owns its blueprint per theme and its
own native pixel size; assets.mapx/mapy are pixels of assets.levelid, not of the
site. A position whose level is unknown renders "level unknown" and is never
drawn on the default level, because a marker on the wrong floor plan looks
entirely correct while pointing at the wrong place.

Repositioning in bulk: filter by unplaced, needs-review or level, search, place,
confirm. Landmark recalibration solves the transform PER AXIS from landmark
pairs and never from image dimensions - the canvas grew taller without
rescaling, so a dimension-derived scale would stretch Y by 1.57 and be wrong
everywhere. It defaults to a dry run, reports what would land off the drawing,
snapshots before applying, and clears mapverifiedat because a transform is a
guess awaiting review. Snapshots restore, including the level and the review
state, and a restore snapshots first so an undo is undoable.

Search: gaugelabreference was matched only for measuring tools and
maintenancereference was matched nowhere at all, for any asset type, while
Settings happily offers both identifiers on machines and PCs. A tag an operator
is told to record has to be findable or it is a write-only field. USB devices
and printed items were unreachable from search entirely - neither is an asset,
so the generic asset search could not see them and no searcher existed; they
now match on serial, asset tag, label, bin code and gage-lab tag, honouring
isactive, with Settings toggles and result labels to match.

The retired-application rule was half a rule: GET /api/knowledgebase hid
articles whose topic application is retired while global search still returned
them and printed the retired application as the subject. A filter is only real
if every path that reaches the row applies it.

Contract to 0.20.0 (additive): Asset gained levelid and mapverifiedat, Location
gained levelid, and resolve_asset_position returns the levelid belonging to
whichever source supplied the coordinates. The five plugins that write a map
position are re-pinned. The install-list text format gained levelid as a NINTH
field, appended, because the shipped Pascal installer reads fields 0-7 by index.

That installer still compiles in one drawing's dimensions and bundles one
blueprint, so its map is accurate for the default level only; /api/maplevels is
deliberately unauthenticated so it can read both at runtime once rebuilt.
Recorded in PRINTER-INSTALLER.md section 6 along with the other known gaps.

Migration 7d33 converts an existing single-map site into one building and one
default level carrying the old map_* settings, then assigns every placed asset
and location to it. Nothing moves on screen. Old settings rows are kept so a
rollback still finds them. Verified end to end on MySQL 5.6 from a
production-shaped database.
2026-08-17 12:55:51 -04:00

5.8 KiB

Printer installer map and install endpoints

How the shop-floor fleet installs network printers from shopdb-flask, replacing the classic ASP 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.

  • Server code: plugins/printers/api/asset_routes.py (printer_install_list, pc_default_printer, printer_install_batch)
  • Consumed as a fleet manifest entry: the common scope's printer map entry (see GE-ENFORCE-DISPLAY.md).

All three endpoints are @jwt_required(optional=True): an anonymous fleet client works, and a logged-in browser (the public map page) works too.


1. The public map page

PrinterInstallerMap is a public (no-login) frontend page: the floor map with printer hotspots positioned at each printer's mapx / mapy. The user clicks the printers they want, and the page requests an install batch. The PC's default printer is preselected via pc-default.


2. GET /api/printers/install-list

Flat, unpaginated list of active NETWORK printers. A printer counts as network only if it has a hostname or a non-USB IP; USB-only printers are excluded.

Fields per row:

Field Notes
printerid Printer id (the token install-batch takes).
name Asset name, else asset number.
machinenumber The asset number.
windowsname Standardized Windows printer name.
sharename Share / CSF name.
hostname Print-queue host.
ipaddress Primary IP (falls back to any communication row).
vendorname Direct vendor, else the model's vendor.
modelnumber Model name.
installpath Installer path for this printer (see install-batch).
iscsf CSF flag.
locationname Location name, if the asset has one.
mapx / mapy Floor-map hotspot position, in the native pixels of levelid.
levelid Which level (drawing) those pixels belong to. Null when unplaced.

?format=text returns a pipe-delimited line per printer, one printer per line, with a fixed field order so the Inno / Pascal installer does a split() instead of parsing JSON:

printerid|windowsname|vendorname|modelnumber|hostname|ipaddress|mapx|mapy|levelid

Any pipe or newline inside a value is neutralized to a space so the field count stays fixed. The web map uses the default JSON.

Fields 0-7 are frozen. The shipped installer reads them positionally (GetField(Line, 6) is mapx), so a new field goes on the END and nowhere else - inserting one shifts every later field and the installer keeps running while reading a model number as a coordinate. levelid is field 8 for that reason, and installers built before levels existed ignore it.

An installer that ignores levelid draws every printer on whichever single blueprint it ships, which is correct only while a site has one level. See ADR-017 and section 6.


3. GET /api/printers/pc-default?machine=NNNN

The PC's default printer, by machine (asset) number persisted at PXE enrollment. Parity with classic apipcdefaultprinter.asp: the installer preselects a PC's default-printer hotspot on the site-map wizard. The link is resolved through the defaultprinter asset relationship (PC asset -> printer asset), so it stays inside the contract surface (no cross-plugin model import).

Returns {printerid, windowsname}, or {} when the machine is unknown or has no active default printer set.

?format=text returns one pipe-delimited line (printerid|windowsname), or an EMPTY body when there is no default (so the installer's split yields nothing).


4. GET /api/printers/install-batch?printerids=1,2,3

Returns a self-deleting Windows .bat that installs the selected printers, grouped the same way classic installprinter.asp grouped them:

  • HP / Xerox: one universal PrinterInstaller.exe /PRINTER="a,b,c" call.
  • Printers with a .exe installpath: a PowerShell Invoke-WebRequest download (using the caller's Windows credentials, against the site base URL + the IIS-served /installers folder) followed by running it /SILENT.
  • No installpath, or a non-.exe payload (e.g. a .zip): listed as a manual install rather than run blindly.

printerids is required, comma-separated; non-numeric tokens are ignored. An empty / missing list is a validation error.

The install name preference is windowsname, else sharename, else the asset name / number.


5. Fleet wiring

The common scope's printer map manifest entry (see GE-ENFORCE-DISPLAY.md) lays down the signed installer that consumes these endpoints. The web map page covers the same install flow for a human at a browser.


6. The shipped installer predates levels (known gap)

The Pascal installer in the inno repo (PrinterInstallerMap) still assumes one drawing per site, in two places that must change together:

  • MAP_SOURCE_W = 3300 / MAP_SOURCE_H = 2550 are compiled-in constants, and every hotspot is scaled by them. They are the dimensions of ONE level.
  • A downsized copy of that level's blueprint is bundled into the installer (880x680), so the picture is fixed at build time.

Two consequences, neither of which the installer can detect:

  1. It ignores field 8, so printers on any level are drawn on the bundled image. Coordinates from a different drawing land somewhere plausible and wrong.
  2. When a level's blueprint is replaced with one of different dimensions, the constants and the bundled image are both stale and every hotspot shifts.

The API side is ready: GET /api/maplevels is deliberately unauthenticated so the installer can fetch level dimensions and blueprint URLs at runtime rather than compiling them in, which is what fixes both. Until the installer is rebuilt against it, treat its map as accurate for the default level only.