Files
shopdb-flask/docs/proposals/printer-assignment.md
cproudlock 2083029ff2
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 3s
CI / frontend (push) Successful in 10s
CI / migrations-mysql (push) Failing after 7s
Generate the collector script per site, and bring EventSaver into the repo
A site adopting ShopDB had to be handed two files and told what to edit in them.
Both are now the product's, and one of them the server writes for you.

GET /api/computers/client-script (admin) returns Report-AssetToShopDB.ps1 with
this site's values already in it: site_base_url becomes the -ApiUrl default and
the new computers_routableranges setting becomes -AllowedRanges. Only the
PARAMETER DEFAULTS are substituted - the copy in plugins/computers/client/ stays
runnable, so there is no second version to drift from the first - and everything
stamped stays overridable by argument or registry, because a bay may need to
differ from its site. Settings > Computers > Asset reporter edits the ranges,
downloads the script and shows its SHA-256.

The collector key is deliberately not stamped in, and a test fails if it ever
is. That file lands on every shop-floor PC, and a token spread across hundreds
of bays cannot be rotated quietly; it stays in the registry, provisioned per
ADOPTING-AT-ANOTHER-SITE.md.

The routable ranges are the last thing that was hardcoded in that script. They
are now a setting, so West Jefferson's two CIDRs move out of source code and
into that site's own configuration - which is what ADR-015 asks for - and a site
that sets nothing still works, because the script falls back to the NIC carrying
the default route.

EventSaver joins it in plugins/slides/client/, source only: EventSaver.cs and
EventSaver.ini, no compiled .scr - a binary is a release asset, like the
installer exe. The share path that was compiled into Config.Folder is gone. It
used to be the fallback when the ini was missing, which silently pointed a new
site at the reference site's file server; it is now empty, and failing visibly
beats displaying another site's slides. Verified by compiling the edited source
in the Windows VM with the in-box csc.exe: 15,872 bytes, exit 0.

Also: the DSC example in the adoption guide gains a CollectorRanges resource and
stops passing -ApiUrl to a script that already reads BaseUrl from the registry
the same example writes, and the guide points at the generated download instead
of hand-editing a URL.

The contract test caught the endpoint importing shopdb directly for the version
string, which ADR-002 forbids a plugin from doing. The product and contract
versions are in app.config now, which a plugin reads through current_app.

Adds docs/proposals/printer-assignment.md: assign printers to a PC in ShopDB and
let the bay install them, with what the fleet data says about drivers - HP and
Xerox cover 41 of 44 printers with universal drivers, there are no Brother
printers at all despite 208 files of Brother inkjet drivers in the installer,
and printerdrivers holds one row pointing at a per-model folder instead of a
universal driver.
2026-08-18 15:51:14 -04:00

7.3 KiB

Proposal: assign printers to a PC in ShopDB, let the PC install them

Status: PROPOSED. Not built. Author: planning session 2026-08-18.

1. What this is

Today a printer reaches a shop-floor PC because a person walks up to it, opens the printer installer, finds the printer on a floor plan and clicks it. That is fine for someone choosing a printer, and wrong for a bay whose printers are a property of the bay.

This proposal makes the assignment data: edit a PC in ShopDB, tick the printers that belong on it, mark one default. The PC converges on its next GE-Enforce cycle - installing what is missing and setting the default - and keeps converging, so a reimaged bay comes back with its printers and a bay that drifts is corrected.

The map installer stays, for the case it is actually good at: a person at an unmanaged or office PC picking a printer that nobody assigned.

2. Why it is worth doing

  • The assignment becomes a record. "Which printers does bay 2107 have" is a question ShopDB can answer, and today it cannot.
  • A reimage stops costing a visit. The bay reinstalls its own printers.
  • Drift is corrected, not just detected. A queue deleted by a user comes back.
  • It removes the walk-up from the common case. The installer's map remains for the uncommon one.

3. What already exists

Most of the model is in place, which is why this is a small feature rather than a project.

piece state
PC to printer link defaultprinter asset relationship, seeded by flask seed reference-data
Default lookup GET /api/printers/pc-default?machine=NNNN
Host lookup GET /api/computers/by-hostname/<hostname>
Printer model Printer.modelnumberid - populated for 44 of 44 printers at the reference site
Driver record PrinterDriver (name, location as SMB path or URL, optional modelnumberid)
Per-printer install path Printer.installpath
Batch install GET /api/printers/install-batch?printerids=1,2,3
Client transport GE-Enforce manifest entries, Type=PS1, running as SYSTEM every cycle
Silent driver staging Proven in PrinterInstaller.iss: trust the catalog's signing cert, then pnputil /add-driver

