feat(carwash): advisory vehicle category from the entry camera — mapping, pre-select, downgrade flag
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
This commit is contained in:
@@ -67,6 +67,18 @@ export const en: Catalog = {
|
||||
carwash: "Car wash",
|
||||
},
|
||||
},
|
||||
vehicleClass: {
|
||||
car: "car",
|
||||
sedan: "sedan",
|
||||
hatchback: "hatchback",
|
||||
suv: "SUV",
|
||||
minivan: "minivan",
|
||||
pickup: "pickup",
|
||||
van: "van",
|
||||
truck: "truck",
|
||||
bus: "bus",
|
||||
motorcycle: "motorcycle",
|
||||
},
|
||||
wash: {
|
||||
tillTitle: "Wash till",
|
||||
tillHint: "Money taken at the bay is recorded on the wash till — open your wash shift first. The booth's shift does not cover it.",
|
||||
@@ -124,6 +136,12 @@ export const en: Catalog = {
|
||||
sponsorship: "Parking discount",
|
||||
sponsorshipHint: "What a finished wash takes off the customer's parking fee. Applied automatically when a wash is marked done.",
|
||||
sponsorshipLabel: "Car wash",
|
||||
// Vision (advisory): the entry camera's body-type read, mapped to a site category.
|
||||
visionSaw: "Camera saw",
|
||||
visionUnmapped: "not mapped to a category",
|
||||
visionClasses: "Camera classes",
|
||||
visionThreshold: "Camera confidence to flag a downgrade",
|
||||
visionThresholdHint: "When the camera is at least this sure and the operator picks a cheaper category than the one its class maps to, the order is flagged for review. It is never blocked.",
|
||||
},
|
||||
update: {
|
||||
available: "Update available",
|
||||
@@ -385,6 +403,7 @@ export const en: Catalog = {
|
||||
"entry.operatorIssued": "Entry ticket issued by operator {{operator}} (physical button broken)",
|
||||
"entry.issue.noPresence": "Operator entry refused — no vehicle detected at the entry",
|
||||
"entry.duplicatePlate": "Possible duplicate entry — plate {{plate}} is already inside under ticket {{otherIdentity}}",
|
||||
"carwash.categoryDowngrade": "Wash category downgraded — camera saw {{visionClass}} ({{visionCategory}}), operator {{operator}} chose {{chosenCategory}}",
|
||||
"exit.refused.closed": "Exit refused — session already closed",
|
||||
"exit.refused.noSession": "Exit refused — unknown ticket",
|
||||
"exit.refused.unpaid": "Exit refused — not paid (take payment first)",
|
||||
|
||||
Reference in New Issue
Block a user