# 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/` | | 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/ ``` 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:* 5. 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.