4. What has to be built

4.1 One endpoint

GET /api/printers/for-host/<hostname>

Returns the printers assigned to that PC and which is default, each with what a client needs to install it: queue name, host or IP, port, driver name, driver location.

Resolved by hostname, not machine number: the collector already upserts PCs by hostname, and an office PC has no machine number.

Per-PC assignments must NOT go in the manifest. Manifests are keyed by scope and PC type and sync broadly; putting per-PC rows there would leak every bay's configuration to every bay and grow without limit. One manifest entry runs one script that asks the API what THIS host gets - the mirror image of Report-AssetToShopDB.ps1.

4.2 Two columns on printerdrivers

  • drivername - the driver's exact name as the INF declares it, e.g. HP Universal Printing PCL 6. Add-PrinterDriver needs it verbatim, and a mismatch is the usual failure. Deriving it by parsing the INF on hundreds of bays is fragile; a human confirming it once in ShopDB is not.
  • installmethod - pnputil or dpinst. See section 6: if Brother really is absent from the fleet, everything is pnputil and this column can wait.

4.3 UI on the PC form

A printer picker writing defaultprinter (one) and an assignment list (many). AssetRelationships.vue already edits relationships; this is a narrowed case of it.

4.4 One client script, in two contexts

Set-ShopdbPrinters.ps1, shipped in plugins/printers/client/ beside the contract it consumes, and run as a manifest entry with DetectionMethod=Always.

As SYSTEM, every cycle:

  1. GET /api/printers/for-host/$env:COMPUTERNAME
  2. For each assigned printer with no queue: trust the driver catalog's cert, pnputil /add-driver, create the port, create the queue
  3. Write the desired default to HKLM:\SOFTWARE\GE\ShopDB DefaultPrinter
  4. Ensure the per-user task exists

In the user's context, at logon and on a repeat:

  1. Read that value, compare with the current default, set it if it differs, and clear "Let Windows manage my default printer" - otherwise Windows silently overrides the choice the next time someone prints elsewhere

The default printer is per-user state, which is the only reason this needs two contexts. Everything else is machine state and belongs to the cycle.

Converge, do not reinstall: when the state matches, the script does nothing. Nothing here needs the manifest to know when a printer changes, because the desired state is fetched, not declared.

5. Decisions to take before writing code

  1. Never remove a queue by default. A transient API failure would otherwise strip printers fleet-wide. Deletion is an explicit opt-in, per PC.
  2. Enforced or set-once for the default? Re-applying every cycle overrides a user who chose their own default - correct for a locked bay, irritating on an office PC. Set-once is Active Setup or RunOnce. Make it a per-PC-type flag rather than one global answer.
  3. Failure is silent and safe: unreachable API means change nothing, log, exit 0 - the convention Report-AssetToShopDB.ps1 already follows.

6. What the fleet data says, and the one prerequisite

The reference site's 44 printers are HP 26, Xerox 15, Zebra 1, HID 1, Epson 1.

  • HP and Xerox are 41 of 44, and both have true universal drivers (HP UPD, Xerox Global Print Driver). One driver record each serves every queue of that make.
  • There are no Brother printers at all, yet the installer carries 208 files of per-model Brother MFC-J inkjet drivers. Those are host-based GDI devices with no Printer-class INF, which is the only reason a second staging method (DPInst) exists. If production confirms no Brother, that payload and that code path can both go.
  • Zebra, HID and Epson are one printer each, and the HP DesignJet plotter is a fourth special case - a PostScript device the UPD does not cover.

Prerequisite: populate printerdrivers. It currently holds ONE row, and it points at a per-model folder (HP LaserJet Pro M607 Driver) rather than the universal driver - the opposite of how a UPD should be used. The table needs roughly four rows: HP UPD, Xerox GPD, one per oddity, and DesignJet when its payload is restored. Nothing in this proposal works until a printer can resolve to a driver.

7. Deployment constraint that shapes the design

The SFLD share is mounted only during GE-Enforce's cycle. Any work touching a share path must run as a manifest entry inside that cycle, never as its own scheduled task. The failure is silent - the task reports 0 processed, 0 installed, 0 failed - and it has cost a session before.

This is why driver staging belongs in the cycle even though the per-user default does not, and why "the assignment script schedules a task that installs drivers" is the wrong shape.

8. What this does not change

  • The printer installer keeps working, for walk-up and self-service.
  • Nothing about how printers are modelled, mapped or reported.
  • The collector contract.
  • Sites not running GE-Enforce: the same endpoint suits an Intune remediation or a DSC Script resource, since it is a plain HTTP GET and a PowerShell script.