feat(entry): admin bypass of the presence gate for faulty radar/camera
The entry button (physical press AND the operator-issued mint) requires
radar/loop presence + camera detection to confirm a real vehicle. When one
of those devices is faulty, the gate blocks legitimate transient entry. Let
the ADMIN drop a specific signal as a requirement until support fixes the
hardware — the admin is not the adversary, but weakening an anti-fraud gate
stays attributed and auditable:
- Granular: bypass radar and camera independently (Setup → controller
section). A dead camera drops only the camera check; a dead radar only
radar. Both off = normal gate; both on = press-to-print.
- Signed: a DEDICATED endpoint (PUT /api/site-config/presence-bypass,
site:update) appends a signed config_change {setting, value, prev,
operator} per actually-changed signal — new ledger type. No-op toggles
sign nothing; disabling signs too. Kept out of the generic site PUT.
- Flagged: every vehicle_entry issued (and every refusal anomaly) while
bypassed carries presenceBypassed:[...] in its signed payload.
- Persists until turned off; amber warning in Setup while active. The
booth entry light treats a bypassed signal as satisfied (server
re-checks authoritatively). Physical-button path falls through to the
cooldown backstop when radar is bypassed.
- Migration 0020: two boolean site_config columns (default off).
Fixes a latent bug surfaced by the tests: firstRelayByDirection returned no
presenceInput, so issueForOperator's radar gate always read "presence loop
unavailable" — operator-issue never actually gated on radar. The resolver
now attaches the presence input serving the relay (mirrors relayForButton).
10 new tests: 5 gate combinations (each bypass drops only its signal +
records it), 5 route tests (RBAC, signed transitions, no-op, validation).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -289,7 +289,19 @@ export function firstRelayByDirection(db: Db, direction: FlowDirection): Resolve
|
||||
(r): r is RelaySpec & { direction: Direction } =>
|
||||
r.direction === direction || r.direction === "both",
|
||||
);
|
||||
if (spec) return { controller, relay: spec.relay, direction: spec.direction };
|
||||
if (spec) {
|
||||
// Attach the presence sensor (if any) serving the SAME relay, so callers that gate on
|
||||
// presence (the operator-issued entry) see it. Without this the ResolvedRelay carried
|
||||
// no presenceInput and the presence gate read as "unavailable". Mirrors relayForButton.
|
||||
const presence = inputsOf(controller).find((i) => i.role === "presence" && i.relay === spec.relay);
|
||||
return {
|
||||
controller,
|
||||
relay: spec.relay,
|
||||
direction: spec.direction,
|
||||
presenceInput: presence?.input,
|
||||
presenceKind: presence?.kind ?? "loop",
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user