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:
2026-07-04 18:41:06 +02:00
parent 094e963e5e
commit b4f1418858
10 changed files with 532 additions and 20 deletions
+16
View File
@@ -2207,3 +2207,19 @@ all interfaces; plus correctness/hygiene (cwd-relative `.env`, `/health` always-
swallowing failures, unbounded `min_confidence`). Reassurance recorded: a forged image can't open a
barrier (server re-gates at 0.85 + debounce), content-type isn't trusted, non-root, `.env` not baked
into the image. Nothing fixed yet — this is the backlog to work from.
## [2026-07-04] update | Entry press gate: camera enforced, cooldown backstop, duplicate-plate anomaly
Field report from park-buzi: a BLINKING entry button (radar-only, no camera confirmation) still
printed a ticket — the [[button-light-indicator]] encoded blink-vs-solid but `#suppressReason`
only ever checked the radar. Fixed in [[entry-double-press]]: (1) CAMERA gate on the physical
press — live only in the lamp's SOLID state when an entry camera is configured; honors
[[entry-presence-bypass]]; suppress-only, so the camera stays advisory; (2) cooldown now a REAL
backstop behind presence (the old code returned early, so `entryCooldownSec` was dead in presence
mode) — bounds the stationary-car motion-radar-dropout double-ticket; (3) post-hoc
`entry.duplicatePlate` signed anomaly when the recognized entry plate is already open under a
recent session (entry-side twin of [[plate-reconciliation]]; ANPR stays non-blocking). REJECTED
along the way: camera-vetoed re-arm (defer re-arm until the lane flips free) — the camera's ~30 s
silence-timeout "free" never fires inside a queue, so it would suppress every queued car after the
first. Proper preventive fix noted open: a pass-through sensor (`passedInput`). 13 new tests
(`entry-press-gate.test.ts`, `entry-duplicate-plate.test.ts`); suite 258 green.