feat(collector): review collector skeleton — apps/collector, its own Komodo stack on the reviewer's host
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:
@@ -60,10 +60,39 @@ Setup → Car wash show queued / delivered / abandoned + the last error.
|
||||
is off and **nothing is queued** (an unbounded queue nobody drains is worse than none). Set per
|
||||
booth in the Komodo stack env; compose forwards them.
|
||||
|
||||
## Not built yet: the collector
|
||||
## The collector — skeleton built 2026-09-06 (`apps/collector`)
|
||||
|
||||
A deliberately small service on the overlay: one ingest endpoint (token per booth, size cap),
|
||||
one review screen (crop, the operator's pick, the camera's pick → the reviewer picks the truth),
|
||||
one export (crops + reviewer labels, nothing else) for phase B training. Keep it that small —
|
||||
it must not grow into a fleet console. The Netbird policy: booths may reach the collector's
|
||||
ingest port and nothing else on it.
|
||||
A deliberately small Fastify + SQLite service **in this monorepo** (so it imports the payload
|
||||
contract and the class vocabulary from `@parking/shared` — the two ends cannot drift), delivered
|
||||
to the reviewer's host by **its own Komodo stack** (`wash-collector` in `komodo/resources.toml`
|
||||
→ `docker-compose.collector.yml` only; the booth stacks never see it and it never sees booth
|
||||
services). Image `parking-collector:<branch>-<sha>` from the same workflow as the others.
|
||||
Three surfaces, nothing else — it must not grow into a fleet console:
|
||||
|
||||
- **`POST /ingest`** — bearer token **per booth** (`COLLECTOR_BOOTH_TOKENS`, `boothId:token`
|
||||
pairs; constant-time compare), `X-Booth-Id` must match the token's booth, multipart `meta` +
|
||||
`image` (JPEG magic checked, 2 MB cap), `meta` validated field by field against the contract
|
||||
above (unknown vision class, non-id item, wrong booth → 422), **idempotent on the item id**
|
||||
(a retry after a lost 2xx → 200 `duplicate`). Stored: `crops/<booth>/<item>.jpg` on the
|
||||
volume + one `items` row. The booth now also sends `operatorCategory.classes` (the classes
|
||||
the chosen category covers at that site) so a reviewer's CLASS can be judged against the
|
||||
operator's CATEGORY without the site's setup.
|
||||
- **`/review`** (+ `/api/items`, `/api/items/:id/image`, `/api/items/:id/review`, `/api/stats`)
|
||||
— the reviewer's screen, served by the process itself (no build, no framework): one pending
|
||||
crop at a time, the operator's pick and the camera's pick beside it, one button (and one
|
||||
key) per vocabulary class + *unusable* + *skip*. HTTP Basic, one login
|
||||
(`COLLECTOR_REVIEWER_USER/PASS`), over the overlay. Stats: per booth received / pending /
|
||||
reviewed; per operator (booth + hash) **agree / disagree / unusable** — disagree = the
|
||||
reviewer's class is outside the operator's chosen category. That column is the honest-mistake
|
||||
/ fraud rate.
|
||||
- **`GET /export/labels.csv`** — reviewed, usable rows: item, booth, crop path, the reviewer's
|
||||
label, the operator's category + classes, the camera's class + confidence, downgraded, at.
|
||||
Crops are not packaged: the phase-B trainer runs **on the same host** (its GPU) and reads them
|
||||
off the volume — `docker-compose.collector.yml` carries the `trainer` seam as a commented
|
||||
`profiles: [train]` one-off job (next increment).
|
||||
|
||||
**Deploy notes.** Bind the published port to the host's **Netbird address** (`COLLECTOR_BIND`),
|
||||
never `0.0.0.0` on a host with a public interface; Netbird policy: booths → this host:8090 and
|
||||
nothing else. The host must be onboarded as a Komodo server like the booths. `TAG` is pinned
|
||||
and promoted with the booths (one sha for all stacks) — fine while the collector stays small;
|
||||
its own repo the day it needs its own cadence.
|
||||
|
||||
Reference in New Issue
Block a user