fix(entry): enforce the camera press-gate + duplicate-ticket defenses
Field report (park-buzi): a BLINKING entry button still printed — the lamp encoded blink-vs-solid (radar-only vs radar+camera) but #suppressReason only checked the radar, so a radar false-positive (rain, pedestrian) minted a real signed ticket. Three layered fixes: 1. CAMERA gate on the physical press: with an entry camera configured, a press is live only in the lamp's SOLID state (LaneStatus.entry busy, mirrored into EntryFlow via onLaneStatus). Suppress-only — the camera stays advisory (never opens, never traps). Camera-less sites keep the radar-only gate; a faulty camera is dropped via the existing bypassPresenceCamera admin toggle. 2. Cooldown as a REAL backstop behind presence: the presence branch returned early, so entryCooldownSec was dead wherever a loop was wired. Now it bounds the stationary-car double-ticket (a motion radar drops a motionless car → spurious loop-clear re-arms one-car-one-ticket → same car reprints). 3. Post-hoc duplicate-plate anomaly (entry-side twin of plateSwapSuspected): when entry ANPR recognizes a plate already OPEN under another session entered within ENTRY_DUP_PLATE_WINDOW_MIN (default 15 min), sign ONE entry.duplicatePlate anomaly naming both tickets for the operator to void. ANPR stays non-blocking (rides the post-open snapshot as before). REJECTED: camera-vetoed re-arm (defer re-arm until the lane flips free). The camera has no leave events — "free" is a ~30s silence timeout that never lapses inside a queue, so every queued car after the first would be suppressed until an operator intervened. Blocking legit entry at peak beats nothing; the proper preventive fix is a pass-through sensor (passedInput) — recorded as open in wiki/concepts/entry-double-press.md. Also: setup.relayTest reason was missing from both web catalogs (parity is only enforced sq<->en, so the build passed) — added. Tests: entry-press-gate.test.ts (blink suppresses / solid prints / camera-less unaffected / bypass honored / cooldown catches the dropout re-press / residual risk documented / still-present re-press stays suppressed) + entry-duplicate-plate.test.ts (flags open dup, ignores closed/stale/self/other plates). Suite 258 green. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -374,6 +374,11 @@ export const REASON_CODES = [
|
||||
// vehicle presence (radar + camera). See wiki/concepts/operator-issued-entry.md.
|
||||
"entry.operatorIssued",
|
||||
"entry.issue.noPresence",
|
||||
// entry-side plate reconciliation: the plate recognized on a fresh transient entry is
|
||||
// already OPEN under another recent session — likely the SAME car minting a second
|
||||
// ticket (e.g. a motion radar dropped the stationary car and re-armed the button).
|
||||
// Post-hoc + advisory (ANPR never gates); the operator voids the duplicate.
|
||||
"entry.duplicatePlate",
|
||||
// exit refusals
|
||||
"exit.refused.closed",
|
||||
"exit.refused.noSession",
|
||||
@@ -421,6 +426,7 @@ export const REASON_EN: Record<ReasonCode, string> = {
|
||||
"entry.held.noTicket": "entry held — ticket not printed: {detail}",
|
||||
"entry.operatorIssued": "entry ticket issued by operator {operator} (physical button)",
|
||||
"entry.issue.noPresence": "operator entry refused — no vehicle detected at the entry",
|
||||
"entry.duplicatePlate": "possible duplicate entry — plate {plate} is already inside under ticket {otherIdentity}",
|
||||
"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)",
|
||||
|
||||
Reference in New Issue
Block a user