Files
parking_solution/apps/server/src/routes/events.ts
T
julian 1b54775b4d feat(shift): two-pane shift history — list + per-shift activity log, timeframe presets
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
2026-06-20 21:56:34 +02:00

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(),
);
}