e67f0ccef0
The operator's category choice is a hypothesis, not truth (user, 2026-09-06): each wash order with a vehicle read queues a package for a trusted reviewer over the private overlay (Netbird); the verdict becomes the phase-B training label and the per-operator error rate. wiki/concepts/vision-review-outbox.md. - Boxes: the vision service returns the vehicle bbox; snapshot.ts stores the vehicle and plate boxes on the read as FRACTIONS of the analysed frame (the stored snapshot is a downscaled copy); vehicleForIdentity() returns them. - carwash_review_outbox (migration 0031) + review-outbox.ts: crop = detector box + 8 % margin, ≤ 640 px, plate blurred in place from the plate box; payload carries a pseudonymous booth id and a keyed operator hash — no site name, no plate, no OSD, no bystanders; multipart POST with a per-booth bearer; 2xx → sent (image dropped); 400/404/413/415/422 → abandoned; anything else → backoff 1 min·2^n capped 6 h; voided orders and items older than 14 days abandoned unsent. Nothing queued while unconfigured. - Enqueue is fire-and-forget off the intake path in createOrder; the loop runs every CARWASH_REVIEW_INTERVAL_SEC (60) and stops on close. - GET /api/carwash/review/status (site:read) + a "Remote review" line in Setup → Car wash. - Env CARWASH_REVIEW_URL / _TOKEN / _BOOTH_ID (all three or off) documented in .env.example and forwarded by compose. - Tests: review-outbox.test.ts (crop + blur on a synthetic frame, config/pseudonyms, queue/drain/backoff/abandon, through the app). Wiki: new concept page, index, venue-modules As built, log. The collector is not built. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
70 lines
4.2 KiB
Markdown
70 lines
4.2 KiB
Markdown
---
|
||
title: Vision review outbox — harvesting the operator's category choice for a trusted reviewer
|
||
type: concept
|
||
status: booth side built 2026-09-06; collector pending
|
||
related: [venue-modules, opencv-anpr-service, threat-model, append-only-event-chain, network-isolation]
|
||
---
|
||
|
||
# Vision review outbox
|
||
|
||
**The idea (user, 2026-09-06).** The Car Wash desk asks the operator for the vehicle's category,
|
||
and the entry camera now proposes one ([[venue-modules]] §Vehicle category from vision). The
|
||
operator's choice is what we would love to train the body-type classifier on — but the
|
||
operator **cannot be fully trusted** (mistake or intent; the [[threat-model]]). So the booth
|
||
hands each decision to a **trusted party** who reviews the picture and the label remotely,
|
||
and *that* verdict is the training label — and, per operator, the honest-mistake / fraud rate.
|
||
The booths sit on a private zero-trust overlay (**Netbird**), so the hand-off can go to a very
|
||
locked-down collector without exposing anything to the open internet.
|
||
|
||
## Rules (all enforced in `apps/server/src/modules/carwash/review-outbox.ts`)
|
||
|
||
1. **Offline-first, never on the intake path.** Creating a wash order *queues* a package (fire
|
||
and forget — a failure is a log line); a background loop drains the queue when the overlay
|
||
is up. The wash never waits on the network.
|
||
2. **One-way.** The booth POSTs; nothing ever comes back into the booth's decisions. The signed
|
||
ledger ([[append-only-event-chain]]) stays the only record of what happened at the wash.
|
||
Reviewer verdicts stay central and reach the owner as a report per site.
|
||
3. **Nothing that names the site leaves the booth.**
|
||
- Only the vehicle **crop** (the detector's box + 8 % margin, ≤ 640 px) — no walls, no camera
|
||
OSD (date / camera name burned into the frame), no bystanders.
|
||
- The **plate is blurred inside the crop** on the booth, from the plate detector's own box.
|
||
- The booth is a **pseudonymous id** set at deploy (`CARWASH_REVIEW_BOOTH_ID`); the operator
|
||
is a **keyed hash** (`sha256(boothId:username)[:16]`). The mapping back to places and
|
||
people is the reviewer's, held off the collector. The dataset export drops even those.
|
||
- Boxes are stored as **fractions of the frame** on the vision read, so the crop is cut from
|
||
the stored (downscaled) snapshot copy.
|
||
4. **The network is not the auth.** A per-booth bearer token on top of the overlay; the booth
|
||
can do nothing at the collector but this one POST. Payloads are small (a crop ≈ 50–80 kB).
|
||
5. **Data minimisation.** Queued only when there is a vehicle box (no box = no sample); the
|
||
image is dropped from the row once delivered; a voided order is abandoned unsent; anything
|
||
older than 14 days is abandoned ("expired") rather than resurfacing a fortnight in a burst.
|
||
|
||
## The package
|
||
|
||
`multipart/form-data`: `meta` (JSON) + `image` (JPEG). Meta = `{ v, booth, item, order, at,
|
||
operator (hash), operatorCategory {id,name}, service, vision {class, confidence, categoryId},
|
||
downgraded, image {width, height, plateBlurred} }`. Headers: `Authorization: Bearer <token>`,
|
||
`X-Booth-Id`.
|
||
|
||
## Draining
|
||
|
||
Every `CARWASH_REVIEW_INTERVAL_SEC` (60): due items oldest-first, 20 per pass. `2xx` → sent
|
||
(image cleared). `400/404/413/415/422` → abandoned (the collector refused the package itself).
|
||
Anything else (auth not yet fixed, 429, 5xx, timeout, no route) → retry with backoff
|
||
`1 min · 2^attempts`, capped at 6 h. `GET /api/carwash/review/status` (site:read) and a line in
|
||
Setup → Car wash show queued / delivered / abandoned + the last error.
|
||
|
||
## Config
|
||
|
||
`CARWASH_REVIEW_URL`, `CARWASH_REVIEW_TOKEN`, `CARWASH_REVIEW_BOOTH_ID` — all three or the outbox
|
||
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
|
||
|
||
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.
|