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
Pins the device-layer bugs we kept hand-verifying, as pure byte-stream assertions
(no sockets, no hardware):
- printer-escpos.test.ts (12): CP852 codepage select; the ë→0x89 / Ë→0xD3 mapping
and the em-dash/⚠ ASCII fallbacks (never a stray 0x3f "?"); and the Code128 MODULE
WIDTH contract — a short ticket id at width 3, but the ~20-char out-of-window
occurrence id at width 2 so it fits the 80mm head (width 3 overflows ~576 dots and
the firmware silently aborts the barcode). Plus the QR-and-Code128 dual encoding and
the Albanian stamp() format.
- printer-routing.test.ts (6): the failover order (booth printer is a fallback for
entry tickets; a receipt never prints on the outside dispenser), rank-then-id
tiebreak, and printWithFailover walking the order + NoPrinterAvailableError.
Wires Vitest into @parking/devices. devices 18/18 green.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V