feat(carwash): review outbox, booth side — plate-blurred vehicle crop + the operator's choice, queued for a trusted remote reviewer
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
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
---
|
||||
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.
|
||||
Reference in New Issue
Block a user