Files
parking_solution/wiki/concepts/printer-roles-failover.md
T
julian e14e31a840 feat(tills): per-till activity log, wash bucket on the booth Z-report, wash-desk printer role
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
2026-09-06 12:37:26 +02:00

3.4 KiB

type, tags, sources, updated
type tags sources updated
concept
parking
printer
device
reliability
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 throws NoPrinterAvailableError only when every candidate fails.
  • It attempts the print directly rather than racing a healthCheck first — 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.