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:
+43
-16
@@ -1,5 +1,5 @@
|
||||
import { sql } from "drizzle-orm";
|
||||
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
import { blob, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
||||
|
||||
// Schema notes:
|
||||
// - TWO event streams, deliberately separate (see wiki/decisions/event-streams-split.md):
|
||||
@@ -39,7 +39,6 @@ export const ledgerEvents = sqliteTable("ledger_events", {
|
||||
index: integer("index").notNull().unique(),
|
||||
type: text("type").notNull(),
|
||||
direction: text("direction", { enum: ["entry", "exit"] }),
|
||||
lane: integer("lane").notNull(),
|
||||
source: text("source"),
|
||||
identity: text("identity"),
|
||||
// Type-specific business payload (JSON). Signed as part of the canonical form.
|
||||
@@ -56,13 +55,12 @@ export const ledgerEvents = sqliteTable("ledger_events", {
|
||||
|
||||
// --- Device telemetry (unsigned, prunable) -------------------------------
|
||||
// Operational monitoring, NOT anti-fraud: relay fired, printer paper-out, camera
|
||||
// offline, reader read, raw input edges. Keyed to a lane_devices instance; lane is
|
||||
// resolved via the LaneMap. No prevHash/signature — this stream may rotate/prune.
|
||||
// offline, reader read, raw input edges. Keyed to a `devices` instance. No
|
||||
// prevHash/signature — this stream may rotate/prune.
|
||||
export const deviceEvents = sqliteTable("device_events", {
|
||||
id: text("id").primaryKey(),
|
||||
// The lane_devices instance that produced it (raw provenance).
|
||||
// The `devices` instance that produced it (raw provenance).
|
||||
deviceId: text("device_id"),
|
||||
lane: integer("lane"),
|
||||
category: text("category", {
|
||||
enum: ["access", "reader", "camera", "printer"],
|
||||
}),
|
||||
@@ -75,13 +73,42 @@ export const deviceEvents = sqliteTable("device_events", {
|
||||
.default(sql`(current_timestamp)`),
|
||||
});
|
||||
|
||||
// --- Per-lane device assignments (first-run setup) -----------------------
|
||||
// One row per (lane, category, instance). `driverId` references a driver in the
|
||||
// @parking/devices registry; `config` is that driver's JSON config. Keeps the
|
||||
// system device-agnostic + admin-configurable. See device-registry.md, first-run-setup.md.
|
||||
export const laneDevices = sqliteTable("lane_devices", {
|
||||
// --- Camera snapshots (unsigned, prunable, blob-in-DB) -------------------
|
||||
// An entry/exit snapshot captured asynchronously AFTER the barrier opens — evidence,
|
||||
// not a gate (camera failure never blocks an open; see entry/exit flows). Stored as a
|
||||
// BLOB so the appliance keeps a single backed-up file with nothing scattered on disk.
|
||||
// Kept in its own table (not inline in device_events) so the hot telemetry scans don't
|
||||
// drag image bytes, and so images can be pruned independently. The signed
|
||||
// vehicle_entry/exit references a snapshot by `id` in its payload — the image is an
|
||||
// independent record (anti-fraud), unsigned and prunable. Retention policy is an open
|
||||
// question — see wiki/concepts/entry-exit-points.md. Served via GET /api/snapshots/:id.
|
||||
export const snapshots = sqliteTable("snapshots", {
|
||||
id: text("id").primaryKey(),
|
||||
direction: text("direction", { enum: ["entry", "exit"] }).notNull(),
|
||||
// The camera `devices` instance that captured it (raw provenance).
|
||||
deviceId: text("device_id"),
|
||||
// The session/credential ref (ticket id, plate, permit) — links to the ledger event.
|
||||
identity: text("identity"),
|
||||
contentType: text("content_type").notNull(),
|
||||
bytes: blob("bytes").notNull().$type<Buffer>(),
|
||||
capturedAt: text("captured_at").notNull(),
|
||||
});
|
||||
|
||||
// --- Device assignments (first-run setup) --------------------------------
|
||||
// One row per device instance. `driverId` references a driver in the @parking/devices
|
||||
// registry; `config` is that driver's JSON config. There is NO lane: a parking lot is
|
||||
// one pool of spaces with a flexible set of entry/exit points. Direction lives INSIDE
|
||||
// the config, per the hardware:
|
||||
// - access controller: config.relays = [{ relay, direction: entry|exit|both, button? }]
|
||||
// — one physical board has several relays; each relay opens one barrier in one
|
||||
// direction (or both). `button` = the input terminal the entry button is wired to
|
||||
// (transient entry trigger; absent = no button at that barrier).
|
||||
// - reader / camera: config.controllerId + config.relay BIND it to the barrier it sits
|
||||
// at; its direction is INHERITED from that relay. Unbound → falls back to a
|
||||
// direction picked in config.
|
||||
// See device-registry.md, first-run-setup.md, wiki/concepts/entry-exit-points.md.
|
||||
export const devices = sqliteTable("devices", {
|
||||
id: text("id").primaryKey(),
|
||||
lane: integer("lane").notNull(),
|
||||
category: text("category", {
|
||||
enum: ["access", "reader", "camera", "printer"],
|
||||
}).notNull(),
|
||||
@@ -120,8 +147,8 @@ export const siteConfig = sqliteTable("site_config", {
|
||||
// `scope` lets multiple be added later without migration. See wiki/concepts/tariff.md.
|
||||
export const tariffs = sqliteTable("tariffs", {
|
||||
id: text("id").primaryKey(),
|
||||
// Only "site" used now; "lane"/"zone" reserved for multi-tariff later.
|
||||
scope: text("scope", { enum: ["site", "lane", "zone"] }).notNull().default("site"),
|
||||
// Only "site" used now; "zone" reserved for multi-tariff later.
|
||||
scope: text("scope", { enum: ["site", "zone"] }).notNull().default("site"),
|
||||
name: text("name").notNull(),
|
||||
createdAt: text("created_at")
|
||||
.notNull()
|
||||
@@ -204,7 +231,6 @@ export const blocklist = sqliteTable("blocklist", {
|
||||
export const sessions = sqliteTable("sessions", {
|
||||
// The session key = the entry's identity (ticket id or plate).
|
||||
id: text("id").primaryKey(),
|
||||
lane: integer("lane"),
|
||||
// Identity that opened the session, and how it was read.
|
||||
identity: text("identity"),
|
||||
source: text("source"),
|
||||
@@ -224,7 +250,8 @@ export const sessions = sqliteTable("sessions", {
|
||||
export type UserRow = typeof users.$inferSelect;
|
||||
export type LedgerEventRow = typeof ledgerEvents.$inferSelect;
|
||||
export type DeviceEventRow = typeof deviceEvents.$inferSelect;
|
||||
export type LaneDeviceRow = typeof laneDevices.$inferSelect;
|
||||
export type SnapshotRow = typeof snapshots.$inferSelect;
|
||||
export type DeviceRow = typeof devices.$inferSelect;
|
||||
export type SetupStateRow = typeof setupState.$inferSelect;
|
||||
export type SiteConfigRow = typeof siteConfig.$inferSelect;
|
||||
export type TariffRow = typeof tariffs.$inferSelect;
|
||||
|
||||
Reference in New Issue
Block a user