feat(setup): operator-tested relay pulse, signed into the ledger
Add a per-relay "Test" control on each saved controller in /setup so an admin can prove barrier wiring without a vehicle. POST /api/setup/test-relay pulses a barrier relay — but because a physical open with no matching signed command is the fraud signal, the route SIGNS a barrier_open_command (reason setup.relayTest, source manual, attributed to the acting admin) BEFORE it fires. Reconciliation then reads the open as explained, not an anomaly, and there's an audit trail. - Admin-only (site:update), CSRF-guarded; fires only against a SAVED controller (real id → clean attribution; also stops a redirected/unsaved config from opening an arbitrary host's barrier). Sign-before-fire; a pulse failure is reported, not a 500. radarAlert relays (lamps) are excluded from the UI. - New reason code setup.relayTest in @parking/shared (+ EN template); sq/en keys. - EventLog constructed before setupRoutes so the route can sign. - Integration test (stub controller, no hardware): RBAC 403, CSRF 403, signed barrier_open_command on success, 400 unknown relay w/ no ledger row, 404 unknown controller, 400 bad relay value. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -395,6 +395,9 @@ export const REASON_CODES = [
|
||||
"sub.refused.unpaidWindow",
|
||||
// a wrongly-printed transient ticket cancelled by the operator (signed void event).
|
||||
"void.ticketCancelled",
|
||||
// an admin fired a barrier relay from Setup to test the wiring. The physical open is
|
||||
// DELIBERATE — signing it keeps reconciliation from reading it as an out-of-band open.
|
||||
"setup.relayTest",
|
||||
] as const;
|
||||
|
||||
export type ReasonCode = (typeof REASON_CODES)[number];
|
||||
@@ -428,6 +431,7 @@ export const REASON_EN: Record<ReasonCode, string> = {
|
||||
"sub.refused.atCapacity": "subscription refused — at capacity ({inUse}/{max} cars in)",
|
||||
"sub.refused.unpaidWindow": "exit refused — out-of-window charge unpaid ({amount} {currency} owed); pay at the booth",
|
||||
"void.ticketCancelled": "ticket cancelled — {reason}",
|
||||
"setup.relayTest": "relay test — admin {operator} pulsed relay {relay} on controller {controller} from Setup",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user