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:
@@ -2,6 +2,7 @@ import { desc, eq, ledgerEvents, sessions, subscriptions, tariffVersions, tariff
|
||||
import { priceSession, type TariffStructure, type Tender } from "@parking/shared";
|
||||
import type { FastifyBaseLogger } from "fastify";
|
||||
import type { EventLog } from "./event-log.js";
|
||||
import { plateForIdentity, platesForIdentities } from "./plate-lookup.js";
|
||||
|
||||
// The PAY STATION: a customer pays for an open session BEFORE walking back to the
|
||||
// car (pay-on-foot — payment is decoupled from exit). Two steps:
|
||||
@@ -78,6 +79,9 @@ export interface ActiveSession {
|
||||
readonly subscriptionId: string | null;
|
||||
/** The subscriber's holder name (for a friendly label instead of the raw key). */
|
||||
readonly subscriptionHolder: string | null;
|
||||
/** Advisory licence plate recognized for this session (ANPR-on-snapshot), shown for
|
||||
* at-a-glance identification. Null when no plate was read. Never an access decision. */
|
||||
readonly plate: string | null;
|
||||
}
|
||||
|
||||
/** Booth session view: everything the pay/exit modal needs in one read. */
|
||||
@@ -104,6 +108,9 @@ export interface SessionLookup {
|
||||
readonly subscription: boolean;
|
||||
readonly subscriptionId: string | null;
|
||||
readonly subscriptionHolder: string | null;
|
||||
/** Advisory licence plate recognized for this session (ANPR-on-snapshot). Null when
|
||||
* none. Display/audit only — never an access decision. */
|
||||
readonly plate: string | null;
|
||||
}
|
||||
|
||||
export class PayStation {
|
||||
@@ -243,7 +250,7 @@ export class PayStation {
|
||||
return {
|
||||
identity: id, found: false, open: false, enteredAt: null, exitedAt: null,
|
||||
paidAt: null, amountMinor: null, currency: null, withinGrace: false, graceExpiresAt: null,
|
||||
overstay: false, subscription: false, subscriptionId: null, subscriptionHolder: null,
|
||||
overstay: false, subscription: false, subscriptionId: null, subscriptionHolder: null, plate: null,
|
||||
};
|
||||
}
|
||||
// Subscription occurrence? The entry payload carries permit:true + permitId.
|
||||
@@ -288,6 +295,7 @@ export class PayStation {
|
||||
paidAt, amountMinor, currency, withinGrace, graceExpiresAt, overstay,
|
||||
subscription: isSubscription, subscriptionId,
|
||||
subscriptionHolder: this.#holderOf(subscriptionId),
|
||||
plate: plateForIdentity(this.#db, id)?.plate ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -337,6 +345,10 @@ export class PayStation {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve advisory plates for all candidate identities in ONE device_events scan
|
||||
// (cheaper than one lookup per row).
|
||||
const plates = platesForIdentities(this.#db, byId.keys());
|
||||
|
||||
const now = Date.now();
|
||||
const out: ActiveSession[] = [];
|
||||
for (const [identity, a] of byId) {
|
||||
@@ -396,6 +408,7 @@ export class PayStation {
|
||||
subscription: isSubscription,
|
||||
subscriptionId: a.subscriptionId ?? null,
|
||||
subscriptionHolder: this.#holderOf(a.subscriptionId ?? null),
|
||||
plate: plates.get(identity)?.plate ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user