feat: explainable activity log — reasons, subscriber names, snapshot gaps
The live activity feed flagged anomalies with no explanation and showed opaque session keys. Make events self-describing and clickable. - Clickable feed rows → read-only event-detail modal: humanized fields, entry/exit snapshots, and signed-chain provenance collapsed behind an audit disclosure (operator sees the story, auditor expands for crypto). - Localized reason codes (backend i18n): the signed ledger now carries a stable REASON_CODE + params (+ English fallback) instead of free-text English. The UI translates via reason.<code> catalogs in sq/en, so an Albanian operator reads Albanian — from the same immutable event. Adding a language is a catalog change, no re-signing. (@parking/shared REASON_CODES, reasonPayload; entry/exit/subscription flows emit codes.) - Subscriber-name resolution: a SUBSESS-… occurrence now shows the subscription holder's name (fallback "Abonent"/"Subscriber"). Resolved read-time server-side (events API + WS push) as a non-signed subscriberLabel; cached with invalidation on subscription edit/delete. - Failed-snapshot visibility: a camera that was attempted but unreachable now shows a "⚠ camera unreachable" tile instead of a silent gap. The snapshots API returns failures[] from telemetry, filtered so a recovered capture shows no stale warning. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import type { TFunction } from "i18next";
|
||||
import { REASON_CODES, type LedgerEvent } from "@parking/shared";
|
||||
|
||||
// Localize a signed event's reason. The ledger signs a STABLE `reasonCode` (+ params)
|
||||
// plus an English `reason` fallback (see @parking/shared REASON_CODES). We translate
|
||||
// the code via the `reason.<code>` catalog so an Albanian operator reads Albanian and
|
||||
// an English operator reads English — from the SAME immutable event. Legacy events
|
||||
// (signed before reason codes existed) carry only `reason`, so we show that verbatim.
|
||||
|
||||
const CODE_SET = new Set<string>(REASON_CODES);
|
||||
|
||||
/** The localized reason sentence for an event, or null if it has no reason at all. */
|
||||
export function renderReason(payload: LedgerEvent["payload"], t: TFunction): string | null {
|
||||
if (!payload) return null;
|
||||
const code = typeof payload.reasonCode === "string" ? payload.reasonCode : null;
|
||||
if (code && CODE_SET.has(code)) {
|
||||
// i18next fills {{param}} from reasonParams; an absent key renders the raw token.
|
||||
return t(`reason.${code}`, (payload.reasonParams ?? {}) as Record<string, unknown>);
|
||||
}
|
||||
// No code (legacy) or an unknown code (forward-compat) → the signed English fallback.
|
||||
return typeof payload.reason === "string" ? payload.reason : null;
|
||||
}
|
||||
Reference in New Issue
Block a user