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
The app plumbing for venue-modules.md §"Vehicle category from vision"; the model is the
open half (no bundled recognizer emits body_type yet, so the desk shows nothing until
phase A lands in the vision service).
- Shared: VEHICLE_CLASSES vocabulary, VehicleRead, CARWASH_VISION_THRESHOLD_DEFAULT,
reason code carwash.categoryDowngrade; settings/order/lookup views carry the read.
- Vision contract: /analyze vehicle.body_type + confidence (service schema); the Node
client normalises to the vocabulary and drops the rest.
- Record: snapshot.ts stores the read in the plate's device_events row (or its own when
the plate was unreadable); vehicleForIdentity() resolves it like the plate.
- Car wash: carwash_categories.vision_classes (site mapping "car, sedan → Vetura"),
carwash_config.vision_threshold (signed config_change when it moves), four vision
columns on orders — migration 0030. Lookup returns vision + suggestedCategoryId.
- Desk pre-selects the mapped category and shows the read + snapshot thumbnail; Setup
offers class chips per category and the threshold. Operator decides.
- Flag: a read at/above the threshold whose mapped category prices HIGHER than the chosen
one signs one `anomaly` (both categories/prices, operator, snapshot) and stores its id on
the order. Equal/upgrade/unsure/unmapped → nothing. Recorded only, never blocks, no
reason prompt (user, 2026-09-06).
Tests in carwash.test.ts; wiki venue-modules (As built), opencv-anpr-service, log.
Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
Surface the advisory ANPR plate (device_events kind="read", keyed by
session identity — unsigned, prunable, never an access decision) next to
entry/exit events in the live feed and on active-session rows.
Resolved at serialize time (new plate-lookup.ts; prefers an entry read;
one device_events scan per page) like subscriber-name enrichment — the
signed ledger is untouched. Adds plate? to the shared LedgerEvent and to
ActiveSession/SessionLookup; a small amber badge in the UI.
Caveat: a vehicle_entry is signed + pushed over WS before the async ANPR
read lands, so a fresh feed row may show no plate until reload; always
present on active sessions.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V