/drawer was record + review only: no current balance, no sight of the open
shift's incomings, no daily activity, no shift history. Rebuilt as a hub:
- Drawer now: the till's running balance (new GET /api/drawer/balance,
shift:read — exposes the service's existing drawerBalance(); the drawer
is one site-wide till, same exposure the X-report already had) with the
open shift's X-report breakdown alongside (float + takings + vouchers =
expected = balance) and a "This shift: ±X" figure (expected − opening
float — the shift's own contribution vs what it inherited).
- Today's cash activity: every cash payment + voucher since local
midnight from the signed chain, live, with day totals (card never
enters the till).
- Record + movements/review: the 2026-07-01 flow, unchanged.
- Closed shifts: drawer-focused history via the scope-aware /api/shifts
(float → takings ± vouchers → expected per shift).
Also: every shift open/close button (header, /shifts, pay modal, end-
shift confirm) now shows an animated spinner + dims while busy — the old
label-swap-only feedback read as a dead click when a shift open ran slow.
The slowness itself (drawer/shift reads fold the WHOLE chain, O(chain))
is recorded as an open item in wiki/concepts/shift.md with the fix
sketch: fold from the last z-report's signed expectedDrawerMinor forward.
No new ledger surface — one read-only endpoint; RBAC test added.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Boots the REAL Fastify app over a fresh in-memory DB (buildServer({ db }), driven by
app.inject — no listen) to exercise the security seam end to end:
- routes.test.ts (7): /health open; login rejects bad creds and sets token+csrf
cookies on good ones; an unauthenticated GET /api/occupancy is 401; a site:read-only
role GETs occupancy but is 403 on PUT /api/site-config (the permission gate, with a
valid CSRF so the 403 is the perm check); an admin passes the same PUT; and a mutation
with the auth cookie but NO csrf header is 403 (double-submit enforced).
Adds seedUser()/login() helpers (real bcrypt + the real /api/auth/login route) and
LOG_LEVEL=silent in the vitest env so asserted 401/403 responses don't flood output.
server 75/75 green (8 suites).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V