wiki: design the business layer (session, tariff, permit, vision, shift, ops)
Pivot from the hardware/integrity layer to the parking operation. All wiki-only; no code yet. Core principle throughout: business entities are projections over the signed append-only event log, never mutable tables. New concepts: parking-session, tariff (composable/versioned, FX-ready), shift (manned-only Z-report), capacity-occupancy, validation-discounts, reporting-analytics, clock-integrity, ticket-encoding, anti-passback. New entities: permit, opencv-anpr-service, blocklist. Decisions: session-model, vision-service (host-side ANPR + vehicle verification; scoped AGPL exception for the isolated service). Updates: append-only-event-chain (new event types + vision witness), local-jwt-auth (drop 8h expiry -> until logout; code change pending), lpr-camera (host-side recognition supersedes edge-AI), standing-decisions (AGPL exception), open-questions (+FX, +pay-station money corners, backup). Deferred + flagged: intercom/help-call, receipts/refunds/change, FX engine, lane topology (#1).
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
---
|
||||
type: entity
|
||||
tags: [parking, domain, business, access-control]
|
||||
sources: []
|
||||
updated: 2026-06-15
|
||||
status: open
|
||||
---
|
||||
|
||||
# Blocklist (Banlist)
|
||||
|
||||
Plates or credentials the lot **refuses** — barred vehicles (non-payers, abusers, court orders) and
|
||||
revoked/stolen cards. Checked in the entry flow.
|
||||
|
||||
## Model
|
||||
|
||||
- A `blocklist` table of `{ kind: 'plate' | 'card' | 'qr', value, reason, addedBy, addedAt }` —
|
||||
admin-managed master data (mutable: add/lift a ban).
|
||||
- **Entry check:** after identifying the vehicle ([[parking-session]] identity — plate via
|
||||
[[opencv-anpr-service|vision]]/LPR, or card/QR), if it matches an active blocklist entry, **refuse
|
||||
entry** and append a signed event (`anomaly` / a refused-entry record) so the attempt is logged.
|
||||
- **Exit is never blocked** — a barred car already inside must still leave ([[fail-state-safety]]:
|
||||
never trap a vehicle). A blocklist hit at exit is logged for follow-up, not used to detain.
|
||||
|
||||
## Notes
|
||||
|
||||
- Plate matching depends on capture quality — a blocklist-by-plate is only as good as the
|
||||
[[opencv-anpr-service|vision]] read; treat a near-miss as a flag for a human, not an automatic
|
||||
refusal that could strand a misread innocent car.
|
||||
- Bans are attributed (`addedBy`) and their enforcement is logged, so the control is auditable
|
||||
([[reconciliation]]) rather than an invisible operator lever.
|
||||
|
||||
## Open
|
||||
|
||||
- Plate-match tolerance (exact vs. fuzzy) and the false-positive handling.
|
||||
- Expiry / review of bans.
|
||||
@@ -13,7 +13,14 @@ Authentication and authorization, kept **fully local** — a direct consequence
|
||||
|
||||
- `@fastify/jwt` signs tokens with a **local secret** (symmetric HMAC). The server **refuses to
|
||||
start** without a strong `JWT_SECRET` (≥32 chars, no placeholder) — there is deliberately no
|
||||
insecure default — and mints tokens with an **8h expiry** (bound to a shift).
|
||||
insecure default.
|
||||
- **Session lifetime: valid until explicit logout — no time expiry** (decision 2026-06-15).
|
||||
Booth reality breaks any fixed clock: relief arrives late, fails to show, or one operator is
|
||||
forced to work two shifts in a row — a token that expired mid-duty would strand an active
|
||||
operator. So the login persists until logout; a **[[shift]] is a separate, explicit boundary**,
|
||||
not tied to token lifetime. (Superseded the earlier "8h expiry, bound to a shift" assumption.)
|
||||
> ⚠️ Code still mints an 8h-expiry token — this page records the decided design; the server
|
||||
> change (drop `expiresIn`, persist until logout) is pending.
|
||||
- A `users` table in [[sqlite]] holds **bcrypt** password hashes plus a **role** column. The
|
||||
first admin is seeded via `pnpm --filter @parking/server seed-admin` (no bootstrap endpoint).
|
||||
- Authorization = a simple `preHandler` role guard per route: **admin / operator / cashier /
|
||||
|
||||
@@ -7,12 +7,17 @@ updated: 2026-06-15
|
||||
|
||||
# LPR Camera
|
||||
|
||||
License-plate-recognition camera (recommended: **Milesight edge-AI LPR**). For
|
||||
**casual/transient** vehicles, the **plate acts as ticket + an independent record**. (See
|
||||
[[parking-system-architecture]] §8, §9.)
|
||||
License-plate-recognition camera. For **casual/transient** vehicles, the **plate acts as ticket +
|
||||
an independent record**. (See [[parking-system-architecture]] §8, §9.)
|
||||
|
||||
- **Edge AI**: recognition runs **on-device**, so it keeps working with no internet — fits
|
||||
[[offline-first]].
|
||||
> **Superseded direction (2026-06-15):** recognition now runs **host-side** on snapshots from
|
||||
> ordinary Hikvision/Dahua cameras via the [[opencv-anpr-service]], **not** on a dedicated edge-AI
|
||||
> LPR camera — see [[vision-service]]. The edge-AI camera below is kept as the original assumption /
|
||||
> a fallback option, but is no longer the planned path. The host-side service also does **vehicle
|
||||
> verification** (anti-plate-spoofing), which an edge-LPR camera does not.
|
||||
|
||||
- **Edge AI (original assumption)**: recognition runs **on-device**, so it keeps working with no
|
||||
internet — fits [[offline-first]].
|
||||
- It's a **host-side** identity source: only the host sees the read; the host decides and
|
||||
commands the relay open (the [[uhppote-controller]] is demoted to a commanded relay for that
|
||||
lane). See [[entry-exit-readers]].
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
type: entity
|
||||
tags: [parking, vision, anpr, anti-fraud, service]
|
||||
sources: []
|
||||
updated: 2026-06-15
|
||||
status: open
|
||||
---
|
||||
|
||||
# OpenCV ANPR / Vision Service
|
||||
|
||||
A **local microservice** that analyses camera snapshots: reads the licence **plate** (ANPR) and
|
||||
extracts **vehicle attributes** for verification. Built by us (decision 2026-06-15) to do
|
||||
recognition **host-side on ordinary IP-camera snapshots**, replacing the dedicated edge-AI
|
||||
[[lpr-camera]]. See decision [[vision-service]].
|
||||
|
||||
## Two jobs
|
||||
|
||||
1. **Identity (ANPR).** snapshot → `{ plate, confidence, bbox }`. Feeds the existing
|
||||
`IdentitySource = "lpr"` ([[parking-session]]): the plate is a session/identity key and the way
|
||||
a plate-bound [[permit]] is matched.
|
||||
2. **Verification (anti-fraud witness).** snapshot → vehicle attributes — at minimum
|
||||
`{ make?, model?, colour, bodyType }`, ideally a compact **visual fingerprint** (an embedding).
|
||||
This is the answer to **plate-spoofing**: *a fraudster prints a registered/paid plate and drives
|
||||
in with a different car.* Plate-reading alone can't catch that; comparing the **vehicle** seen at
|
||||
entry vs. exit (and vs. the [[permit]]'s known car) can. A plate that entered on a red hatchback
|
||||
but exits on a black SUV is a **reconciliation anomaly** — exactly the independent-witness role
|
||||
the [[append-only-event-chain]] flags as the unbuilt gap. See [[reconciliation]].
|
||||
|
||||
> The two jobs are why this is worth building rather than just plate-OCR: the service is both an
|
||||
> **identity source** and an **independent witness**, the visual analogue of the whole system's
|
||||
> "two records that must reconcile" thesis.
|
||||
|
||||
## Architecture — separate localhost process
|
||||
|
||||
- A **Python service** (e.g. FastAPI) running **on the appliance**, called by the Node backend over
|
||||
**localhost HTTP** (`POST /analyze` with the JPEG bytes the camera driver already pulls — see
|
||||
[[lpr-camera]] "driver/storage boundary": `Snapshot.bytes`).
|
||||
- **Fully offline** ([[offline-first]]): all inference is local, no cloud. Model weights ship on the
|
||||
appliance.
|
||||
- **Process isolation is deliberate** — it keeps a heavy Python/native/AGPL stack out of the
|
||||
Node app's process and license surface (see licensing below), and gives it its own failure
|
||||
domain. If the service is down/slow, the host falls back (transient ticket path) rather than
|
||||
blocking the lane.
|
||||
- **Request/response (first cut):**
|
||||
- `POST /analyze` → `{ plate: {text, confidence, bbox}|null, vehicle: {colour, bodyType, make?, model?, embedding?}, modelVersion, tookMs }`
|
||||
- `GET /health` → readiness + model versions.
|
||||
- The Node side wraps it behind an internal interface (like a device adapter) so the recognizer can
|
||||
be swapped without touching business logic.
|
||||
|
||||
## Licensing — scoped AGPL exception (amends the standing rule)
|
||||
|
||||
The app is strictly **MIT/Apache/BSD** ([[technology-stack]], [[standing-decisions]]). Accurate
|
||||
ANPR/vehicle models are mostly **AGPL** (YOLO/Ultralytics detectors, OpenALPR) or commercial.
|
||||
Decision (2026-06-15): **allow AGPL inside this service only.** It is a **separate process**, not
|
||||
linked into the app, so its obligations don't reach the Node/React codebase; the app's permissive
|
||||
guarantee is preserved. Recorded as an explicit exception in [[standing-decisions]] /
|
||||
[[vision-service]].
|
||||
|
||||
- OpenCV core itself is **Apache-2.0** (clean either way).
|
||||
- AGPL note: if the appliance is ever offered as a network service to third parties, AGPL's
|
||||
network-use clause could require offering the service's source — relevant only if productised
|
||||
beyond the on-site appliance; flag at that point.
|
||||
|
||||
## Anti-fraud / threat-model fit
|
||||
|
||||
- **Plate spoofing** (the motivating case): vehicle-attribute / fingerprint mismatch entry↔exit or
|
||||
vs. a [[permit]]'s registered car → anomaly. Doesn't *block* on its own (recognition is
|
||||
probabilistic) — it **flags for [[reconciliation]]** and is captured in the signed record.
|
||||
- The recognition result and the source image both attach to the signed [[append-only-event-chain]]
|
||||
entry, so the *evidence* is tamper-evident even though recognition itself is host-side and
|
||||
fallible.
|
||||
- Recognition is **advisory, never the sole authority** to open a barrier where money/access is at
|
||||
stake — confidence thresholds + fallback to ticket/manual; a low-confidence read must not strand a
|
||||
car ([[fail-state-safety]]).
|
||||
|
||||
## Open
|
||||
|
||||
- **Recognizer choice** (permissive-only vs. AGPL model) and accuracy targets — see
|
||||
[[vision-service]]; AGPL now permitted in-service.
|
||||
- **Vehicle fingerprint**: attribute classifier vs. embedding-similarity; what threshold makes a
|
||||
mismatch an anomaly without false-positiving on lighting/angle.
|
||||
- **Compute footprint** on the appliance (CPU-only vs. a small GPU/NPU) — procurement input
|
||||
([[bom]], [[open-questions]]).
|
||||
- Per-camera **opt-in** ("optionally bound", user's word): which lanes/cameras route snapshots to
|
||||
the service.
|
||||
@@ -0,0 +1,119 @@
|
||||
---
|
||||
type: entity
|
||||
tags: [parking, domain, business, subscriptions, identity]
|
||||
sources: []
|
||||
updated: 2026-06-15
|
||||
status: open
|
||||
---
|
||||
|
||||
# Permit (Subscription)
|
||||
|
||||
A **subscription**: a known holder authorized to enter/exit without paying per-stay, for a covered
|
||||
period. The second of the "two populations" ([[entry-exit-readers]]); a valid permit
|
||||
**short-circuits the payment step** of a [[parking-session]] ([[session-model]]). Transient is
|
||||
built first; permits layer on top.
|
||||
|
||||
## Credentials (how a permit is presented) — confirmed with operator 2026-06-15
|
||||
|
||||
A permit is recognized by a credential read at the lane. Two kinds, mapping to the two identity
|
||||
paths:
|
||||
|
||||
- **RF tag / chip / card.** An RFID/proximity credential. Read **host-side** (reader → host →
|
||||
`pulseOpen`): autonomy isn't required (resolved below), and the [[dingtian-relay]] has no onboard
|
||||
card list anyway, so there's no need to route RF into a controller. A Wiegand-out reader is still
|
||||
fine and keeps a future autonomous path open ([[entry-exit-readers]]), but isn't required.
|
||||
- **QR code.** Read by the **optical reader** — inherently **host-side** ([[entry-exit-readers]]:
|
||||
pure optical/network readers are invisible to a controller). Host decodes the QR → looks up the
|
||||
permit → decides.
|
||||
|
||||
Both feed the host as a reader event whose `source` is `wiegand` / `qr` (the `IdentitySource`
|
||||
already in the model) and whose value is the credential id.
|
||||
|
||||
## Two optional, independent bindings — confirmed 2026-06-15
|
||||
|
||||
A permit has **two constraints the admin may or may not apply**, orthogonally. Either, both, or
|
||||
neither — the four combinations are all valid.
|
||||
|
||||
### 1. Car-count binding (default: 1)
|
||||
|
||||
- **Optional.** By default a permit is bound to **1 car at a time**. The admin may raise the limit
|
||||
(a household, a company fleet) or **unbind it entirely** (no cap on how many cars use it).
|
||||
- The limit is on **cars inside at once** (`maxConcurrent`), enforced over the
|
||||
[[parking-session]] projection: at entry, count the permit's currently-open sessions; if
|
||||
`< maxConcurrent` (or unbound) allow, else reject (allowance full). This is exactly why
|
||||
sessions-as-projection matters — "how many of this permit's cars are inside right now" is a fold
|
||||
over open entry/exit events, **not a counter someone can edit**.
|
||||
|
||||
### 2. Plate binding (default: off)
|
||||
|
||||
- **Optional.** By default a permit is **not** plate-bound — any car may use it (identity is the
|
||||
card/QR). The admin may bind it to a set of specific licence plates.
|
||||
- When **bound**, an allowed plate is an **accepted identity in its own right** — a valid
|
||||
**card/QR OR a matching plate** opens the lane (either, not a second factor):
|
||||
|
||||
```
|
||||
entry: read card/QR → find permit → car-count ok → open
|
||||
OR LPR plate ∈ permit's bound plates → find permit → car-count ok → open
|
||||
```
|
||||
|
||||
- **Accepted tradeoff:** card-OR-plate is the most convenient but does **not** prevent
|
||||
card-sharing (a lent card still opens). Fine for a trusted permit population; the signed
|
||||
[[append-only-event-chain]] records exactly which credential/plate entered, so abuse is visible
|
||||
to [[reconciliation]] after the fact.
|
||||
- **Plate-spoofing defence:** a printed copy of a registered plate on a *different* car is caught
|
||||
not here but by the [[opencv-anpr-service]]'s **vehicle-attribute verification** — the seen car
|
||||
must reconcile with the permit's known car, not just the plate string.
|
||||
|
||||
> The two are independent: a plate-bound permit may have no car cap; a car-capped permit may accept
|
||||
> any plate. The binding fields are simply absent/null when a constraint isn't applied.
|
||||
|
||||
## Data model (first cut — to firm up with [[session-model]])
|
||||
|
||||
A `permits` table (and supporting rows). Unlike the event log, reference/master data like permits
|
||||
**is** mutable (an admin grants/revokes/renews) — but every *use* of a permit still produces a
|
||||
signed `vehicle_entry`/`vehicle_exit` event in the [[append-only-event-chain]], so the audit trail
|
||||
stays append-only even though the permit record itself is editable.
|
||||
|
||||
| Field | Notes |
|
||||
| --- | --- |
|
||||
| `id`, `holderName`/contact | the subscriber |
|
||||
| `credentials[]` | one or more: `{ kind: 'rf' \| 'qr', value }` |
|
||||
| `maxConcurrent` | car-count binding; **default 1**, raise for fleets, or `null` = unbound |
|
||||
| `plates[]` | plate binding; **default empty/false** = any car; when set, these plates are accepted identities |
|
||||
| `validFrom`, `validTo` | coverage window |
|
||||
| `status` | active / suspended / revoked |
|
||||
|
||||
> Both bindings are nullable/empty by default — a bare permit is "1 car at a time, any plate,
|
||||
> identified by its card/QR".
|
||||
|
||||
## Interaction with the session model
|
||||
|
||||
- **Entry:** credential read → permit lookup → valid (active, in window, plate allowed **if
|
||||
plate-bound**, concurrent cars `< maxConcurrent` **if car-bound**) → signed `vehicle_entry`
|
||||
(source = `wiegand`/`qr`/`lpr`), open barrier. No ticket, no fee. (A bare permit applies neither
|
||||
extra check — just active + in window.)
|
||||
- **Exit:** credential/plate read → matching open permit session → signed `vehicle_exit`, open. No
|
||||
payment required.
|
||||
- **Lapsed mid-stay:** permit expires while a car is parked → the uncovered time falls back to the
|
||||
transient [[tariff]] (edge case to design).
|
||||
- **Revoked:** a revoked permit fails the entry check → treated as transient (take a ticket) or
|
||||
refused, per policy (OPEN).
|
||||
|
||||
## Resolved (2026-06-15)
|
||||
|
||||
- **Two optional bindings, independent:** car-count (`maxConcurrent`, **default 1**, raisable or
|
||||
unbound) and plate-binding (`plates[]`, **default off** = any car). Either, both, or neither.
|
||||
- **Plate vs. credential:** when plate-bound, **card/QR OR matching plate** — either is accepted
|
||||
identity (not a second factor); card-sharing not prevented by design, caught by
|
||||
[[reconciliation]] after.
|
||||
- **Autonomy:** **host-in-the-loop for everything** — no onboard card list needed, so the
|
||||
[[dingtian-relay]] stays sufficient (no new controller). Permit entry **fails closed** if the
|
||||
host is down ([[fail-state-safety]]). One code path for transient + permit.
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Reader hardware** — confirm the RF reader and the QR/optical reader models (procurement;
|
||||
relates to [[bom]] and [[open-questions]]). RF need not be Wiegand now that autonomy isn't
|
||||
required, but a Wiegand-out reader keeps options open.
|
||||
2. **Lapsed-mid-stay & revoked** policy (fall back to transient [[tariff]] vs. refuse) — confirm
|
||||
with operator.
|
||||
Reference in New Issue
Block a user