docs(wiki): venue modules design — Car Wash pilot, Parking as a peer module (open decision)

Records the 2026-09-04/05 design sessions on wiki/decisions/venue-modules.md:
manifest-registry module system (folder per module, always-migrated schema,
one ledger union with prefixed event types, relations only via manifest
dependsOn + ledger events), enablement as entitled ∩ activated (vendor-set
Komodo env, site-admin site-config toggle recorded as config_change; server
enforces with requireModule, web only hides; disabling never deletes),
Parking recast as one module on a venue POS/audit core, Car Wash as the
pilot (inside the parking, entry snapshot as identity, bay camera for the
unrecorded-wash anti-fraud signals, v1 scope + build order), and vision
vehicle category as an advisory anomaly flag. Name stays parking-system;
validation stays for the Bar, only the Lavazh station retires with Car Wash.
Open-questions #15, index, log.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-05 10:24:20 +02:00
parent d86bffa500
commit db9c3e0e31
4 changed files with 282 additions and 1 deletions
+7 -1
View File
@@ -2,7 +2,7 @@
type: decision type: decision
tags: [parking, decisions, open] tags: [parking, decisions, open]
sources: [parking-system-architecture] sources: [parking-system-architecture]
updated: 2026-06-29 updated: 2026-09-04
status: open status: open
--- ---
@@ -129,3 +129,9 @@ procurement. (See [[parking-system-architecture]] §10.)
app code**, and are **unverified on hardware**. Close this once the printer transport per site is app code**, and are **unverified on hardware**. Close this once the printer transport per site is
fixed and (if USB) the udev/usblp rule is in the image and a real USB print is verified. Relates fixed and (if USB) the udev/usblp rule is in the image and a real USB print is verified. Relates
to #1 (lane topology / image standardization). See [[printer-usb-transport]], [[rongta-printer]]. to #1 (lane topology / image standardization). See [[printer-usb-transport]], [[rongta-printer]].
15. **Venue modules — Car Wash / Bar as peers of Parking.** _(Raised by the user, 2026-09-04.)_
Optional per-site modules on a shared venue core, with Parking itself becoming a module.
Name stays `parking-system` (settled 2026-09-05); validation stays for the Bar, only the
Lavazh station retires when Car Wash (the pilot module) ships. Full design and the remaining
questions on [[venue-modules]].
+237
View File
@@ -0,0 +1,237 @@
---
type: decision
tags: [parking, decisions, open, modules, architecture]
sources: []
updated: 2026-09-05
status: open
---
# Venue modules — Car Wash, Bar/Restaurant, and Parking as peers
**Status: OPEN.** Design captured from a working session with the user on 2026-09-04, after the
desktop-shell run closed. Nothing here is built. Decisions marked **(settled)** were stated by the
user in that session; everything else is the proposed shape awaiting a go.
## The ask
Some sites need a **Car Wash** (al. *lavazh*), some a **Bar / Caffè / Restaurant**, some both, some
neither. These must be optional per site, may relate to each other, and must not be second-tier
citizens of a product whose every identifier says "parking".
## What exists today (and the gap)
Optionality already appears three ways, none named: a process-level env flag (`VISION_ENABLED`
→ [[vision-service]]), a per-site DB flag (`validation_programs.active`, the Bar/Lavazh
checkboxes of [[validation-discounts]]), and permissions gating nav/routes ([[local-jwt-auth]]).
There is **no registry**: adding a feature hand-wires seven places (`server.ts` route list, the
`RESOURCES`/`PERMISSIONS` catalog in `@parking/shared`, the `LedgerEventType` union, a schema
table + migration, `router.tsx` routes + nav, i18n). Nothing keeps them consistent and nothing
lets the server say "this feature is off at this site."
## Decisions taken in the session
1. **Validation stays — for the Bar (revised 2026-09-05).** The 2026-09-04 session first
settled on decommissioning [[validation-discounts]] entirely; the user revised this the next
day: the merchant-scan validation is needed **as is for the Bar** until a Bar management
module exists, at which point it folds into that module. Only the **Lavazh station is
retired** when Car Wash ships (Car Wash sponsors parking through its own order flow, below),
so `STATIONS` shrinks to `["bar"]`; no tables are dropped, the `"validation"` ledger type stays
live. Consequence, accepted: two sponsorship mechanisms coexist for a while — merchant-scan
validation (Bar) and order-driven comp/credit (Car Wash). In the registry, validation is
registered as its own module (`validation`, no dependencies) so the Bar module can later
declare `dependsOn` or absorb it.
2. **Parking is a module, a peer of Car Wash and Bar (settled in principle).** What is *not*
parking in the server today — identity/roles, the signed ledger, device adapters + monitoring,
[[shift]], cash drawer, payment terminal, receipt printing, reports, site config, logs, backup —
is a point-of-sale + audit **platform for a venue**, and every new module needs all of it. The
core is that list; Parking is access control (barriers, readers, ANPR), [[parking-session]],
occupancy, [[tariff]], [[subscription]].
3. **Updates/enablement authority — two layers (proposed, user agreed in discussion).** See below.
## Proposed shape
### One binary, modules enabled per site at runtime
Not build variants, not a package per site. The fleet just reached "same image + same installer at
every booth" ([[fleet-deployment-komodo]], [[desktop-shell-tauri]]); per-site builds would undo
it. Enabling is a runtime decision, recorded, reversible.
### A module = a manifest + three folders
- **Manifest** in `packages/shared`: `id`, `dependsOn: id[]`, the permission `resources` it
contributes, the `ledgerEventTypes` it appends (prefixed: `carwash_*`, `bar_*`), site-config
defaults.
- **Folders**: `apps/server/src/modules/<id>/` (`register(app, deps)` + schema),
`apps/web/src/modules/<id>/` (routes + nav entries), an i18n namespace.
- **Registry**: one array of manifests. `server.ts` iterates it instead of ~30 flat calls;
`router.tsx` likewise. Adding a module = a folder + one registry line; a missing piece fails at
startup, not in the field.
- **Not packages yet.** Folder-per-module is enough at this scale; packages earn their keep only
when a module needs its own release cadence, and with one image per commit none will.
### Enablement: entitled ∩ activated
- **Layer 1 — entitlement (vendor).** What a site *may* have is a commercial/deployment decision
and belongs to the vendor, not to any app role. Home: the Komodo stack environment, next to
`VISION_ENABLED`/`TAG`/secrets — e.g. `MODULES_ENTITLED=parking,carwash`. Changes only via a
Komodo sync + redeploy (the vendor's channel, offline-safe, invisible to app roles). This is
exactly today's vision pattern: env = entitlement, `anprEntryEnabled` = the site's own switch.
- **Layer 2 — activation (site admin).** Whether the site is *using* it now (a car wash closed for
winter) belongs to the site admin, within the entitled set: a checkbox in Setup → Site, stored
in `site_config.modules`, every change a signed `config_change` ledger event with the actor
(the presence-bypass precedent, [[entry-presence-bypass]]). Rides on `site:update`; a separate
`module:update` permission is one line later if a "tariffs but not modules" owner role is ever
needed. No new super-role.
- **Effective set = entitled ∩ activated.** The server enforces it with a `requireModule(id)`
guard beside `requirePermission` (permissions alone are insufficient: a role may hold
`carwash:create` at a site with no car wash). `/api/site-config` and `/api/auth/me` expose it
so the SPA can hide nav before it knows anything else — the web only *hides*, the server
*enforces*.
- **Rules:** disabling never deletes (tables, history, role grants all stay; routes reject, UI
disappears; re-enable restores). Dependencies enforced at the point of change from the
manifests: enabling Car Wash with a dependency off enables it or refuses with a message;
disabling a dependency of an enabled module refuses. Entitlement is a boundary against app
roles, not against root on the box — right level given [[disk-os-hardening]]; if commercial
enforcement ever matters, it becomes a small **minisign-signed** file checked with the same key
infrastructure the updater already uses, with nothing above changing.
### Schema and ledger stay uniform
Tables for every module are **always migrated**, enabled or not (empty tables; no conditional
migrations on an offline appliance). The ledger stays **one** append-only union; module event
types just carry the module prefix, so the chain and its signing never change shape.
### Relations between modules: manifest + ledger, never imports
Car Wash and Bar both need "this customer's parking is sponsored/discounted". For the Bar that
is today's merchant-scan validation, kept as is (decision 1). For Car Wash it is expressed as
**events**: a `carwash_wash_complete` event is appended; the parking module reacts (comp/credit
the session) through the existing event bus ([[event-streams-split]]). A module never calls another module's
routes or imports its code; `dependsOn` in the manifest is the only coupling the registry knows.
Merchant-type users (bar tender, wash operator) belong to the module they operate.
### Naming and identity — one irreversible constraint
The product name touches three things at different costs:
- **Desktop app `identifier` (`com.parking.desktop`) and the `.deb` package name (from
`productName`) — irreversible in practice.** Changing either means an installed booth will not
update into the new app: dpkg treats it as a different package, and a new identifier gets a
fresh config dir (saved server address + session lost). Survivable now with one staging booth
and a manual reinstall; a fleet migration later. **Settle the platform name and apply it here
before the second booth is provisioned, then never touch it again.**
- Repo name / Gitea project / image names / Komodo stack names — cheaper, but each is a place
the wiki and runbooks point at. Let them follow at the point the first non-parking module ships.
- Package scope `@parking/*` — can stay until the core/parking boundary exists in code. Renaming is
cheap once, expensive twice.
### Migration path (no big-bang)
1. Remove validation (its own commit).
2. Decide the platform name; apply it to the desktop identifier + package name (irreversible one
first).
3. Introduce the registry: `site_config.modules`, manifest type, `requireModule`, nav gating, the
entitlement env. Register **parking** as the first module *without* moving code yet
(its seam is drawn in a wiki page; code moves across it as each subsystem is touched, starting
with the obviously-core pieces such as shift and cash).
4. Build **Car Wash** as the first new module against the contract: it must need zero changes to
core files beyond its own folder and registry line. Its data model, queue, services, pricing
are a separate scoping conversation.
## Vehicle category from vision — advisory, flagged, never authoritative
Raised alongside: Car Wash prices by body type (e.g. **SUV > Car**), so can the ANPR service
help? Facts first ([[vision-service]], [[opencv-anpr-service]]): the service is **stateless**
(`GET /health`, `POST /analyze`, no DB, no volume; the Node server is the only writer of record),
and today's `fast-alpr` is plate-only — YOLOv9 *plate* detector + CCT OCR, no notion of the
vehicle. The Hikvision push's `detectionTarget` only says `vehicle`/`human` on the G3H.
- **Cheap path:** a general detector beside the plate detector, behind the existing `Recognizer`
class boundary (no app change). Licence decides the model: Ultralytics YOLOv8 is **AGPL — out**;
**YOLOX** and **RT-DETR** ship Apache-2.0 ONNX weights on the ONNX Runtime already in use. COCO
gives `car/bus/truck/motorcycle/bicycle` — **no van, minivan, pickup, and no SUV vs sedan**.
- **Real path for SUV-vs-Car:** a body-type classifier (sedan/hatchback/SUV/minivan/pickup/van)
fine-tuned on a few thousand **own entry-camera frames** on an Apache-2.0 backbone; public
car datasets are often research-only — check the licence before touching one. Expect 85–95 %
on frontal gate views once tuned: enough to *flag*, nowhere near enough to *bill*.
- **Design (the [[threat-model]] shape — operator is the adversary):** vision **proposes**, the
operator can override, the override is on the record.
- Wash intake ties the order to the parking session by plate, so the **entry snapshot already
exists** — no new camera, no new capture; classify the vehicle crop in that frame.
- The intake form pre-selects vision's category. The order stores `visionCategory`,
`visionConfidence`, `operatorCategory`, actor.
- Differ **and** confidence ≥ threshold → append an **`anomaly`** ledger event (existing type,
new reason) with the snapshot attached; a reviewer sees car, both categories, operator, in one
row. **Downgrades** (vision SUV, operator Car — the cash-difference vector) get the flag and
optionally a mandatory reason; upgrades log without one. Reports: discrepancies per operator
per [[shift]].
- **Never block.** A wrong classifier must not stop a wash. Flag, don't gate.
- Classifier output is advisory data on the event, **never a tariff input by itself**; threshold
and the flagged-category set are **site config** (a minivan-heavy site tunes the noise down).
- CPU: a second model per frame on the i5-8500 — analyse one frame per vehicle, not every push.
## Car Wash — the pilot module (settled 2026-09-05)
- **Car Wash is the pilot for the registry (settled).** It is built *as* the first module, and
the acceptance test of the module design is that it needs zero changes outside its own folder
and registry line. Validation removal clears the ground first.
- **The wash sits inside the parking (settled).** Every vehicle therefore already has a
[[parking-session]], a plate, and an entry snapshot — the plate is the customer identity for
free, no intake capture, and every anti-fraud signal below works from day one. Walk-ins from the
street are out of scope.
- **A camera on the wash bay (settled).** Through the same stateless [[vision-service]]; its job
is presence/vehicle counting at the bay, not plates.
### v1 scope
- **In:** a services catalogue priced by vehicle category; orders with a queue (waiting → in
progress → done → paid); vision's category as the advisory pre-selection with the override
flag (above); payment through the existing [[shift]] / cash drawer / P2PE terminal; receipts
on the existing printer path; one integration with parking — a completed wash may comp or
credit the session, emitted as a ledger event the parking module reacts to; reports per
operator and per shift. A wash desk is just a second desktop install pointed at the same
server (runtime backend address, [[desktop-shell-tauri]]).
- **Out (each is its own module-sized thing):** memberships / prepaid packages, loyalty, chemical
stock, staff scheduling, appointment booking, customer accounts. Design the order so a payment
can later reference a package, and stop there.
### Anti-fraud — the reason this fits here and not a generic wash product
Same adversary as the booth ([[threat-model]]): the person taking cash. The fraud is the
**unrecorded wash** — cash pocketed, nothing in the system. Two signals, both from things the
platform already owns:
1. **Session vs order.** A vehicle that dwelt at the bay (bay camera presence, or simply a long
session with no order) and exited with no wash order → `anomaly` ledger event with the entry
snapshot and the dwell evidence attached.
2. **Bay count vs order count.** The bay camera counts vehicles washed per shift; orders recorded
per shift come from the ledger; a divergence above a site-config tolerance → `anomaly` per
shift, on the operator's record. Never blocks the wash; reporting only.
Plus the category-override flag described above (SUV recorded as Car).
### Build order
1. Retire the Lavazh validation station (small; Bar station and all tables stay).
2. ~~Platform name~~ — settled, unchanged.
3. Registry: `site_config.modules`, manifest type, `requireModule`, nav gating,
`MODULES_ENTITLED` env; register `parking` and `validation` without moving code.
4. Car Wash v1 as above, on the staging booth. Vision category last — it needs gate frames
collected and labelled first; the bay-count signal can ship before it (presence only).
Rough size: four to six weeks including the registry.
## Open questions to settle before building
- ~~Platform name~~ — **settled 2026-09-05: it stays `parking-system` / `com.parking.desktop`.**
"This is a Parking Systems after all." The second-tier concern is answered by the architecture
(peer modules on a shared core), not by renaming; the irreversible-identifier warning above
remains true and is now simply moot.
- **Which body-type categories the Car Wash tariff actually needs** — decides COCO-five vs
training.
- **Entitlement as env vs signed file** — start with env; revisit only for commercial reasons.
- The **Bar** data model — separate scoping session (Car Wash v1 scope is above).
## Related
[[standing-decisions]] · [[desktop-shell-tauri]] · [[fleet-deployment-komodo]] ·
[[validation-discounts]] (kept for the Bar; Lavazh station retired) · [[validation-sponsorship]] · [[vision-service]]
· [[threat-model]] · [[append-only-event-chain]] · [[open-questions]] #15
+1
View File
@@ -135,6 +135,7 @@ Counts: 4 sources · 19 entities · 47 concepts · 8 decision records.
- [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python. - [[vision-service-packaging]] — the vision service lives in this monorepo (apps/vision/), separate process, wired into Turbo via a package.json shim; uv-managed Python.
- [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events). - [[event-streams-split]] — split the signed business ledger (ledger_events) from unsigned device telemetry (device_events).
- [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. Auto-updater mirrors signed releases to public `mca/public_releases` (source repo is private — field appliances have no Gitea creds). - [[desktop-shell-tauri]] — ✅ Tauri v2 chosen over Electron for the desktop kiosk shell; thin wrapper, server keeps all logic. Best case Ubuntu 26.04 LTS (resolves WebKitGTK); worst case Windows+WSL → kiosk browser, no native shell. Auto-updater mirrors signed releases to public `mca/public_releases` (source repo is private — field appliances have no Gitea creds).
- [[venue-modules]] — 🟡 OPEN: optional per-site modules (Car Wash, Bar/Restaurant) with Parking as a peer module on a venue POS/audit core; manifest registry, entitled ∩ activated enablement (vendor env + site-admin config), validation kept for the Bar (Lavazh station retires with Car Wash), name stays parking-system, vision vehicle-category as an advisory anomaly flag.
- [[container-deployment]] — Docker images for the non-desktop apps: parking-server (Fastify API + bundled SPA via @fastify/static) + parking-vision (Python/uv ANPR); branch+SHA tags, per-env compose, Gitea registry, build-images.yml CI; pnpm deploy (not prune) for native better-sqlite3; migrate-at-boot. - [[container-deployment]] — Docker images for the non-desktop apps: parking-server (Fastify API + bundled SPA via @fastify/static) + parking-vision (Python/uv ANPR); branch+SHA tags, per-env compose, Gitea registry, build-images.yml CI; pnpm deploy (not prune) for native better-sqlite3; migrate-at-boot.
- [[fleet-deployment-komodo]] — fleet control plane: Komodo Periphery on each booth, driven by Komodo Core over a NetBird mesh, running the same compose files. Deploys manual + pinned to dev-<sha> (no webhook); secrets Komodo-managed per-booth+unique; booth.sh demoted to break-glass. Threat-model caveats: Periphery is a root agent (mesh-bound only), EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs. komodo/ is infra-as-code. - [[fleet-deployment-komodo]] — fleet control plane: Komodo Periphery on each booth, driven by Komodo Core over a NetBird mesh, running the same compose files. Deploys manual + pinned to dev-<sha> (no webhook); secrets Komodo-managed per-booth+unique; booth.sh demoted to break-glass. Threat-model caveats: Periphery is a root agent (mesh-bound only), EVENT_SIGNING_KEY-in-Core is a fraud-root blast radius until ATECC608 signs. komodo/ is infra-as-code.
- [[appliance-provisioning]] — booth-PC provisioning runbook (Dell 7070, i5-8500, discrete Nuvoton TPM): BIOS/Secure-Boot → direct-flash Ubuntu 26.04 USB (not Ventoy) → passphrase-LUKS install → manual PCR-7 TPM seal (workaround for the installer's dbt PCR_UNUSABLE error) → Docker. Verified on hardware 2026-06-23; TPM auto-unlock works. - [[appliance-provisioning]] — booth-PC provisioning runbook (Dell 7070, i5-8500, discrete Nuvoton TPM): BIOS/Secure-Boot → direct-flash Ubuntu 26.04 USB (not Ventoy) → passphrase-LUKS install → manual PCR-7 TPM seal (workaround for the installer's dbt PCR_UNUSABLE error) → Docker. Verified on hardware 2026-06-23; TPM auto-unlock works.
+37
View File
@@ -2885,3 +2885,40 @@ Decision (user, 2026-09-04): the .deb stays, updates are an admin action behind
in-app prompt now says so (en + sq). A root systemd updater timer shipped in the .deb (minisign- in-app prompt now says so (en + sq). A root systemd updater timer shipped in the .deb (minisign-
verified, notify-only in-app) is recorded as the deferred fleet-grade option on verified, notify-only in-app) is recorded as the deferred fleet-grade option on
[[desktop-shell-tauri]]. [[desktop-shell-tauri]].
## [2026-09-04] decision | Venue modules design recorded as OPEN — Car Wash / Bar as peers of Parking
Captured the 2026-09-04 design conversation on [[venue-modules]]: a manifest-registry module
system (folder per module, always-migrated schema, one ledger union with prefixed event types,
relations only via manifest dependsOn + ledger events), enablement as entitled ∩ activated
(vendor-set Komodo env vs site-admin site-config toggle recorded as config_change; server
enforces with requireModule, web only hides; disabling never deletes), Parking recast as one
module on a venue POS/audit core, validation decommissioned (ledger type kept for history), the
desktop identifier / .deb name flagged as the one irreversible naming step, and vision-derived
vehicle category (SUV vs Car for the wash tariff) as an advisory signal that raises an `anomaly`
ledger event on operator override — never a tariff input by itself. Added as open-questions #15;
indexed.
## [2026-09-05] decision | Car Wash is the pilot module; wash inside the parking; bay camera
Settled with the user on [[venue-modules]]: Car Wash is built as the first module and is the
acceptance test of the registry (zero changes outside its folder). The wash sits inside the
parking, so every vehicle already has a session, plate and entry snapshot — no intake capture,
walk-ins out of scope. A bay camera (same stateless vision service, presence/counting not
plates) gives two anti-fraud signals for the unrecorded-wash vector: session-vs-order and
bay-count-vs-order-count per shift, both as `anomaly` ledger events, never blocking. v1 scope
(catalogue by category, queue, advisory vision category, existing shift/cash/receipts, one
parking comp/credit event, per-operator reports) and the out-list (memberships, loyalty, stock,
scheduling, booking, accounts) recorded, plus the build order. Remaining before code: platform
name (→ desktop identifier) and the validation-removal go.
## [2026-09-05] decision | Name stays parking-system; validation kept for the Bar, only the Lavazh station retires
Two revisions to [[venue-modules]] from the user: (1) the platform name stays — "this is a
Parking Systems after all" — so `com.parking.desktop` and the .deb name are untouched and the
irreversible-identifier concern is moot; the peer-module architecture, not a rename, answers the
second-tier worry. (2) Validation is NOT decommissioned: the merchant-scan flow is needed as is
for the Bar until a Bar module exists and absorbs it. Only the Lavazh station is retired when Car
Wash ships (Car Wash sponsors parking via its own order event). Two sponsorship mechanisms
coexist for now, accepted. Build order updated; validation is registered as its own module in the
registry so Bar can later depend on or absorb it.