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:
@@ -17,20 +17,26 @@ export function SnapshotStrip({ identity }: { identity: string }) {
|
||||
const [zoom, setZoom] = useState<string | null>(null);
|
||||
|
||||
const shots = data?.snapshots ?? [];
|
||||
const failures = data?.failures ?? [];
|
||||
|
||||
/** Localized direction label for a snapshot/failure tile. */
|
||||
const dirLabel = (dir: "entry" | "exit" | null): string =>
|
||||
dir === "entry" ? t("pay.snapEntry") : dir === "exit" ? t("pay.snapExit") : "—";
|
||||
|
||||
if (isLoading) return <div className="text-[11px] text-term-muted">{t("pay.loadingSnapshots")}</div>;
|
||||
if (shots.length === 0) return <div className="text-[11px] text-term-muted">{t("pay.noSnapshots")}</div>;
|
||||
if (shots.length === 0 && failures.length === 0)
|
||||
return <div className="text-[11px] text-term-muted">{t("pay.noSnapshots")}</div>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{shots.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
onClick={() => setZoom(s.id)}
|
||||
className="group flex flex-col items-center gap-1 rounded-term border border-term-border bg-term-panel-2 p-1 hover:border-term-amber"
|
||||
title={`${s.direction ?? "snapshot"} · ${new Date(s.capturedAt).toLocaleString()}`}
|
||||
title={`${dirLabel(s.direction)} · ${new Date(s.capturedAt).toLocaleString()}`}
|
||||
>
|
||||
<img
|
||||
src={snapshotImageUrl(s.id)}
|
||||
@@ -43,10 +49,25 @@ export function SnapshotStrip({ identity }: { identity: string }) {
|
||||
s.direction === "entry" ? "text-term-green" : s.direction === "exit" ? "text-term-red" : "text-term-muted"
|
||||
}`}
|
||||
>
|
||||
{s.direction ?? "—"}
|
||||
{dirLabel(s.direction)}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{/* Failed captures — a placeholder tile so an absent image is explained, not
|
||||
silently missing. Shown only when no successful shot exists for the same
|
||||
direction (the server already filters recovered captures out). */}
|
||||
{failures.map((f, i) => (
|
||||
<div
|
||||
key={`fail-${f.direction ?? "both"}-${i}`}
|
||||
className="flex h-[6.75rem] w-28 flex-col items-center justify-center gap-1 rounded-term border border-dashed border-term-amber/60 bg-term-amber/5 p-1 text-center"
|
||||
title={`${dirLabel(f.direction)} · ${f.error}${f.occurredAt ? ` · ${new Date(f.occurredAt).toLocaleString()}` : ""}`}
|
||||
>
|
||||
<span className="text-lg leading-none text-term-amber">⚠</span>
|
||||
<span className="text-[9px] uppercase tracking-wider text-term-amber">{dirLabel(f.direction)}</span>
|
||||
<span className="px-1 text-[9px] leading-tight text-term-muted">{t("pay.snapFailed")}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{zoom && (
|
||||
|
||||
Reference in New Issue
Block a user