1b54775b4d
Rework the shift screen into a master/detail view on /shift: the shift CONTROL (open/close, drawer vouchers, X-report) on top, then a two-pane history below — shift list on the LEFT, the selected shift's signed activity log on the RIGHT. - Timeframe presets replace the bare from/to inputs: Yesterday / Last week / Last month / All / Custom (custom reveals the date pickers). Filters the shift list by start time. - Activity log = every ledger event in the selected shift's [start, end] window (entries, exits, payments, vouchers, anomalies, the Z-report), rendered like the booth live feed (same EVENT_STYLE), with the shift's drawer reconciliation in the pane header. - Scope unchanged + enforced SERVER-SIDE: an operator sees only their own shifts (no operator filter); an admin (shift:cash) sees all + the operator filter. The list auto-selects the newest shift. API: /api/events gains an optional `until` (ISO) upper bound so a shift's window can be fetched ([start,end]); fetchEvents passes it. Verified on live data: a closed shift window returns just its 20 events out of 260. Build+lint 12/12 (i18n parity). The same component also backs /setup/shifts. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
62 lines
2.7 KiB
TypeScript
62 lines
2.7 KiB
TypeScript
import type { FastifyInstance } from "fastify";
|
|
import { and, desc, gte, lte, ledgerEvents, type Db } from "@parking/db";
|
|
import type { LedgerEvent } from "@parking/shared";
|
|
import { requirePermission } from "../auth.js";
|
|
import { enrichEvents } from "../event-enrich.js";
|
|
import type { EventLog } from "../event-log.js";
|
|
|
|
// Read access to the append-only signed event log. NO write/update/delete routes
|
|
// exist by design — events are only ever appended internally (entry flow, device
|
|
// pushes). Corrections are new appended events, never edits. See
|
|
// wiki/concepts/append-only-event-chain.md.
|
|
|
|
export async function eventRoutes(
|
|
app: FastifyInstance,
|
|
db: Db,
|
|
eventLog: EventLog,
|
|
): Promise<void> {
|
|
// Reading the log (the audit trail).
|
|
const guard = requirePermission("event:read");
|
|
|
|
// Recent events, newest first. `limit` caps the page (default 100, max 1000).
|
|
// Optional `since` (ISO) scopes to events at/after that instant — the booth passes
|
|
// the current shift's start so the live feed shows ONLY this shift's activity. An
|
|
// optional `until` (ISO) closes the upper bound — the shift-history screen passes a
|
|
// selected shift's [start, end] to show just that shift's signed activity log.
|
|
// (logs are per-shift, not all history). See wiki/concepts/shift.md.
|
|
app.get<{ Querystring: { limit?: string; since?: string; until?: string } }>(
|
|
"/api/events",
|
|
{ preHandler: guard },
|
|
async (req) => {
|
|
const limit = Math.min(Math.max(Number(req.query.limit) || 100, 1), 1000);
|
|
const since = (req.query.since ?? "").trim();
|
|
const until = (req.query.until ?? "").trim();
|
|
const bounds = [
|
|
since ? gte(ledgerEvents.occurredAt, since) : undefined,
|
|
until ? lte(ledgerEvents.occurredAt, until) : undefined,
|
|
].filter(Boolean);
|
|
const rows = db
|
|
.select()
|
|
.from(ledgerEvents)
|
|
.where(bounds.length ? and(...bounds) : undefined)
|
|
.orderBy(desc(ledgerEvents.index))
|
|
.limit(limit)
|
|
.all();
|
|
// Attach read-time display fields (subscriber name, advisory plate) without
|
|
// touching the signed record. One plate scan for the whole page (enrichEvents).
|
|
// The cast bridges the Drizzle row to the shared LedgerEvent.
|
|
const events = enrichEvents(db, rows as unknown as LedgerEvent[]);
|
|
return { events };
|
|
},
|
|
);
|
|
|
|
// Integrity self-check: walk the chain and verify hashes + signatures. Admin-
|
|
// only (it's an audit action). Returns the first break, or ok. This is what a
|
|
// reconciliation job / "is the log intact?" check calls.
|
|
app.get(
|
|
"/api/events/verify",
|
|
{ preHandler: requirePermission("event:read") },
|
|
async () => eventLog.verifyChain(),
|
|
);
|
|
}
|