refactor(setup): unify controller I/O — event-driven relays[] + generic inputs[]
The controller new/edit modal hardcoded both its outputs and its inputs, so an
operator could neither add a generic event-driven relay nor a free-standing input
(e.g. a second radar at the exit). This unifies both into symmetric, first-class
lists. Behaviour for existing booths is unchanged (back-compat, no DB migration).
Outputs — one event→action relays[] list:
- A relay is "when EVENT X happens, do its action": entry/exit/both pulse a
barrier; a new `radarAlert` event drives a non-barrier alert lamp (blink while
its trigger input is active, SOLID once the camera confirms a car).
- Dropped the separate config.buttonLight block — the lamp is just a relays[] row
with direction:"radarAlert" (triggerInput + blink cadence). `alertRelaysOf()`
replaces `buttonLightOf()`; ButtonLightController keeps its proven 3-state
machine (serialized UDP, fail-OFF, hot-reload), now keyed per controllerId:relay
so several alert lamps on one controller run independently. Every barrier
resolver skips radarAlert rows (no auto-open; barrier-not-a-door intact).
Inputs — one first-class config.inputs[] list (the twin of relays[]):
- Each row is { input, role, relay?, kind?, activeLow?, cooldownSec? } with a
"+ Add input" button. role ∈ button | presence | alertTrigger; button/presence
name the relay they serve. An exit radar is just another presence row.
- Keystone `inputsOf(row)`: returns config.inputs[] or SYNTHESIZES it from the
legacy relays[].button/presenceInput/... fields, so relayForButton /
relayForPresence resolve identically from either shape — zero-downtime, no
migration. entry-flow.ts is unchanged (resolves through the same functions).
- Fixed a latent bug this exposed: the alert lamp's camera lock was hardcoded to
the ENTRY camera. Added relays[].lockLane ("entry"|"exit", default entry); the
lamp now locks on its own lane's camera, so an exit radar's lamp tracks the exit
camera. button-light tracks both #entryBusy/#exitBusy.
- Driver: extracted activeLowFrom(config) — merges inputs[] activeLow, legacy
relays[].presenceActiveLow, and the inputActiveLow escape hatch.
UI: the relay dropdown gained a "Radar alert" option (reveals trigger/lock/blink
inputs); InputEditor is rewritten to a generic list (role select folds loop/radar);
i18n sq+en kept at type-parity.
Tests: new device-resolve.test.ts (inputs[] resolution + legacy fallback identical
+ exit-radar resolves to the exit relay); button-light gains a two-independent-
alert-relays case and an exit-lamp lockLane case; access-dingtian gains
activeLowFrom cases. Full workspace build/lint/test green (i18n parity included).
Wiki + memory updated (button-light-indicator, entry-double-press, dingtian-relay).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
+41
@@ -1753,3 +1753,44 @@ event for this class — it's the *target filter* that matters.
|
||||
Flagged an **observability gap**: a camera with `alarmPushEnabled=true` and 0 pushes ever should be
|
||||
a surfaced status (cf. the reader-liveness fix). Recorded in the `g3h-anpr-push-gotchas` memory + a
|
||||
new troubleshooting section in [[lpr-camera]]. No code changed — diagnosis + camera reconfig only.
|
||||
|
||||
## [2026-06-28] refactor | Unified controller relays into one event→action list (drop config.buttonLight)
|
||||
Reframed the controller "Outputs — relays" model with the user: **Entry / Exit / Both are EVENTS**,
|
||||
not a "direction" — a relay is uniformly *"when EVENT X happens, do action Y"*. Barrier events
|
||||
(`entry`/`exit`/`both`) `pulseOpen`; a new **`radarAlert`** event drives a non-barrier alert lamp
|
||||
(blink while its trigger input is active, SOLID once the camera confirms a car). **Dropped the
|
||||
separate `config.buttonLight` block** — the lamp is now just another `config.relays[]` row
|
||||
(`direction:"radarAlert"`, carrying `triggerInput` + blink cadence). One list, one editor, one shape;
|
||||
a future "R4 alert" is just another row with its own trigger input — no new config, no code change.
|
||||
The proven `ButtonLightController` 3-state machine (serialized UDP, fail-OFF, hot-reload) is kept
|
||||
verbatim — only its source changed from `buttonLightOf()` to `alertRelaysOf()`, keyed per
|
||||
`controllerId:relay` so several alert relays on one controller run independently. Every barrier
|
||||
resolver skips `radarAlert` rows (no auto-open; barrier-not-a-door intact). Touched
|
||||
`device-resolve.ts`, `button-light.ts`, `device-monitor.ts`, web `api.ts` + `SetupWizard.tsx` (the
|
||||
dropdown gained a "Radar alert" option that reveals trigger/blink inputs), i18n sq+en. Tests:
|
||||
rewrote `button-light.test.ts` to the `radarAlert` row + added a two-independent-alert-relays case;
|
||||
full workspace `build lint test` green (173 server tests). Updated [[button-light-indicator]],
|
||||
[[dingtian-relay]], memory `access-direction-is-per-relay`.
|
||||
|
||||
## [2026-06-28] refactor | Generic controller inputs (config.inputs[]) — the twin of unified relays[]
|
||||
After unifying OUTPUTS into one event→action `relays[]`, did the same for INPUTS — the user hit the
|
||||
wall that **there was no way to add a free-standing input** (e.g. an EXIT radar): inputs were fields
|
||||
bolted onto an entry barrier relay (`relays[].button/presenceInput/...`) and the UI only rendered a
|
||||
button+presence block per entry/both relay. Now a first-class **`config.inputs[]`** list — each row
|
||||
`{ input, role: "button"|"presence"|"alertTrigger", relay?, kind?, activeLow?, cooldownSec? }` — with
|
||||
a "+ Add input" button. An exit radar = just another `presence` row serving the exit relay. **Keystone:
|
||||
`inputsOf(row)`** returns `config.inputs[]` or SYNTHESIZES it from the legacy per-relay fields, so
|
||||
`relayForButton`/`relayForPresence` resolve identically from either shape — **zero-downtime, no DB
|
||||
migration** (old configs keep working until re-saved; the UI seeds its editor from the synth).
|
||||
`entry-flow.ts` is unchanged (resolves through the same functions). Also fixed a latent bug this
|
||||
exposed: the alert lamp's camera **lock** was hardcoded to the ENTRY camera — added
|
||||
`relays[].lockLane: "entry"|"exit"` (button-light tracks both `#entryBusy`/`#exitBusy`; a lamp goes
|
||||
SOLID off its own lane's camera), so an exit radar's lamp locks on the EXIT camera. Driver: extracted
|
||||
`activeLowFrom(config)` (merges inputs[] `activeLow` + legacy `presenceActiveLow` + the `inputActiveLow`
|
||||
escape hatch). Touched `device-resolve.ts`, `button-light.ts`, `access-dingtian.ts`, web `api.ts` +
|
||||
`SetupWizard.tsx` (InputEditor rewritten to a generic list; role select folds loop/radar; OutputEditor
|
||||
radarAlert row gained a lock-lane select), i18n sq+en. Tests: new `device-resolve.test.ts` (inputs[]
|
||||
resolution + legacy-fallback identical + exit-radar resolves to the exit relay), exit-lamp lockLane
|
||||
case in `button-light.test.ts`, `activeLowFrom` cases in the dingtian suite. Full workspace
|
||||
`build lint test` green. Updated [[entry-double-press]], [[button-light-indicator]], [[dingtian-relay]],
|
||||
memory `access-direction-is-per-relay`.
|
||||
|
||||
Reference in New Issue
Block a user