feat(collector): review collector skeleton — apps/collector, its own Komodo stack on the reviewer's host
CI / check (push) Failing after 40s
Build & push images / images (push) Failing after 32s
Build desktop / desktop (push) Successful in 5m24s

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
This commit is contained in:
2026-09-07 08:26:22 +02:00
parent ec44547122
commit b485e9870b
22 changed files with 1045 additions and 10 deletions
+52
View File
@@ -18,6 +18,37 @@ importers:
specifier: 6.0.3
version: 6.0.3
apps/collector:
dependencies:
'@fastify/multipart':
specifier: ^9.2.1
version: 9.4.0
'@parking/shared':
specifier: workspace:*
version: link:../../packages/shared
better-sqlite3:
specifier: 12.10.1
version: 12.10.1
fastify:
specifier: 5.8.5
version: 5.8.5
devDependencies:
'@types/better-sqlite3':
specifier: 7.6.13
version: 7.6.13
'@types/node':
specifier: 25.9.3
version: 25.9.3
tsx:
specifier: 4.22.4
version: 4.22.4
typescript:
specifier: 6.0.3
version: 6.0.3
vitest:
specifier: ^4.1.9
version: 4.1.9(@types/node@25.9.3)(jsdom@25.0.1)(vite@8.0.16(@types/node@25.9.3)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4))
apps/desktop:
dependencies:
'@tauri-apps/plugin-process':
@@ -744,12 +775,18 @@ packages:
'@fastify/ajv-compiler@4.0.5':
resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==}
'@fastify/busboy@3.2.2':
resolution: {integrity: sha512-yXSS27qPExaXeuLvMRMXOLtpipzfQYNjG3FkunDWKGfMYjKuhFXko9CVzqxm8jcF+lmtS9Fd89QNdh9XDjnbNg==}
'@fastify/cookie@11.0.2':
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
'@fastify/cors@11.2.0':
resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==}
'@fastify/deepmerge@3.2.1':
resolution: {integrity: sha512-N5Oqvltoa2r9z1tbx4xjky0oRR60v+T47Ic4J1ukoVQcptLOrIdRnCSdTGmOmajZuHVKlTnfcmrjyqsGEW1ztA==}
'@fastify/error@4.2.0':
resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==}
@@ -765,6 +802,9 @@ packages:
'@fastify/merge-json-schemas@0.2.1':
resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==}
'@fastify/multipart@9.4.0':
resolution: {integrity: sha512-Z404bzZeLSXTBmp/trCBuoVFX28pM7rhv849Q5TsbTFZHuk1lc4QjQITTPK92DKVpXmNtJXeHSSc7GYvqFpxAQ==}
'@fastify/proxy-addr@5.1.0':
resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==}
@@ -3380,6 +3420,8 @@ snapshots:
ajv-formats: 3.0.1(ajv@8.20.0)
fast-uri: 3.1.2
'@fastify/busboy@3.2.2': {}
'@fastify/cookie@11.0.2':
dependencies:
cookie: 1.1.1
@@ -3390,6 +3432,8 @@ snapshots:
fastify-plugin: 5.1.0
toad-cache: 3.7.1
'@fastify/deepmerge@3.2.1': {}
'@fastify/error@4.2.0': {}
'@fastify/fast-json-stringify-compiler@5.0.3':
@@ -3410,6 +3454,14 @@ snapshots:
dependencies:
dequal: 2.0.3
'@fastify/multipart@9.4.0':
dependencies:
'@fastify/busboy': 3.2.2
'@fastify/deepmerge': 3.2.1
'@fastify/error': 4.2.0
fastify-plugin: 5.1.0
secure-json-parse: 4.1.0
'@fastify/proxy-addr@5.1.0':
dependencies:
'@fastify/forwarded': 3.0.1