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:
@@ -11,16 +11,14 @@ CREATE TABLE `blocklist` (
|
||||
CREATE TABLE `device_events` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`device_id` text,
|
||||
`lane` integer,
|
||||
`category` text,
|
||||
`kind` text NOT NULL,
|
||||
`detail` text,
|
||||
`occurred_at` text DEFAULT (current_timestamp) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `lane_devices` (
|
||||
CREATE TABLE `devices` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`lane` integer NOT NULL,
|
||||
`category` text NOT NULL,
|
||||
`driver_id` text NOT NULL,
|
||||
`config` text NOT NULL,
|
||||
@@ -33,7 +31,6 @@ CREATE TABLE `ledger_events` (
|
||||
`index` integer NOT NULL,
|
||||
`type` text NOT NULL,
|
||||
`direction` text,
|
||||
`lane` integer NOT NULL,
|
||||
`source` text,
|
||||
`identity` text,
|
||||
`payload` text,
|
||||
@@ -70,7 +67,6 @@ CREATE TABLE `permits` (
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `sessions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`lane` integer,
|
||||
`identity` text,
|
||||
`source` text,
|
||||
`permit_id` text,
|
||||
@@ -85,6 +81,22 @@ CREATE TABLE `setup_state` (
|
||||
`completed_at` text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `site_config` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`capacity` integer,
|
||||
`updated_at` text DEFAULT (current_timestamp) NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `snapshots` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`direction` text NOT NULL,
|
||||
`device_id` text,
|
||||
`identity` text,
|
||||
`content_type` text NOT NULL,
|
||||
`bytes` blob NOT NULL,
|
||||
`captured_at` text NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE `tariff_versions` (
|
||||
`id` text PRIMARY KEY NOT NULL,
|
||||
`tariff_id` text NOT NULL,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
CREATE TABLE `site_config` (
|
||||
`id` integer PRIMARY KEY NOT NULL,
|
||||
`capacity` integer,
|
||||
`updated_at` text DEFAULT (current_timestamp) NOT NULL
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "cd09c11f-4306-4ac8-a335-7c050d080ab6",
|
||||
"id": "a6d81d46-c4a4-4ee7-8565-ec012bbe0252",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"blocklist": {
|
||||
@@ -82,13 +82,6 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
@@ -125,8 +118,8 @@
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"lane_devices": {
|
||||
"name": "lane_devices",
|
||||
"devices": {
|
||||
"name": "devices",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
@@ -135,13 +128,6 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
@@ -217,13 +203,6 @@
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
@@ -436,13 +415,6 @@
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identity": {
|
||||
"name": "identity",
|
||||
"type": "text",
|
||||
@@ -524,6 +496,97 @@
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"site_config": {
|
||||
"name": "site_config",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"capacity": {
|
||||
"name": "capacity",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"snapshots": {
|
||||
"name": "snapshots",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"direction": {
|
||||
"name": "direction",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identity": {
|
||||
"name": "identity",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"content_type": {
|
||||
"name": "content_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"bytes": {
|
||||
"name": "bytes",
|
||||
"type": "blob",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"captured_at": {
|
||||
"name": "captured_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"tariff_versions": {
|
||||
"name": "tariff_versions",
|
||||
"columns": {
|
||||
|
||||
@@ -1,724 +0,0 @@
|
||||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "538cd745-2fe9-410b-bb9f-e4d3c2086211",
|
||||
"prevId": "cd09c11f-4306-4ac8-a335-7c050d080ab6",
|
||||
"tables": {
|
||||
"blocklist": {
|
||||
"name": "blocklist",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"reason": {
|
||||
"name": "reason",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"active": {
|
||||
"name": "active",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"added_by": {
|
||||
"name": "added_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"added_at": {
|
||||
"name": "added_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"device_events": {
|
||||
"name": "device_events",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"device_id": {
|
||||
"name": "device_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"detail": {
|
||||
"name": "detail",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"occurred_at": {
|
||||
"name": "occurred_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"lane_devices": {
|
||||
"name": "lane_devices",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"driver_id": {
|
||||
"name": "driver_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"config": {
|
||||
"name": "config",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"enabled": {
|
||||
"name": "enabled",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"ledger_events": {
|
||||
"name": "ledger_events",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"index": {
|
||||
"name": "index",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"type": {
|
||||
"name": "type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"direction": {
|
||||
"name": "direction",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identity": {
|
||||
"name": "identity",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"payload": {
|
||||
"name": "payload",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"occurred_at": {
|
||||
"name": "occurred_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"prev_hash": {
|
||||
"name": "prev_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"signature": {
|
||||
"name": "signature",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"key_id": {
|
||||
"name": "key_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"ledger_events_index_unique": {
|
||||
"name": "ledger_events_index_unique",
|
||||
"columns": [
|
||||
"index"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"permit_credentials": {
|
||||
"name": "permit_credentials",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permit_id": {
|
||||
"name": "permit_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"kind": {
|
||||
"name": "kind",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"permit_plates": {
|
||||
"name": "permit_plates",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permit_id": {
|
||||
"name": "permit_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"plate": {
|
||||
"name": "plate",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"permits": {
|
||||
"name": "permits",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"holder_name": {
|
||||
"name": "holder_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"contact": {
|
||||
"name": "contact",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"max_concurrent": {
|
||||
"name": "max_concurrent",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"valid_from": {
|
||||
"name": "valid_from",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"valid_to": {
|
||||
"name": "valid_to",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"status": {
|
||||
"name": "status",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'active'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"sessions": {
|
||||
"name": "sessions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"lane": {
|
||||
"name": "lane",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"identity": {
|
||||
"name": "identity",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"source": {
|
||||
"name": "source",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"permit_id": {
|
||||
"name": "permit_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"entered_at": {
|
||||
"name": "entered_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"exited_at": {
|
||||
"name": "exited_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"state": {
|
||||
"name": "state",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'open'"
|
||||
},
|
||||
"last_event_index": {
|
||||
"name": "last_event_index",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"setup_state": {
|
||||
"name": "setup_state",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"completed_at": {
|
||||
"name": "completed_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"site_config": {
|
||||
"name": "site_config",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"capacity": {
|
||||
"name": "capacity",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"tariff_versions": {
|
||||
"name": "tariff_versions",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"tariff_id": {
|
||||
"name": "tariff_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"effective_from": {
|
||||
"name": "effective_from",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"currency": {
|
||||
"name": "currency",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"structure": {
|
||||
"name": "structure",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_by": {
|
||||
"name": "created_by",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"tariffs": {
|
||||
"name": "tariffs",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "'site'"
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"password_hash": {
|
||||
"name": "password_hash",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": "(current_timestamp)"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"users_username_unique": {
|
||||
"name": "users_username_unique",
|
||||
"columns": [
|
||||
"username"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,9 @@
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "6",
|
||||
"when": 1781539958008,
|
||||
"when": 1781632874398,
|
||||
"tag": "0000_baseline",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "6",
|
||||
"when": 1781590195573,
|
||||
"tag": "0001_site_config",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
+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