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:
@@ -103,6 +103,67 @@ export const en: Catalog = {
|
||||
evtShiftZ: "SHIFT Z",
|
||||
evtCashMovement: "CASH",
|
||||
evtAnomaly: "ANOMALY",
|
||||
// live-feed event detail line + classification badges (computed from payload)
|
||||
evtNoReason: "no reason recorded",
|
||||
badgeEntryRefused: "entry refused",
|
||||
badgeExitRefused: "exit refused",
|
||||
badgeLotFull: "lot full",
|
||||
badgeBarrierFailed: "barrier did not open",
|
||||
badgeManualOpen: "manual open",
|
||||
badgeSubRefused: "subscription refused",
|
||||
badgeNoTicket: "ticket not printed",
|
||||
feedSourceBooth: "booth",
|
||||
feedSourceReader: "reader",
|
||||
// event-detail modal
|
||||
eventDetail: "Event detail",
|
||||
edType: "Type",
|
||||
edTime: "Time",
|
||||
edIndex: "Ledger index",
|
||||
edDirection: "Direction",
|
||||
edSource: "Source",
|
||||
edIdentity: "Identity",
|
||||
edReason: "Reason",
|
||||
edSnapshots: "Snapshots",
|
||||
edPayload: "Signed payload",
|
||||
edChain: "Chain",
|
||||
edSignature: "Signature",
|
||||
edKeyId: "Key",
|
||||
edPrevHash: "Prev hash",
|
||||
edNoPayload: "No payload on this event.",
|
||||
edCopy: "Copy",
|
||||
edCopied: "Copied",
|
||||
edDetails: "Details",
|
||||
edAuditData: "Audit data (signature & chain)",
|
||||
edAmount: "Amount",
|
||||
edTender: "Tender",
|
||||
edSession: "Session",
|
||||
edPlate: "Plate",
|
||||
edCategory: "Category",
|
||||
edOperator: "Operator",
|
||||
edTariffVersion: "Tariff version",
|
||||
edRawPayload: "Raw signed payload",
|
||||
edOccurrence: "Occurrence id",
|
||||
subscriber: "Subscriber",
|
||||
},
|
||||
// Localized messages for the signed REASON_CODES (see @parking/shared). Keys MUST
|
||||
// match the codes 1:1; {{param}} placeholders are filled from the event's
|
||||
// reasonParams. Legacy events with no code fall back to the signed English `reason`.
|
||||
reason: {
|
||||
"entry.refused.full": "Entry refused — lot full ({{count}}/{{capacity}})",
|
||||
"entry.held.noTicket": "Entry held — ticket not printed: {{detail}}",
|
||||
"exit.refused.closed": "Exit refused — session already closed",
|
||||
"exit.refused.noSession": "Exit refused — no open session for ticket",
|
||||
"exit.refused.unpaid": "Exit refused — not paid (take payment first)",
|
||||
"exit.refused.graceExpired": "Exit refused — walk-back grace expired (top-up required)",
|
||||
"exit.open.noBarrier": "Exit recorded, but no exit barrier is configured — open manually",
|
||||
"exit.open.unavailable": "Exit recorded, but the barrier is unavailable — open manually",
|
||||
"exit.open.failed": "Exit recorded, but the barrier did not open — open manually",
|
||||
"exit.freeGrace": "Free entry-grace (no charge)",
|
||||
"exit.manualOpen": "Manual barrier open (human intervention)",
|
||||
"sub.refused.notFound": "Subscription refused — not found",
|
||||
"sub.refused.outOfWindow": "Subscription refused — {{status}}/out-of-window",
|
||||
"sub.refused.noSession": "Subscription exit with no open session (already out / never entered)",
|
||||
"sub.refused.atCapacity": "Subscription refused — at capacity ({{inUse}}/{{max}} cars in)",
|
||||
},
|
||||
tariff: {
|
||||
title: "Tariff",
|
||||
@@ -446,5 +507,8 @@ export const en: Catalog = {
|
||||
reprinting: "printing…",
|
||||
noSnapshots: "no snapshots",
|
||||
loadingSnapshots: "loading snapshots…",
|
||||
snapEntry: "entry",
|
||||
snapExit: "exit",
|
||||
snapFailed: "camera unreachable",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -105,6 +105,66 @@ export const sq = {
|
||||
evtShiftZ: "TURN Z",
|
||||
evtCashMovement: "ARKË",
|
||||
evtAnomaly: "ANOMALI",
|
||||
// rreshti i detajeve të eventit live + etiketat e klasifikimit (nga payload)
|
||||
evtNoReason: "pa arsye të regjistruar",
|
||||
badgeEntryRefused: "hyrje e refuzuar",
|
||||
badgeExitRefused: "dalje e refuzuar",
|
||||
badgeLotFull: "parkimi plot",
|
||||
badgeBarrierFailed: "barriera nuk u hap",
|
||||
badgeManualOpen: "hapje manuale",
|
||||
badgeSubRefused: "abonimi u refuzua",
|
||||
badgeNoTicket: "bileta nuk u printua",
|
||||
feedSourceBooth: "kabinë",
|
||||
feedSourceReader: "lexues",
|
||||
// dritarja e detajeve të eventit
|
||||
eventDetail: "Detajet e eventit",
|
||||
edType: "Lloji",
|
||||
edTime: "Ora",
|
||||
edIndex: "Indeksi në regjistër",
|
||||
edDirection: "Drejtimi",
|
||||
edSource: "Burimi",
|
||||
edIdentity: "Identiteti",
|
||||
edReason: "Arsyeja",
|
||||
edSnapshots: "Fotot",
|
||||
edPayload: "Të dhënat e nënshkruara",
|
||||
edChain: "Zinxhiri",
|
||||
edSignature: "Nënshkrimi",
|
||||
edKeyId: "Çelësi",
|
||||
edPrevHash: "Hash-i i mëparshëm",
|
||||
edNoPayload: "Ky event nuk ka të dhëna shtesë.",
|
||||
edCopy: "Kopjo",
|
||||
edCopied: "U kopjua",
|
||||
edDetails: "Detajet",
|
||||
edAuditData: "Të dhënat e auditimit (nënshkrimi & zinxhiri)",
|
||||
edAmount: "Shuma",
|
||||
edTender: "Mënyra e pagesës",
|
||||
edSession: "Sesioni",
|
||||
edPlate: "Targa",
|
||||
edCategory: "Kategoria",
|
||||
edOperator: "Operatori",
|
||||
edTariffVersion: "Versioni i tarifës",
|
||||
edRawPayload: "Të dhënat e papërpunuara të nënshkruara",
|
||||
edOccurrence: "ID e hyrjes",
|
||||
subscriber: "Abonent",
|
||||
},
|
||||
// Mesazhet e përkthyera për REASON_CODES e nënshkruara (shih @parking/shared).
|
||||
// Çelësat përputhen 1:1 me kodet; {{param}} mbushet nga reasonParams i eventit.
|
||||
reason: {
|
||||
"entry.refused.full": "Hyrja u refuzua — parkimi plot ({{count}}/{{capacity}})",
|
||||
"entry.held.noTicket": "Hyrja u mbajt — bileta nuk u printua: {{detail}}",
|
||||
"exit.refused.closed": "Dalja u refuzua — sesioni është mbyllur tashmë",
|
||||
"exit.refused.noSession": "Dalja u refuzua — biletë e panjohur",
|
||||
"exit.refused.unpaid": "Dalja u refuzua — e papaguar (bëj pagesën në fillim)",
|
||||
"exit.refused.graceExpired": "Dalja u refuzua — afati i daljes skadoi (kërkohet pagesë shtesë)",
|
||||
"exit.open.noBarrier": "Dalja u regjistrua, por nuk ka barrierë daljeje të konfiguruar — hape me dorë",
|
||||
"exit.open.unavailable": "Dalja u regjistrua, por barriera është e padisponueshme — hape me dorë",
|
||||
"exit.open.failed": "Dalja u regjistrua, por barriera nuk u hap — hape me dorë",
|
||||
"exit.freeGrace": "Periudhë pa pagesë në hyrje (pa tarifë)",
|
||||
"exit.manualOpen": "Hapje manuale e barrierës (ndërhyrje njerëzore)",
|
||||
"sub.refused.notFound": "Abonimi u refuzua — nuk u gjet",
|
||||
"sub.refused.outOfWindow": "Abonimi u refuzua — {{status}}/jashtë afatit",
|
||||
"sub.refused.noSession": "Dalje me abonim pa sesion të hapur (tashmë jashtë / nuk ka hyrë kurrë)",
|
||||
"sub.refused.atCapacity": "Abonimi u refuzua — në kapacitet ({{inUse}}/{{max}} makina brenda)",
|
||||
},
|
||||
tariff: {
|
||||
title: "Tarifa",
|
||||
@@ -459,6 +519,9 @@ export const sq = {
|
||||
// snapshots
|
||||
noSnapshots: "asnjë foto",
|
||||
loadingSnapshots: "duke ngarkuar fotot…",
|
||||
snapEntry: "hyrje",
|
||||
snapExit: "dalje",
|
||||
snapFailed: "kamera e paarritshme",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user