Closes the three known follow-ups of the Tills decision (venue-modules.md): - Activity log per till: `tillOfEvent(type, payload)` in @parking/shared (money events by payload till, other events by their owning module's till, everything else booth), applied by `/api/events?till=` in SQL and passed by the hub log, the Drawer "today" panel and the booth feed (history + live pushes). The events route admits a role that holds a module feed permission without event:read and returns only that module's event types — the live-socket rule. - Booth Z-report: `chargesByModuleMinor` sums the chargeLines on the till's payments by module; the ticket bucket excludes them (Bileta = parking only); printed "Lavazh (në biletë)" only when any was taken. The wash till's slip prints "Lavazh:". - Printer role `wash-desk`: the wash till's Z-report and vouchers print there, falling back to the booth printer; nothing falls back to the desk. `printerRoleOf()` is the one reading of the role field (the entry/booth loaders treated any non-booth role as an entry dispenser). Footer label "at wash desk". Also: `GET /api/carwash/settings` opens to carwash:read OR site:read (new requireAnyPermission) — the Wash operator job could not load the desk's category and service pickers. Tests for all four; wiki (shift, printer-roles-failover, venue-modules, log) updated. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
3.4 KiB
type, tags, sources, updated
| type | tags | sources | updated | ||||
|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-14 |
Printer roles & failover
A lane runs more than one printer, and the system knows each one's job so it can fail over automatically. This is a reliability decision, not a threat-model one: an entry ticket must still print when the outside dispenser jams or drops off the network.
Roles
Each printer instance (a devices row, category printer) declares a role in its
config:
entry-dispenser— outside, at the lane. Prints the entry ticket the driver takes.booth-receipt— inside the booth. Prints receipts at exit/payment, the booth till's Z-reports and vouchers, AND serves as the backup for entry tickets (and for wash slips).wash-desk— at the Car Wash desk (added 2026-09-06). Prints the wash till's slips: its Z-report and drawer vouchers (see shift §Tills). Nothing else ever prints here.
It also declares a failoverRank (higher = preferred within a role) to order multiple
printers of the same role deterministically (ties broken by id).
Failover rule (asymmetric, on purpose)
For an entry ticket (wantRole = entry-dispenser): try the entry dispensers (best rank
first), then fall back to the booth printer. So a driver still gets a ticket when the
outside unit is offline — the operator hands it over from the booth.
The reverse is deliberately not done: a receipt never prints on the outside dispenser. Receipts are a booth-only job; an entry dispenser falling back to print receipts makes no physical sense.
For a wash slip (wantRole = wash-desk): the desk printers first, then the booth
printer — a site that has not bought a desk printer keeps printing the wash till's Z-report
and vouchers in the booth, exactly as it did before the role existed. Nothing falls back to
the wash desk: a booth receipt or an entry ticket never prints there. ShiftService resolves
the role from the till (TILL_PRINTER_ROLE: booth → booth-receipt, carwash → wash-desk)
and keeps one legacy fallback — a booth with a single printer that carries no booth role still
prints its slips on it. printerRoleOf(config) is the one reading of the saved role field,
so every loader (entry flow, booth receipts, shift slips) agrees on what a printer is; the
device footer shows a desk printer as "at wash desk".
Where the logic lives
- The driver (
rongta) is role-agnostic — role/rank are just config; the transport doesn't care. Keeps device-adapter-pattern swappable. - Selection is pure logic in
packages/devices/printer-routing.ts:orderForRole()ranks candidates;printWithFailover()attempts the print down the list and throwsNoPrinterAvailableErroronly when every candidate fails. - It attempts the print directly rather than racing a
healthCheckfirst — the print is the real reachability test, and a health probe that passes can still be followed by a failed print.
Open: the all-printers-down policy
When printWithFailover exhausts every candidate, what should entry do — raise the barrier
with no paper ticket (the plate/lpr-camera is the independent record), or hold? That policy
belongs to the entry flow (device-input-flow, fail-state-safety), not the printer
layer, and is not yet decided. The signed event (append-only-event-chain) is created
regardless of whether paper prints.