The far end of the Car Wash review outbox (wiki/concepts/vision-review-outbox.md): a small
Fastify + SQLite service in the monorepo (shares the payload contract and the class
vocabulary via @parking/shared), delivered to art-docker-station by its own stack so
nothing booth-side lands there and nothing of it on a booth.
- POST /ingest: bearer token per booth (constant-time), X-Booth-Id must match, multipart
meta + JPEG (magic checked, 2 MB cap), meta validated against the contract, idempotent on
the item id; crop stored at crops/<booth>/<item>.jpg on the volume + one items row.
- /review + /api/*: the reviewer's screen served by the process (Basic auth, one login):
one pending crop at a time, operator's pick and camera's pick beside it, one button/key
per vocabulary class + unusable + skip; stats per booth and per hashed operator
(agree / disagree / unusable — disagree = the reviewer's class is outside the operator's
category).
- GET /export/labels.csv: reviewed usable rows for training; formula-leading cells are
neutralised (booth-supplied names). Crops stay on the volume for the trainer on the host.
- Booth payload now carries operatorCategory.classes so the comparison needs no site setup.
- Delivery: apps/collector/Dockerfile (monorepo context), docker-compose.collector.yml
(bind to the overlay IP; commented `trainer` profile seam for the GPU), a third build
step in build-images.yml, a `wash-collector` stack in komodo/resources.toml with one
secret per booth referenced from both the collector's token list and the booth's own
stack (park-2 lines templated, commented, DNS name for the URL).
- Tests: app.test.ts (ingest ok/dup/refusals, review + stats + export, config). Image
built and smoke-tested locally (health, ingest, duplicate, auth, verdict, export).
Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
console.error/console.warn only forward to the server when the client log
level is debug/trace (default: info) — the earlier error-logging fix never
actually surfaced anything, and a real update failure produced zero logs
anywhere. desktop-updater.ts now calls logClient() directly, unconditionally,
plus download-progress events. Also documents the resource-sync-park-systems
branch misconfig (pointed at dev, Stacks are stage-tier) found while chasing
this — full writeup on fleet-deployment-komodo.md.
rongta-printer still named the cashino driver id (→ escpos + migration
0023 note); tariff-time-tiers listed the composer price preview as
deferred (→ delivered by the lab fee breakdown); tariff.md lab section
gained the breakdown + composer increment-guard paragraph; i18n.md now
records the "25 Qer 14:30" date standard + never-toLocaleString-for-
dates rule; fleet-deployment-komodo gained the park-lab stack + tier
table (the park-lab addition had also slipped the log — both fixed).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Model the staging-vs-production split that fleet-deployment-komodo flagged as open.
Three tiers: dev (working, no booth) -> stage (staging booth park-buzi, real-world
test) -> main (production, manual + pinned).
- build-images.yml: trigger on [dev, stage, main]. The tag computation is already
branch-derived, so :stage / :stage-<sha> build with no other change.
- komodo/resources.toml: park-buzi now branch=stage + TAG=stage-<sha> (pinned;
no webhook even on staging). BACKUP_KEY already wired as a per-booth secret.
- komodo/README.md: a Promotion (dev->stage->main) section; per-booth secret list
now includes backup_key; hard-rule #1 generalised to pinned <branch>-<sha>.
- wiki: fleet-deployment-komodo open-item resolved + a Promotion-tiers table;
deploy-trigger choice generalised; container-deployment tag list gains :stage.
Promotion is a merge: when dev is ready, merge dev->stage, CI builds the image,
bump TAG=stage-<sha> in resources.toml, deploy from Core. stage is branched from
dev HEAD so the first real-world test carries the full current app. Per-booth
secrets must pre-exist in Core; migrations run at boot so a promotion auto-migrates
the staging ledger (where a bad migration is caught before production).
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Adopt Komodo Periphery (over the NetBird mesh) as the booth fleet control plane,
superseding SSH-and-booth.sh. The booth runs the SAME compose files; Komodo Core
drives them remotely. booth.sh is demoted to a break-glass local fallback.
- komodo/resources.toml mirrors the working park-buzi Stack (built by hand in the
Core UI, then exported to TOML — field names match the running v2.2). Stack-only:
servers are created by the agent onboarding OUTBOUND (one-time onboarding key →
Periphery self-registers, auto-rotating keys, booth opens no inbound port), so
there is no [[server]] block. Per-booth secrets via [[...]] refs to Core's store.
- komodo/README.md + .env.komodo.example document the flow and the hard rules
(no webhook; onboarding/outbound/mesh-only; per-booth unique secrets; never
down -v the ledger volume).
- wiki/decisions/fleet-deployment-komodo.md records the decision + threat-model
analysis (Periphery is a root agent → mesh-bound; EVENT_SIGNING_KEY-in-Core is a
fraud-root blast radius until ATECC608 signs; Core is now Tier-0; GPL-3.0 is fine
as external ops tooling). container-deployment reframed (booth.sh = fallback);
index + log updated.
Verified end-to-end against a real booth (park-buzi): onboarded OK, Stack deployed,
all containers green, admin seeded.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V