feat(carwash): entry-stream sampling for the review outbox; park-2 wired to the collector
Build & push images / images (push) Successful in 4m22s

The wash stream is small; the entry camera photographs every car in exactly the view the
classifier is trained on. The booth can now queue entry vehicle reads as pure training
material — crop + the camera's class, no order, no operator, no category.

- Core announces every vehicle read (deviceEvents.emitVehicleRead from snapshot.ts); the
  Car Wash module listens, samples entry reads in-process (sampleEntry: exactly one in N)
  and queues them (enqueueEntry). CARWASH_REVIEW_ENTRY_SAMPLE=N; 1 = every entry (storage
  and bandwidth are not the limit — user); 0/unset = off. Forwarded by compose.
- Packages carry kind: "wash" | "entry". Collector: kind column, entry meta validated
  without the operator fields, review screen shows an entry sample as such, export has a
  kind column, operator agreement computed from wash items only. Setup line shows
  "1 in N entries sampled"; status carries entrySample.
- komodo: park-2's four review lines enabled (collector URL by Netbird DNS name, booth-2,
  the shared per-booth secret, every entry sampled) — the collector is up on the overlay.
- Tests on both sides. Wiki: vision-review-outbox (entry stream + the internet-feed
  assessment), log.

Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
2026-09-07 09:38:55 +02:00
parent 3e57af5abc
commit dbbb051ebd
18 changed files with 242 additions and 64 deletions
+8
View File
@@ -1,3 +1,4 @@
import { deviceEvents } from "../../device-events.js";
import type { ServerModule } from "../index.js";
import { ReviewOutbox, reviewUploadConfigFromEnv } from "./review-outbox.js";
import { carwashRoutes } from "./routes.js";
@@ -18,6 +19,13 @@ export const carwashModule: ServerModule = {
const outbox = new ReviewOutbox(deps.db, app.log, cfg);
app.log.info(cfg ? `carwash review upload: on → ${new URL(cfg.url).host} as ${cfg.boothId}` : "carwash review upload: off");
outbox.start();
// Entry-stream sampling: one in N entry vehicle reads goes to the reviewer as pure
// training material (the gate view, no order attached). The core announces the read;
// the module decides. Off unless CARWASH_REVIEW_ENTRY_SAMPLE is set.
const offVehicleRead = deviceEvents.onVehicleRead((e) => {
if (e.direction === "entry" && outbox.sampleEntry()) void outbox.enqueueEntry(e.read);
});
app.addHook("onClose", async () => offVehicleRead());
app.addHook("onClose", async () => outbox.stop());
const service = new CarwashService(deps, app.log, outbox);
// A wash ordered with payAt = "booth" is a charge line on the parking settlement;