docs(wiki): activity-log explainability, dates/i18n, KP-300H barcode fix
Record this session's work across the affected pages + three log entries. - ticket-encoding: id 13→11 digits (guess-resistance rationale, legacy-safe validation) + a barcode-geometry rule (symbol dots must fit the narrowest deployed printer's line — the KP-300H 72mm overflow). - rongta-printer: KP-300H raster-garbage root cause (line overflow, not corruption), sendRaw graceful-close fix, Albanian human dates (formatStampSq). - i18n: localized ledger reason codes, relative/human dates + the "browser ICU lacks Albanian" gotcha, toggle stale-router-context fix. - shift: Albanian Z-report, shift-history UI + permission scoping. - booth-console: explainable activity log (inline reasons/badges, event-detail modal with snapshots + audit disclosure, subscriber names, failed-snapshot tiles). - index/log updated; all added wikilinks resolve. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+12
@@ -880,3 +880,15 @@ A printed exit voucher (photo from the booth) surfaced three glitches in the new
|
||||
## [2026-06-18] feat | Dynamic RBAC — composable roles + resource×CRUD permissions
|
||||
|
||||
Replaced the hardcoded role enum (admin/operator/cashier/readonly, checked literally as requireRole("admin",...) across ~15 routes) with DYNAMIC RBAC: roles are DATA, route guards check a PERMISSION. @parking/shared now defines a code-defined grid — RESOURCES (user/role/tariff/subscription/site/device/shift/payment/session/event/report) × Action (create/read/update/delete + domain verbs void/cash) → PERMISSIONS (resource:action). DB: new `roles` + `role_permissions` tables; users.role enum → role_id FK; migration 0007_rbac (create tables, seed builtin admin role + all 26 perms, seed operator/cashier/readonly composable roles matching old behaviour, rebuild users to swap the column copying all rows). auth.ts: JWT payload role→roleId; permissionsFor(roleId) with in-memory cache + bumpPermsCache(); requirePermission(...perms) preHandler (jwtVerify+CSRF+perm check); requireAuth for /me & /language; initAuth(db) wires the resolver once in buildServer. Every route guard mapped to a permission (tariff:read/update, payment:create/read, session:read, shift:read/create/cash, site:read/update, device:read, subscription:*, event:read; ws→report:read); device ingress (devices.ts/qr-reader.ts) stays auth-free by design. New routes/users.ts (user:* CRUD, bcrypt 12, last-admin guard) + routes/roles.ts (role:* CRUD, builtin-protected, perms validated against the grid, cache bump on write). auth/me + /login now return {roleId, roleName, permissions, language}. seed-admin.mjs → roleId:'admin'. Frontend: api.ts SessionUser carries permissions + can() helper + users/roles CRUD fns; router.tsx nav/route guards gate by permission (requirePerm factory replaces adminOnly), SiteSettings edit now gated by site:update; new UsersManager.tsx + RolesManager.tsx (permission checkbox grid; admin role read-only/locked); i18n nav.users/roles + users/roles blocks (sq+en, parity green). DECISIONS (with user): one role per user; protected built-in admin (no-lockout); JWT carries roleId, perms resolved per-request (role edits apply immediately). VERIFIED: full monorepo build green; a 20-assertion inject test (cashier 403s on tariff publish + user list, admin passes, granting tariff:update to the cashier role applies on the NEXT request = cache invalidation works, last-admin delete/downgrade → 409, builtin admin role edit/delete → 409) all pass; migration 0007 applied to a COPY of the live DB (incl WAL/shm) → existing admin maps to role_id='admin', 4 roles seeded, 26 admin perms, all user rows preserved. Updated [[local-jwt-auth]]. Append-only event chain untouched (event:void gates appending a void, not a delete).
|
||||
|
||||
## [2026-06-19] feat | Explainable activity log — reasons, subscriber names, snapshot gaps
|
||||
|
||||
The booth live-feed flagged anomalies with no explanation (a red row + an id). Made events self-describing + clickable. (1) **Localized reason codes (backend i18n for the signed ledger):** reasons were free-text English baked into the immutable signed `payload.reason` → unlocalizable at render time. Now the ledger signs a stable `reasonCode` + `reasonParams` (+ English fallback) from a closed `REASON_CODES` set in @parking/shared (entry.*/exit.*/sub.* groups + `reasonPayload()` helper; emitted from entry/exit/subscription flows). UI translates `reason.<code>` via sq/en catalogs — an Albanian operator reads Albanian from the SAME immutable event; adding a language = catalog change, no re-signing. Legacy events show the signed English fallback. (2) **Clickable rows → event-detail modal:** humanized labelled fields (not raw JSON) + entry/exit snapshots + signed-chain provenance (signature/keyId/prevHash) collapsed behind an "audit data" disclosure. Rationale for showing signatures: makes tamper-evidence visible vs the booth-operator threat, survives a signer swap. (3) **Subscriber names:** a SUBSESS-… occurrence renders the holder name (fallback "Abonent"/"Subscriber"), resolved read-time server-side (events API + WS push attach a non-signed `subscriberLabel` from permitId→holder_name; cached, invalidated on sub edit/delete). (4) **Failed-snapshot visibility:** the snapshots API returns failures[] from telemetry; UI shows a "⚠ camera unreachable" tile so a missing image isn't a silent gap (surfaced a real EHOSTUNREACH on a subscriber entry camera; by design snapshot = evidence not gate, so the open proceeded). Committed f31e57b. Updated [[booth-console]], [[i18n]].
|
||||
|
||||
## [2026-06-19] feat | Human + relative dates across UI and printed slips
|
||||
|
||||
Dates were raw ISO on paper and time-only in the UI (a 2-day-old session showed just "10:48"). (1) **Printed slips** (tickets/receipts/subscription cards): `stamp()` now formats "19 Qershor 2026 10:48:25" (Albanian month, 24h+seconds) via a hardcoded `SQ_MONTHS` table; exported as `formatStampSq` so the shift Z-report shares it. (2) **Z-report** is now fully Albanian (Operatori/Nga/Deri/Para në dorë/-- Arka --/Arka e pritur…), was English-only with ISO dates. (3) **Web** sessions/logs/history: `formatRelativeDateTime` → "Sot/Today 10:48" / "Dje/Yesterday 17:33" / "17 Qershor/June 10:48". GOTCHA: the appliance browser's ICU has NO Albanian locale data — `Intl.DateTimeFormat("sq",{month:"long"})` returns English ("June"), so month names come from a `common.months` catalog array, not Intl. Also FIXED a latent bug: the SQ/EN + dark/light toggles read the active value from TanStack Router context (`useRouteContext()`), which is captured at route-resolution and does NOT re-render on setUser — so after one switch the highlight froze + switching back was blocked until a page refresh. Now driven off live state (language from i18n.language via useTranslation; theme from local useState). Committed 00f3d14. Updated [[shift]], [[i18n]].
|
||||
|
||||
## [2026-06-19] fix | KP-300H barcode line-overflow — ticket id 13→11 digits
|
||||
|
||||
The Cashino KP-300H entry dispenser printed entry tickets as RASTER GARBAGE (solid black bars/banding) while the Rongta printed the IDENTICAL byte stream fine. Diagnosed on hardware: plain-text-only prints were clean → isolated to the `GS k` Code128 barcode. ROOT CAUSE = barcode line-overflow, not corruption: a Code128-B symbol is (11·chars+35)·moduleWidth dots; the old 13-digit id at module width 3 = ~534 dots OVERRAN the KP-300H's 72mm line (512 usable dots @ 203 dpi). The Rongta runs 80mm (576 dots) and had just enough room — why only the Cashino failed. FIX: shorten the ticket id 13→11 digits (10 random + Luhn) → ~468 dots, fits 72mm; scanned the full value at the exit reader (verified). Length is driven by GUESS-RESISTANCE not volume (10^10 space, ~1-in-10^7 to hit a live open ticket vs the booth-operator threat); chose 11 over the requested 9 (10^8 → ~1-in-10^5, too weak). validateTicketCode made length-agnostic (\d{10,14}+Luhn) so legacy 13-digit tickets still validate. NB: module width must stay 3 — a width-2 test scanned but returned TRUNCATED values (partial reads logged as exit.refused.noSession anomalies). Also fixed a separate latent transport bug in sendRaw: write-then-destroy could RST mid-stream (the write callback ≠ peer-flushed) and truncate a job; now end(payload)+FIN, resolve on socket `close`, timeout-after-write = success. NOT the cause of the garbage but a real risk. Committed bbf61c4. Updated [[ticket-encoding]], [[rongta-printer]].
|
||||
|
||||
Reference in New Issue
Block a user