feat: show recognized plate in live feed + active sessions
Surface the advisory ANPR plate (device_events kind="read", keyed by session identity — unsigned, prunable, never an access decision) next to entry/exit events in the live feed and on active-session rows. Resolved at serialize time (new plate-lookup.ts; prefers an entry read; one device_events scan per page) like subscriber-name enrichment — the signed ledger is untouched. Adds plate? to the shared LedgerEvent and to ActiveSession/SessionLookup; a small amber badge in the UI. Caveat: a vehicle_entry is signed + pushed over WS before the async ANPR read lands, so a fresh feed row may show no plate until reload; always present on active sessions. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { eq, subscriptions, type Db } from "@parking/db";
|
||||
import type { LedgerEvent } from "@parking/shared";
|
||||
import { plateForIdentity, platesForIdentities } from "./plate-lookup.js";
|
||||
|
||||
// READ-TIME event enrichment. The signed ledger stays minimal and stable; some fields
|
||||
// are nice to SHOW but must not be signed (they can change, or depend on other tables).
|
||||
@@ -45,12 +46,43 @@ export function clearHolderCache(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach read-time display fields to a raw ledger row before it goes to a client.
|
||||
* Currently: `subscriberLabel` for subscription occurrences. Idempotent and cheap;
|
||||
* non-subscription events pass through unchanged (no `subscriberLabel`).
|
||||
* Attach read-time display fields to a raw ledger row before it goes to a client:
|
||||
* - `subscriberLabel` for a subscription occurrence (payload.permitId → holder name);
|
||||
* - `plate` for an entry/exit event whose session has an advisory ANPR read.
|
||||
* Idempotent and cheap; events without either pass through unchanged. Used by the WS
|
||||
* feed (per event). For the bulk feed page prefer `enrichEvents` (one plate scan).
|
||||
*/
|
||||
export function enrichEvent<T extends LedgerEvent>(db: Db, event: T): T {
|
||||
let out: T = event;
|
||||
const permitId = event.payload && typeof event.payload.permitId === "string" ? event.payload.permitId : null;
|
||||
if (!permitId) return event;
|
||||
return { ...event, subscriberLabel: holderName(db, permitId) ?? SUBSCRIBER_FALLBACK };
|
||||
if (permitId) out = { ...out, subscriberLabel: holderName(db, permitId) ?? SUBSCRIBER_FALLBACK };
|
||||
if ((event.type === "vehicle_entry" || event.type === "vehicle_exit") && event.identity) {
|
||||
const p = plateForIdentity(db, event.identity);
|
||||
if (p) out = { ...out, plate: p.plate };
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk variant for the feed page: enriches a list of events with subscriber labels AND
|
||||
* plates using a SINGLE device_events scan for all the plates (instead of one per row).
|
||||
* Order preserved.
|
||||
*/
|
||||
export function enrichEvents<T extends LedgerEvent>(db: Db, events: T[]): T[] {
|
||||
// Collect identities of entry/exit events to resolve their plates in one scan.
|
||||
const wanted = new Set<string>();
|
||||
for (const e of events) {
|
||||
if ((e.type === "vehicle_entry" || e.type === "vehicle_exit") && e.identity) wanted.add(e.identity);
|
||||
}
|
||||
const plates = wanted.size ? platesForIdentities(db, wanted) : new Map();
|
||||
return events.map((e) => {
|
||||
let out: T = e;
|
||||
const permitId = e.payload && typeof e.payload.permitId === "string" ? e.payload.permitId : null;
|
||||
if (permitId) out = { ...out, subscriberLabel: holderName(db, permitId) ?? SUBSCRIBER_FALLBACK };
|
||||
if ((e.type === "vehicle_entry" || e.type === "vehicle_exit") && e.identity) {
|
||||
const p = plates.get(e.identity);
|
||||
if (p) out = { ...out, plate: p.plate };
|
||||
}
|
||||
return out;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user