devices: pool-of-spaces model — drop lane, per-relay direction

A parking lot is one pool of spaces with a flexible set of entry/exit
points — no "lane". Direction is a property of each RELAY inside an access
controller; readers/cameras bind to a controller relay and inherit it.

Schema:
- drop `lane` from ledger_events, device_events, sessions
- rename lane_devices -> devices (no lane/direction columns)
- access config.relays=[{relay,direction,button?}]; reader/camera
  config.controllerId+relay binding
- fresh 0000_baseline migration (history reset; dev data was throwaway)

Signed ledger:
- remove `lane` from canonicalize(); bump signer keyId sw-hmac-v1 -> v2
  (v1 events won't verify under v2 — intentional, gated per-event by keyId)

Server:
- new device-resolve.ts (replaces lane-map.ts): relayForButton,
  relayForDevice, firstRelayByDirection, devicesByDirection
- entry-flow: button terminal -> its relay; exit/permit: reader's bound
  relay; dispatcher resolves the bound relay + inherited direction
- camera snapshots fire by direction site-wide, async, never block open
- DeviceConfig widened to nested JSON for relays[]

Web:
- wizard: no lane selector; add controllers (relay map + entry-button
  terminal) first, then bind readers/cameras/printers to a controller relay

Wiki: new entry-exit-points.md (replaces lane-direction); reworked
entry-exit-readers, parking-session, first-run-setup, device-registry,
append-only-event-chain, device-events; removed stale lane/LaneMap mentions.
This commit is contained in:
2026-06-16 20:29:38 +02:00
parent 15d3e1ba08
commit 1efa77bf56
46 changed files with 1221 additions and 1167 deletions
+44
View File
@@ -700,3 +700,47 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
`Connection: close` + shuts the socket.
- Fix: endpoint sets `reply.header("connection","close")`. Verified the header is now sent.
- Updated [[gee-qr-er80]] (⚠️ Connection: close requirement).
## [2026-06-16] feature | lane direction (per-device entry/exit) + camera snapshots
- Direction is per **device**, not per lane: added `direction` (`entry`/`exit`/`both`, default
`both`) to `lane_devices`. A lane's entry set = devices tagged entry|both, exit set likewise —
no join table; tagging in the wizard IS the grouping. Rejected a lane-level `lanes` table (can't
model one bidirectional lane). New [[lane-direction]] concept page; cross-linked [[entry-exit-readers]].
- All flows now resolve via `deviceRowsFor(lane, category, direction)` (lane-map.ts), replacing the
ad-hoc "first access on lane" lookups. Dispatcher trusts the **reader's own direction**; a
directional reader that contradicts the car's session state is a wrong-lane/anti-passback refusal.
`both` keeps the old infer-from-session behavior.
- Relay open channel is now config-driven (`config.openChannel`, default 1) since a lane can hold
an entry **and** an exit relay. LPR stays a snapshot sink — ANPR POSTs a `plate` read to the
reader endpoint (no camera-as-input coupling).
- Camera snapshots wired into all four paths (transient/permit × entry/exit): fired AFTER
pulseOpen, **never awaited** (evidence, not a gate — camera failure can't block an open). Stored
as BLOB in a new `snapshots` table (not files); telemetry `kind:"snapshot"` device_event per
capture/failure; linked to the signed event by `identity`. Served via `GET /api/snapshots/:id`.
- Migration `0002_wild_odin.sql` (additive). Snapshot **retention** left unresolved →
[[open-questions]] #10. Whole monorepo typechecks; no test suite exists in-repo.
## [2026-06-16] redesign | SUPERSEDES the above — pool-of-spaces, per-relay direction, NO lane
- User correction: direction is NOT a property of a device row. One Dingtian board has 2+ relays;
a single board drives both the entry barrier (relay 1) and the exit barrier (relay 2), and a
single relay can even serve **both**. So the row-level `direction` from the entry above was wrong.
- Further: the whole **"lane" concept was dropped**. Occupancy is site-wide, device grouping is now
the reader→relay binding, and anti-fraud never used lane. A parking lot = **one pool of spaces**
with a flexible set of entry/exit points (1 in + 2 out, etc). New [[entry-exit-points]] page
(replaces lane-direction); reworked [[entry-exit-readers]], [[parking-session]], [[first-run-setup]],
[[device-registry]].
- Model now: access `config.relays = [{ relay, direction: entry|exit|both, button? }]` (`button` =
the input terminal the entry button is wired to). Readers/cameras `config.controllerId + relay`
bind to the barrier they sit at; direction inherited ("the relay at that reader" opens on a read).
- Schema: dropped `lane` from `ledger_events`, `device_events`, `sessions`; renamed `lane_devices`
→ `devices` (no lane/direction columns). `lane` was in the SIGNED canonical form, so canonicalize()
dropped it and the signer keyId bumped **sw-hmac-v1 → sw-hmac-v2** (v1 events won't verify under
v2 — intentional, gated by per-event keyId; done pre-deployment on throwaway data). Migration
history reset to a fresh `0000_baseline` (dev DBs deleted + re-migrated).
- Resolvers in new `device-resolve.ts` (replaces lane-map.ts): `relayForButton`, `relayForDevice`,
`firstRelayByDirection`, `devicesByDirection`. `DeviceConfig` widened to nested JSON for `relays[]`.
- Wizard rewritten: no lane selector; Controllers section (relay map + entry-button terminal per
relay), then readers/cameras/printers bind to a controller relay. Whole monorepo typechecks +
builds; no test suite in-repo.
- Residual: incidental `lane_devices` / "per-lane" mentions remain in some secondary wiki pages
(device-events, device-input-flow, ticket-encoding, etc.) — flagged for a later lint pass.