Files
parking_solution/apps/web/src/modules/carwash/api.ts
T
julian e67f0ccef0 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
2026-09-06 22:33:43 +02:00

76 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { CarWashPayAt, CarwashOrderView, CarwashSettingsView, Tender, VehicleClass, VehicleRead } from "@parking/shared";
import { apiFetch } from "../../api.js";
// The Car Wash module's API client — module-local so apps/web/src/api.ts (the core
// client) never learns about wash endpoints. Shapes come from @parking/shared.
export type { CarWashPayAt, CarwashOrderView, CarwashSettingsView };
export interface CarwashTicketLookup {
identity: string;
found: boolean;
open: boolean;
subscription: boolean;
plate: string | null;
enteredAt: string | null;
currency: string | null;
orders: CarwashOrderView[];
/** What the camera saw at entry (advisory) and the category the site mapping
* suggests — pre-selected on the desk; the operator may change it. */
vision: VehicleRead | null;
suggestedCategoryId: string | null;
}
export interface CarwashSettingsBody {
categories?: { id?: string; name: string; active?: boolean; visionClasses?: VehicleClass[] }[];
services?: { id?: string; name: string; active?: boolean }[];
prices?: { categoryId: string; serviceId: string; priceMinor: number }[];
/** Where wash money is taken at this site (site-level; the desk no longer asks). */
payAt?: CarWashPayAt;
/** Confidence floor (0–1) for a vision class to flag a category downgrade. */
visionThreshold?: number;
}
/** The review outbox's health (Setup → Car wash). */
export interface CarwashReviewStatus {
enabled: boolean;
boothId: string | null;
queued: number;
sent: number;
failed: number;
lastSentAt: string | null;
lastError: string | null;
}
export function fetchCarwashReviewStatus(): Promise<CarwashReviewStatus> {
return apiFetch("/api/carwash/review/status");
}
export function fetchCarwashSettings(): Promise<CarwashSettingsView> {
return apiFetch("/api/carwash/settings");
}
export function saveCarwashSettings(body: CarwashSettingsBody): Promise<CarwashSettingsView> {
return apiFetch("/api/carwash/settings", { method: "PUT", body: JSON.stringify(body) });
}
export function lookupCarwashTicket(identity: string): Promise<CarwashTicketLookup> {
return apiFetch(`/api/carwash/session/${encodeURIComponent(identity.trim())}`);
}
export function fetchCarwashOrders(scope: "open" | "recent" = "open"): Promise<{ orders: CarwashOrderView[] }> {
return apiFetch(`/api/carwash/orders?scope=${scope}`);
}
export function createCarwashOrder(body: {
identity: string;
categoryId: string;
serviceId: string;
}): Promise<CarwashOrderView> {
return apiFetch("/api/carwash/orders", { method: "POST", body: JSON.stringify(body) });
}
export function markCarwashDone(id: string): Promise<CarwashOrderView> {
return apiFetch(`/api/carwash/orders/${encodeURIComponent(id)}/done`, { method: "POST" });
}
export function payCarwashAtBay(id: string, tender: Tender): Promise<CarwashOrderView> {
return apiFetch(`/api/carwash/orders/${encodeURIComponent(id)}/pay`, { method: "POST", body: JSON.stringify({ tender }) });
}
export function voidCarwashOrder(id: string, reason: string): Promise<CarwashOrderView> {
return apiFetch(`/api/carwash/orders/${encodeURIComponent(id)}/void`, { method: "POST", body: JSON.stringify({ reason }) });
}