Replace the single signed-± cash_movement with two distinct financial
documents — the direction is the event TYPE, not the sign of an amount:
cash_in = Mandat Arkëtimi (receipt / pay-IN, +) voucher AR-NNNN
cash_out = Mandat Pagese (disbursement / pay-OUT, −) voucher PA-NNNN
Each carries a positive magnitude, voucher number, reason, the operator who
raised it and the admin who authorized it, and prints an Albanian slip.
Authorization changes from admin-only to operator-RAISED / admin-AUTHORIZED:
any shift:create holder raises the voucher, but POST /api/cash-voucher only
commits when authorizedBy is a real admin (shift:cash) re-entering their
password (verified server-side). Keeps the float control while letting the
operator do the booth paperwork.
Legacy cash_movement events are kept — they still verify and still fold into
the drawer (signed-±); the append-only chain is never rewritten. The drawer
fold and the Z-report window now sum all three types.
Verified against a copy of the live DB with the real signing modules:
cash_in 3000 + cash_out 5000 → drawer −2000, hash-chain verifies OK.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Generalise printer-only monitoring to every configured device. New
DeviceMonitor polls all enabled devices each tick (default 8s): printers
via rich readStatus(), relays/readers/cameras via the generic healthCheck()
reachability probe, flattened to one traffic-light (ready/degraded/offline)
+ detail, deduped (emit on change only), fail-toward-offline.
- device-status bus event + GET /api/devices/status snapshot.
- Pushed over the existing /api/ws (hello carries the initial set;
device-status frame per change).
- Web: live-store devices map, WS handler, DeviceFooter chip-per-device
(role label not vendor; click a degraded/offline chip for an issues panel).
Verified roleKind resolution + change-only emit on a fresh DB.
Note: the footer's UI surface (api type, router mount, i18n devices) rides
in the subsequent subscription commit due to shared-file overlap.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
A shift becomes a SITE-WIDE accountability period — at most one open at a
time — so every taking is unambiguously attributed to one operator. Login
stays decoupled from shifts (an operator can log in off-shift to review).
Backend:
- ShiftService.currentOpenShift()/requireOpenShift(); open() refuses when ANY
shift is open and throws ShiftAlreadyOpenError{heldBy} (self vs. other).
- requireShift preHandler gates /api/pay, /api/exit, /api/voucher,
/api/barrier/reopen → 409 {code:"no_shift"}; read-only lookups stay open.
- GET /api/shift/current returns site-wide {open:{startedAt,operator},isMine}.
- GET /api/events?since=<iso> for per-shift log scoping (db: re-export gte).
Frontend:
- Header shift button: open / close-mine / disabled-when-another-holds-it.
- Pay/exit modal gate banner (one-click open; "held by X" when another's);
pay/exit/voucher disabled until this operator's shift is open.
- Active-Sessions barrier re-open gated the same way.
- Live feed scoped to the open shift's window; shared useShift() Query
invalidated over the WS on shift_open/shift_z_report/cash_movement.
- sq/en strings for the control + gate.
Wiki: shift.md (site-wide single-open + gate; superseded per-operator note),
booth-console.md (header control + gate), log entry.
Verified: site-wide invariant + heldBy + handover + chain integrity on a
fresh migrated DB (11/11); db/server/web build clean.
Add tailwindcss (Bloomberg-terminal theme in index.css), @tanstack/react-query +
react-router, zustand, and Radix primitives. Router with role-guarded routes;
QueryClient wrapping the existing apiFetch; a small Zustand live store fed by a
/api/ws client that invalidates Query caches. Booth screen: live occupancy gauge
+ streaming entry/exit/payment feed. Vite proxies the WS upgrade.
Note: BoothScreen references the pay/exit modal + active-sessions panel added in
following commits; final HEAD builds.