diff --git a/wiki/concepts/append-only-event-chain.md b/wiki/concepts/append-only-event-chain.md index f61703f..ae06529 100644 --- a/wiki/concepts/append-only-event-chain.md +++ b/wiki/concepts/append-only-event-chain.md @@ -21,9 +21,25 @@ Three layered properties: self-consistent — someone who owns the machine still cannot forge a valid entry. It only becomes trustworthy as an external fraud control when paired with [[reconciliation]] -against an authority the operator can't alter. Every device event — including those ingested -from the [[uhppote-controller]] via [[event-log-ingestion]] — should land in this host-side -chain. +against an authority the operator can't alter. + +## Two event streams — the signed ledger vs. device telemetry (decision 2026-06-15) + +These are **different concerns and live in different tables**: + +- **`ledger_events`** — this signed, hash-chained, [[atecc608]]-signed **business ledger**: + `vehicle_entry` / `vehicle_exit` / `payment` / `void` / `shift_z_report`, plus the witness-grade + `barrier_open_command` / `barrier_open_observed` and `anomaly`. This is the anti-fraud record that + [[reconciliation]] runs against; sessions/[[tariff]]/occupancy are projections over it. (This is + the table formerly called `events`.) +- **`device_events`** — **unsigned operational telemetry**: relay fired, printer paper-out, camera + offline, reader read, raw input edges. High-volume, churny, **not** anti-fraud; may rotate/prune. + Keeping it out of the signed chain keeps the ledger small and high-value. + +> A raw button press is **device telemetry**, not a business fact. It lands in `device_events`; the +> entry flow then mints a **signed `vehicle_entry`** in the ledger once a ticket prints and the +> barrier is commanded. (This supersedes the earlier "every device event lands in the chain" framing +> and the `input_received`-as-signed-event approach — see [[device-input-flow]].) ## Implementation (apps/server) @@ -58,14 +74,9 @@ so old events stay verifiable. > *accidental* corruption, but an operator with the signing key + DB access could re-sign a > forged chain. This is the central reason #6 matters. -### What currently feeds the log +### Business-layer event types (the ledger) -Dingtian **input (button) pushes** → bus → `input_received` events (see [[device-input-flow]], -[[dingtian-relay]]). These are recorded faithfully as raw inputs, **not** as `vehicle_entry` — -the richer entry event waits for the entry flow (ticket print + barrier command). - -**Business-layer event types (designed, not yet implemented — see [[session-model]]).** The -[[parking-session]] domain folds over these signed events, extending `input_received`: +The [[parking-session]] domain folds over these **signed ledger** events: - `vehicle_entry` / `vehicle_exit` — a stay's endpoints; `identity` carries the ticket id or plate. - `payment` — a settled fee at the pay station, referencing the session it pays for (amount in @@ -73,20 +84,21 @@ the richer entry event waits for the entry flow (ticket print + barrier command) whole point: an operator can't forge it or silently delete it. - `void` — a correction / lost-ticket write-off; like every other void here it is an **appended event, never an erasure**. +- `shift_z_report` — the signed per-[[shift]] takings summary. A session is a **projection** over this chain, never a mutable table — the same anti-fraud reason the chain exists. See [[parking-session]]. -- **`lane`** is now resolved from the firing device. A `LaneMap` (`apps/server/src/lane-map.ts`) - caches `lane_devices.id → lane`, built at startup and refreshed by the setup routes on every - assign/unassign. Device events carry the device instance id, not a lane; the handler looks it - up. A device with no mapping (assigned without a lane, or a stale id) logs **`lane: -1`** and a - warning — never `0`, which is a real lane — and is still recorded (the chain is append-only; - nothing is dropped). -- **`source` stays `null`** for `input_received`, and deliberately so: `source` is an - `IdentitySource` (`wiegand | lpr | qr | ticket | manual`) — *how a vehicle was identified* — not - a device/IP field. A raw button push has no vehicle identity. The device provenance lives in - **`identity`** (e.g. `dingtian: input:1/on`). +### ⚠️ As-built vs. the table split (pending) + +The current code records Dingtian **input (button) pushes** as `input_received` rows **in the +signed chain** (with `lane` resolved via the `LaneMap`, `source` null, device provenance in +`identity`). Per the 2026-06-15 split (above), a raw button press is **device telemetry** and +belongs in **`device_events`**, *not* the signed ledger — only the business `vehicle_entry` it +drives gets signed. So `input_received`-in-the-ledger is **transitional**; the pending refactor +moves raw inputs to `device_events` and renames the chain table to `ledger_events`. (`LaneMap` +lane-resolution and the "never stamp `lane: 0` for an unmapped device" rule carry over to whichever +stream records the event.) ### ⚠️ Limitation: the log captures HOST-ORIGINATED actions only diff --git a/wiki/concepts/device-events.md b/wiki/concepts/device-events.md new file mode 100644 index 0000000..14a52ad --- /dev/null +++ b/wiki/concepts/device-events.md @@ -0,0 +1,45 @@ +--- +type: concept +tags: [parking, devices, monitoring, telemetry] +sources: [] +updated: 2026-06-15 +status: open +--- + +# Device Events (telemetry) + +The **unsigned** operational record of what the hardware did and reported — distinct from the +signed business [[append-only-event-chain|ledger]] (see [[event-streams-split]]). For monitoring, +diagnostics, and live booth status — **not** anti-fraud. + +## What lands here + +- **Relays/barriers:** relay fired/released, pulseOpen issued (the *device-side* echo; the + authoritative `barrier_open_command` is a signed ledger event). +- **Printers:** paper-out / near-end / cover-open / cutter / offline (already polled — + [[printer-status-monitoring]]). +- **Cameras:** reachable/offline, snapshot success/failure ([[lpr-camera]]). +- **Readers / inputs:** a raw read, raw input edges (Dingtian button `input N on/off` — + [[device-input-flow]]). + +## Properties + +- **Unsigned, not chained** — no `prevHash`/`signature`. It's telemetry, so it carries none of the + ledger's integrity machinery. +- **Disposable** — high-volume and churny; **may rotate/prune** on a retention policy (the ledger + never does). +- **Device-keyed** — references the `lane_devices` instance; `lane` resolved via the same `LaneMap` + as before. Stores raw device provenance. + +## The boundary that matters + +A device event is *evidence the host saw something happen*; it does **not** by itself authorize or +record a business fact. A button press here becomes a **signed `vehicle_entry`** in the ledger only +after the entry flow runs (ticket + barrier command). This keeps device chatter on the device side +of the [[device-adapter-pattern|adapter boundary]] and the signed ledger focused on money/access. + +## Open + +- Retention/rotation policy (size- or age-based). +- Whether any witness-grade device fact (e.g. a loop-sensor `barrier_open_observed`) should *also* + write a signed ledger entry for [[reconciliation]] — see [[append-only-event-chain]]. diff --git a/wiki/decisions/event-streams-split.md b/wiki/decisions/event-streams-split.md new file mode 100644 index 0000000..ea43bd3 --- /dev/null +++ b/wiki/decisions/event-streams-split.md @@ -0,0 +1,57 @@ +--- +type: decision +tags: [parking, decisions, integrity, devices, schema] +sources: [] +updated: 2026-06-15 +status: open +--- + +# Decision: Split the signed business ledger from device telemetry + +Taken 2026-06-15, at the start of the business-layer schema work. + +## The problem + +The existing `events` table (signed, hash-chained — [[append-only-event-chain]]) had grown to carry +**two unrelated concerns**: the financial/accountability ledger *and* raw device telemetry (button +pushes recorded as `input_received`). They have opposite requirements — the ledger must be small, +signed, and reconciled; telemetry is high-volume, churny, and disposable. + +## Decision — two tables + +- **`ledger_events`** — the signed, hash-chained, [[atecc608]]-signed **business ledger** (rename of + `events`). Holds only business/accountability facts: `vehicle_entry`, `vehicle_exit`, `payment`, + `void`, `shift_z_report`, and the witness-grade `barrier_open_command` / `barrier_open_observed` / + `anomaly`. [[reconciliation]] runs against this; sessions/[[tariff]]/occupancy are projections of + it. +- **`device_events`** — **unsigned** operational telemetry (see [[device-events]]): relay fired, + printer paper-out, camera offline, reader read, raw input edges. May rotate/prune. Never signed, + never reconciled. + +A raw button press is **telemetry** → `device_events`. The entry flow then mints a **signed +`vehicle_entry`** in the ledger once a ticket prints + the barrier is commanded. So +`input_received`-as-a-signed-event is **dropped** (it was transitional). + +## Why + +- Keeps the **signed ledger small and high-value** — fewer rows to sign, hash, verify, reconcile, + and export; signal isn't drowned in device noise. +- Right **durability semantics per stream**: the ledger is precious + append-only forever; telemetry + can age out. +- Clean separation matches the [[device-adapter-pattern]] philosophy — device chatter stays on the + device side of the boundary. + +## Consequences / migration (no production data yet) + +- No `.sqlite` with real chain data exists, so renaming + restructuring is safe now (no signatures + to invalidate). This is the moment to do it. +- Code: rename `events` → `ledger_events`; `EventLog`/`canonicalize`/`verifyChain` and the + `/api/events` routes follow the rename; add an unsigned `device_events` writer; move the Dingtian + input-push handler to emit `device_events` (+ the entry flow signs `vehicle_entry`). +- `ParkingEventType` in `packages/shared` splits into ledger types vs. a device-event type set. + +## Open + +- `device_events` retention/rotation policy. +- Which device facts (if any) are witness-grade enough to *also* warrant a signed ledger entry + (e.g. `barrier_open_observed` from a loop sensor) — see [[append-only-event-chain]] witness gap. \ No newline at end of file diff --git a/wiki/index.md b/wiki/index.md index af210bc..1f5b84e 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -7,7 +7,7 @@ updated: 2026-06-14 # Index Content catalog for the wiki. Start at [[overview]]. Maintained on every ingest. -Counts: 1 source · 18 entities · 21 concepts · 4 decision records. +Counts: 1 source · 18 entities · 23 concepts · 5 decision records. ## Overview & navigation - [[overview]] — the top-level synthesis and entry point. @@ -81,6 +81,7 @@ Counts: 1 source · 18 entities · 21 concepts · 4 decision records. - [[clock-integrity]] — fees depend on the host clock; detect/flag backdating on an offline box. - [[ticket-encoding]] — transient ticket id as QR; printed at entry, scanned at pay station + exit; plate-as-ticket alt. - [[anti-passback]] — block/flag one id entering twice without an exit; fold over open sessions. +- [[device-events]] — unsigned hardware telemetry (relay/printer/camera/reader/input); separate from the signed ledger. - [[permit]] — subscription; RF/QR or plate identity, registered-cars + max-concurrent, host-in-loop; short-circuits payment. - [[opencv-anpr-service]] — host-side vision microservice: ANPR (plate identity) + vehicle verification (anti-plate-spoofing witness). - [[blocklist]] — barred plates/cards refused at entry (never at exit); signed, attributed. @@ -97,3 +98,4 @@ Counts: 1 source · 18 entities · 21 concepts · 4 decision records. - [[dingtian-vs-mqtt]] — transport choice: direct HTTP/UDP now, MQTT parked until multi-lane scale. - [[session-model]] — business layer start: session = projection; transient-first; pay-on-foot. New event types. - [[vision-service]] — build a host-side ANPR + vehicle-verification service; replaces edge-LPR; scoped AGPL exception. +- [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events). diff --git a/wiki/log.md b/wiki/log.md index 0df0ffb..866098b 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -469,3 +469,20 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section). procurement (may change what the `payment` event stores → flagged before schema). - Still open & load-bearing: **lane topology** (#1) — not resolved; scopes sessions/occupancy/shifts. - Updated [[open-questions]] (#9), [[index]]. + +## [2026-06-15] decision | Split signed business ledger from device telemetry +- User correction before schema: the `events` table conflated TWO things — the anti-fraud business + ledger AND device telemetry (button pushes as `input_received`). Split them. +- `ledger_events` (rename of `events`): signed, hash-chained, ATECC608-signed business facts only + (vehicle_entry/exit, payment, void, shift_z_report + witness barrier_open_command/observed, + anomaly). Reconciliation + session/tariff/occupancy projections run on this. +- `device_events` (new, [[device-events]]): UNSIGNED hardware telemetry (relay fired, paper-out, + camera offline, reader read, raw input edges); high-volume, may rotate/prune; never reconciled. +- A raw button press is telemetry → device_events; the entry flow then mints a SIGNED vehicle_entry. + So `input_received`-as-signed-event is dropped (was transitional). No prod chain data exists, so + the rename/restructure is safe now (no signatures to invalidate). +- New: decision [[event-streams-split]], concept [[device-events]]; updated [[append-only-event-chain]] + (two streams + as-built-vs-pending), [[index]]. +- NEXT (schema): rename events→ledger_events; add device_events; split ParkingEventType in shared; + then tariffs/versions, permits, blocklist, sessions projection. EventLog/canonicalize/verifyChain + + /api/events follow the rename (code refactor, separate from this wiki commit).