File concept pages for the operator-UI architecture ([[booth-console]]: stack, /api/ws live feed, anti-CSWSH) and [[i18n]] (per-user server-stored language; resolves a dangling code-comment link). Qualify the stale 'plain React' note on react-vite-spa. Backfill log entries for the live WebSocket, frontend foundation, and i18n builds (which had none), plus a reconciliation lint entry. Catalog booth-exit-flow + the two new pages in index; fix the concept count (27→41).
4.5 KiB
type, tags, sources, updated, status
| type | tags | sources | updated | status | |||||
|---|---|---|---|---|---|---|---|---|---|
| concept |
|
2026-06-18 | open |
Booth Console (operator UI architecture)
The operator console — the real-time UI an attendant runs at a manned booth. Built 2026-06-17/18 on top of the react-vite-spa. This page covers the architecture (stack, live feed, layout); the booth's business flows live in booth-exit-flow, shift, parking-session.
Stack (added 2026-06-17, beyond plain React)
The operator UI outgrew "plain React + useState" once it needed live updates and a real layout:
- TanStack Query owns SERVER state (fetch/cache/refetch/loading-error), wrapping the existing thin
apiFetchclient. Server data is never duplicated into client state. - TanStack Router — real routes (
/booth,/shift,/setup,/tariff,/permits,/site), role-guarded (admin-only routes redirect non-admins to/booth). Code-based route tree. - Zustand — small CLIENT state only: the live WebSocket status + a rolling in-memory event feed + the latest pushed occupancy. Anything durable is re-fetched via Query.
- Tailwind v4 with a "Bloomberg-terminal" theme (
apps/web/src/index.css,@theme): near-black surfaces, amber/green/red/cyan status accents, monospace, dense/keyboard-first. Radix primitives (Dialog, etc.) for accessible unstyled components. - react-i18next for i18n (Albanian default).
This SUPERSEDES the original "plain React, no framework" note on react-vite-spa — that held while the UI was a few admin forms; the live booth console justified the additions.
Live feed — one WebSocket (/api/ws)
The booth must reflect entries/exits/payments the instant they happen, so the console opens one
authenticated WebSocket app-wide instead of polling. See the WS tap in append-only-event-chain
(EventLog.append fires a read-side onAppended callback → the device bus emitLedger → the WS
route fans it out):
- On each signed ledger append (entry/exit/payment/void/anomaly/cash_movement/shift_*) the server pushes the event plus the recomputed capacity-occupancy (a fold over the same ledger, always authoritative). Printer-status changes (printer-status-monitoring) ride the same socket.
- The client appends to the Zustand feed for the live ticker AND invalidates the matching Query caches (events, occupancy, active-sessions) — so Query stays the source of truth; the WS is the freshness trigger. Auto-reconnect with capped backoff survives a server restart.
Auth — anti-CSWSH
The handshake is a normal GET through Fastify, so the HttpOnly JWT cookie that guards the REST API
guards the WS too. But a browser WebSocket can't send the CSRF double-submit header, which would
leave the socket open to Cross-Site WebSocket Hijacking (a malicious page opens
ws://<booth>/api/ws, the browser auto-attaches the cookie, the attacker reads the live feed). So the
WS route replaces CSRF with an Origin allowlist (same-origin always; extra origins via
WS_ALLOWED_ORIGINS for the dev SPA): a missing/cross origin is rejected before auth. The stream is
read-only — it can never mutate state. (Found + fixed by automated security review, 2026-06-17.)
The booth screen (/booth)
Dense terminal layout: a ticket input (HID-scanner-friendly — types the id + Enter) spanning the top; a left column with the occupancy gauge above the booth-exit-flow list; a right column with the live event ticker. Submitting/clicking a ticket opens the pay/exit modal (entry/duration/total, tender, voucher checkbox, entry/exit snapshots). All live-refreshed via the WS.
Dev notes
- Vite proxies
/api/ws(ws: true) to the backend; the backend's Origin allowlist must include the dev SPA origin (WS_ALLOWED_ORIGINS=http://localhost:5173). In production Fastify serves the SPA same-origin, so the allowlist isn't needed. - Start the dev SPA via
pnpm devfromapps/web(notnpx vite --host …, which has mangled args and served 404s in this environment).
Open
- No automated frontend tests — the booth/live-feed/modal logic is verified manually (Playwright + curl + DB inspection), not by a suite. The standing test-harness gap (see reconciliation-adjacent notes) now spans front and back.
- The pre-existing admin screens (Setup/Tariff/Permits/Site/Shift) still carry their old inline styles — reachable and functional, not yet on the terminal component system.