From db9c3e0e315ead081f19685125cb8f3f34b98c2e Mon Sep 17 00:00:00 2001 From: Julian Cuni Date: Sat, 5 Sep 2026 10:24:20 +0200 Subject: [PATCH] =?UTF-8?q?docs(wiki):=20venue=20modules=20design=20?= =?UTF-8?q?=E2=80=94=20Car=20Wash=20pilot,=20Parking=20as=20a=20peer=20mod?= =?UTF-8?q?ule=20(open=20decision)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- wiki/decisions/open-questions.md | 8 +- wiki/decisions/venue-modules.md | 237 +++++++++++++++++++++++++++++++ wiki/index.md | 1 + wiki/log.md | 37 +++++ 4 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 wiki/decisions/venue-modules.md diff --git a/wiki/decisions/open-questions.md b/wiki/decisions/open-questions.md index 3af3d23..88b9621 100644 --- a/wiki/decisions/open-questions.md +++ b/wiki/decisions/open-questions.md @@ -2,7 +2,7 @@ type: decision tags: [parking, decisions, open] sources: [parking-system-architecture] -updated: 2026-06-29 +updated: 2026-09-04 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 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]]. + +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]]. diff --git a/wiki/decisions/venue-modules.md b/wiki/decisions/venue-modules.md new file mode 100644 index 0000000..7fd31cc --- /dev/null +++ b/wiki/decisions/venue-modules.md @@ -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//` (`register(app, deps)` + schema), + `apps/web/src/modules//` (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 diff --git a/wiki/index.md b/wiki/index.md index 6fd75e8..edbd1ac 100644 --- a/wiki/index.md +++ b/wiki/index.md @@ -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. - [[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). +- [[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. - [[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- (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. diff --git a/wiki/log.md b/wiki/log.md index 3ae5fa3..5640d36 100644 --- a/wiki/log.md +++ b/wiki/log.md @@ -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- verified, notify-only in-app) is recorded as the deferred fleet-grade option on [[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.