Files
parking_solution/wiki/decisions/venue-modules.md
T
julian db9c3e0e31 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
2026-09-05 10:24:20 +02:00

15 KiB
Raw Blame History

type, tags, sources, updated, status
type tags sources updated status
decision
parking
decisions
open
modules
architecture
2026-09-05 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).

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