fix: safe status-recommendation flow, MO-016 order independence, brand constant, message codes

- Add a single shared, pure vehicle-status evaluator (app/services/vehicle_status.py)
  used identically by the data-quality scanner, a new non-mutating status-recommendation
  preview endpoint, and a transactional apply endpoint with optimistic-concurrency token
  revalidation -- eliminates the old opaque "calculate and apply" action and the unsafe
  "maintenance + active booking -> auto rented" shortcut. Frontend
  DataQualityIssueDetail.tsx now shows a review/decide/confirm panel with localized
  why/evidence/consequence text in nl-BE/en-GB/fr-BE, with an exact "Change status to
  <status>" confirm action per the brief.
- Fix MO-016 issue-order dependency: resolving the booking-overlap issue before vs.
  after the status-conflict issue now converges on the same final vehicle status,
  proven by test_mo_016_status_conflict_recommendation_is_order_independent.
- Make "Fleet Ops" a non-localizable brand constant (frontend/src/product.ts,
  backend PRODUCT_NAME) via {{productName}} interpolation everywhere the brand name
  appeared in locale prose; add a permanent test guarding against a translation file
  ever defining the brand name or an "appName" key again.
- Convert dynamic backend prose to stable message codes + params: return status
  reasons, audit field/actor-type labels, automation last_error, and search
  section/vehicle/booking/issue results all now carry codes the frontend localizes,
  with raw technical text demoted to a "Technical details" disclosure.
- docs/fleet-ops-correction/: gap audit, i18n inventory, and the vehicle-status
  decision table documenting the evaluator's rules and safe-status principles.

148 backend tests + Ruff + mypy green; Alembic migration verified upgrade/downgrade;
frontend tsc/build and the i18n-coverage Playwright suite green.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
NuklearRabbit
2026-08-03 21:37:34 +02:00
co-authored by Claude Sonnet 5
parent 18344bc8b7
commit 6deb95524d
68 changed files with 1734 additions and 225 deletions
@@ -40,6 +40,29 @@
"was": "{{field}}: was {{value}}",
"changed": "{{field}}: {{before}} → {{after}}"
},
"actorTypes": {
"user": "Gebruiker",
"system": "Systeem",
"service": "Dienst"
},
"fields": {
"status": "Status",
"operational_status": "Operationele status",
"registration_number": "Kenteken",
"make": "Merk",
"model": "Model",
"location": "Locatie",
"first_name": "Voornaam",
"last_name": "Achternaam",
"email": "E-mail",
"phone": "Telefoon",
"postal_code": "Postcode",
"city": "Stad",
"booking_end_odometer_km": "Eindkilometerstand boeking",
"vehicle_odometer_km": "Kilometerstand voertuig",
"survivor": "Behouden profiel",
"loser": "Samengevoegd profiel"
},
"actions": {
"demo_login": "Ingelogd",
"demo_logout": "Uitgelogd",
+1 -1
View File
@@ -3,7 +3,7 @@
"orgLine": "Demo-organisatie: {{orgName}} (fictief)",
"headline1": "Elke overdracht.",
"headline2": "Eén helder overzicht.",
"defaultDescription": "Fleet Ops brengt voertuig-, boekings- en operationele gegevens samen, ondersteunt verhuurprocessen, detecteert datakwaliteitsproblemen en automatiseert gecontroleerde vervolgstappen.",
"defaultDescription": "{{productName}} brengt voertuig-, boekings- en operationele gegevens samen, ondersteunt verhuurprocessen, detecteert datakwaliteitsproblemen en automatiseert gecontroleerde vervolgstappen.",
"footnote": "Synthetische demo · geen echte klant- of voertuiggegevens · op elk moment herstelbaar",
"accessEyebrow": "Demo-toegang",
"accessHeading": "Kies hoe je wil starten",
+1 -2
View File
@@ -1,5 +1,4 @@
{
"appName": "Fleet Ops",
"orgName": "Northstar Mobility",
"brandTagline": "Bedieningscentrum",
"actions": {
@@ -35,7 +34,7 @@
"fr-BE": "Français"
},
"footer": {
"productLine": "Fleet Ops Demo",
"productLine": "{{productName}} Demo",
"locale": "Europa/Brussel · Synthetische demogegevens"
},
"demo": {
+4 -4
View File
@@ -83,7 +83,7 @@
"whatYouWillSee": "Een overzicht van wat in deze demo functioneel geïmplementeerd is, wat synthetisch is, en welke koppelingen nog niet live zijn.",
"whyItMatters": "Een demo is pas overtuigend als bezoekers zelf kunnen nagaan wat echt werkt en wat nog toekomstmuziek is.",
"startAction": "Lees de pagina 'Over deze demo'.",
"expectedOutcome": "Je kan zelf uitleggen wat Fleet Ops wel en niet is, zonder mondelinge toelichting."
"expectedOutcome": "Je kan zelf uitleggen wat {{productName}} wel en niet is, zonder mondelinge toelichting."
}
}
},
@@ -146,16 +146,16 @@
},
"about": {
"eyebrow": "Over deze demo",
"title": "Wat Fleet Ops wel en niet is",
"title": "Wat {{productName}} wel en niet is",
"description": "{{orgName}} is een fictieve verhuurorganisatie die dient om deze demo tastbaar te maken — geen bestaand bedrijf.",
"loading": "Demo-informatie laden…",
"ctaTitle": "Liever meteen aan de slag?",
"ctaBody": "De gegidste demo doorloopt alle acht stappen hierboven in de praktijk.",
"ctaButton": "Start begeleide demo",
"problemTitle": "Het fictieve probleem",
"problemBody": "{{orgName}} verhuurt zo'n 50 campers en bestelwagens vanuit één hoofdlocatie. Boekingen, retours, klantgegevens en onderhoud kwamen tot nu toe uit losse spreadsheets en mondelinge afspraken, waardoor fouten (dubbele klanten, foutieve kilometerstanden, dubbel geboekte voertuigen) laat aan het licht kwamen. Fleet Ops toont hoe één samenhangend systeem die problemen vroeg signaleert en gecontroleerd laat oplossen.",
"problemBody": "{{orgName}} verhuurt zo'n 50 campers en bestelwagens vanuit één hoofdlocatie. Boekingen, retours, klantgegevens en onderhoud kwamen tot nu toe uit losse spreadsheets en mondelinge afspraken, waardoor fouten (dubbele klanten, foutieve kilometerstanden, dubbel geboekte voertuigen) laat aan het licht kwamen. {{productName}} toont hoe één samenhangend systeem die problemen vroeg signaleert en gecontroleerd laat oplossen.",
"scopeTitle": "Voor wie en met welke scope",
"scopeBody": "Deze demo is bedoeld voor wie wil zien hoe Fleet Ops operationele problemen bij een kleine verhuurder aanpakt: Operations Managers en Rental Employees, en iedereen die de aanpak evalueert. De scope is bewust afgebakend tot één samenhangende proof of concept — geen boekhouding, geen betalingen, geen publieke reservaties, geen volledig CRM of ERP.",
"scopeBody": "Deze demo is bedoeld voor wie wil zien hoe {{productName}} operationele problemen bij een kleine verhuurder aanpakt: Operations Managers en Rental Employees, en iedereen die de aanpak evalueert. De scope is bewust afgebakend tot één samenhangende proof of concept — geen boekhouding, geen betalingen, geen publieke reservaties, geen volledig CRM of ERP.",
"realTitle": "Wat écht werkt",
"realBody": "Alles hieronder is functionele code, niet alleen een mockup: rol-gebaseerde toegang en sessies, voertuig- en boekingsbeheer, retourverwerking met serverzijdige validatie, vijf datakwaliteitsregels met elk een eigen oplossingsstap, een volledige audit trail, geautomatiseerde aflevering naar n8n met begrensde herpogingen, Docker-gebaseerde deployment en een geautomatiseerde testsuite (backend en Playwright end-to-end).",
"syntheticTitle": "Wat synthetisch is",
+9 -1
View File
@@ -63,6 +63,14 @@
"noQualityIssues": "Geen kwaliteitsproblemen geregistreerd.",
"fuel": "Brandstof {{percent}}%",
"damage": "Schade",
"technicalWarning": "Technische melding"
"technicalWarning": "Technische melding",
"inspectionTypes": {
"departure": "Vertrekinspectie",
"return": "Retourinspectie"
},
"maintenanceCategories": {
"periodic_service": "Periodiek onderhoud",
"repair": "Herstelling"
}
}
}
@@ -65,6 +65,13 @@
"noAction": "—",
"eventTypes": {
"vehicle.returned.v1": "Voertuigretour verwerkt"
},
"errorCodes": {
"connectionError": "De workflowdienst was tijdelijk niet bereikbaar. De retour is veilig opgeslagen en kan opnieuw worden aangeboden.",
"malformedPayload": "De opdracht bevatte onvolledige gegevens en kon niet worden afgeleverd. De onderliggende gegevens blijven veilig bewaard.",
"remoteReportedFailure": "De workflowdienst heeft de aflevering geweigerd. De opdracht kan opnieuw worden aangeboden.",
"staleLeaseRecovered": "Deze opdracht werd hersteld nadat een eerdere afleverpoging vastliep zonder resultaat.",
"unknownError": "Er is een onverwachte fout opgetreden bij het afleveren van deze opdracht."
}
}
}
@@ -24,7 +24,7 @@
"Welke controles zijn verplicht vóór vertrek?"
],
"emptyTitle": "Evidentie voor antwoorden",
"emptyDescription": "Vraag naar retours, schade, inspecties of een andere geïndexeerde procedure. Fleet Ops verzint geen antwoord wanneer evidentie ontbreekt.",
"emptyDescription": "Vraag naar retours, schade, inspecties of een andere geïndexeerde procedure. {{productName}} verzint geen antwoord wanneer evidentie ontbreekt.",
"retrievalFlow": {
"question": "Vraag",
"sources": "Bronnen",
@@ -27,12 +27,22 @@
"resetConfirmYes": "Ja, herstellen",
"resetCancel": "Annuleren",
"resetFailed": "Demogegevens konden niet hersteld worden.",
"searchLabel": "Zoek in Fleet Ops",
"searchLabel": "Zoek in {{productName}}",
"searchPlaceholder": "Zoek wagenpark, boeking of onderdeel…",
"searchShortcutHint": "Ctrl K",
"searchSearching": "Zoeken…",
"searchUnavailable": "Zoeken is momenteel niet beschikbaar.",
"searchNoResults": "Geen resultaten voor \"{{query}}\".",
"searchVehicleSummary": "{{make}} {{model}} · {{location}}",
"searchSections": {
"overview": "Operationeel dashboard",
"fleet": "Wagenparkregister",
"bookings": "Verhuurboekingen",
"quality": "Kwaliteitswerkbank",
"knowledge": "Procedureassistent",
"integrations": "Automatiserings- en integratiestatus",
"audit": "Auditgeschiedenis"
},
"switchRole": "Wissel van rol",
"switchRoleTitle": "Wissel van demo-rol",
"languageSwitcherLabel": "Taal wijzigen"
+40 -5
View File
@@ -180,13 +180,48 @@
"heading": "Statusconflict oplossen",
"description": "Eén gezaghebbende regel beveelt een gecorrigeerde operationele status voor dit voertuig aan.",
"currentStatus": "Huidige status",
"calculateAndApply": "Aanbevolen status berekenen en toepassen",
"confirmTitle": "Statuswijziging bevestigen",
"confirmBody": "De gezaghebbende aanbevolen status voor dit voertuig toepassen?",
"reviewRecommendation": "Aanbeveling bekijken",
"loadingRecommendation": "Aanbeveling laden…",
"recommendationFailed": "Kon geen aanbeveling ophalen.",
"recommendedStatus": "Aanbevolen status",
"whyHeading": "Waarom",
"evidenceHeading": "Feiten",
"consequenceHeading": "Gevolg",
"consequence": {
"statusWillChange": "De voertuigstatus wordt gewijzigd naar {{status}}.",
"issueWillBeRechecked": "Dit kwaliteitsprobleem wordt opnieuw gecontroleerd.",
"changeWillBeAudited": "De wijziging wordt vastgelegd in de audit trail.",
"bookingsNotDeleted": "De boekingen zelf worden niet verwijderd."
},
"changeStatusTo": "Status wijzigen naar {{status}}",
"applying": "Bezig met toepassen…",
"confirmYes": "Ja, toepassen",
"applied": "Toegepast {{status}} — {{reason}}",
"applyFailed": "Kon geen aanbevolen status toepassen."
"applyFailed": "Kon geen aanbevolen status toepassen.",
"staleRecommendation": "De situatie is intussen gewijzigd. Bekijk de aanbeveling opnieuw voor u ze toepast.",
"reviewAgain": "Aanbeveling opnieuw bekijken",
"manualReview": {
"heading": "Handmatige beoordeling vereist",
"body": "De feiten voor dit voertuig spreken elkaar tegen. Een Operations Manager moet dit persoonlijk beoordelen in plaats van een automatische statuswijziging toe te passen.",
"hint": "Gebruik 'Uitstellen' of 'Verwerpen' hieronder, of onderzoek het voertuig en de betrokken boekingen manueel."
},
"noConflict": {
"heading": "Geen wijziging nodig",
"body": "De huidige status van dit voertuig komt al overeen met de feiten."
},
"evidence": {
"activeBookings": "Actieve boeking(en): {{refs}}",
"overlappingBookings": "Overlappende reserveringen: {{pairs}}",
"serviceThresholdReached": "Kilometerstand ({{odometer}} km) heeft de onderhoudsdrempel ({{threshold}} km) bereikt.",
"openOverlapIssue": "Openstaand boekingsoverlapprobleem: {{ref}}"
},
"reasonCodes": {
"vehicle.active_rental": "Dit voertuig heeft een actieve verhuring maar staat niet als verhuurd geregistreerd. De status wordt gecorrigeerd zodat ze de werkelijke situatie weerspiegelt.",
"vehicle.service_threshold_reached": "Dit voertuig heeft de onderhoudsdrempel bereikt. Het wordt naar onderhoud gezet zodat het niet wordt ingezet voordat de service is uitgevoerd.",
"vehicle.booking_conflict": "Dit voertuig heeft twee overlappende reserveringen. Het blokkeren voorkomt dat het opnieuw als inzetbaar wordt beschouwd voordat het boekingsconflict is opgelost.",
"vehicle.rental_ended": "De verhuring voor dit voertuig is beëindigd en er is geen actieve boeking meer. Het wordt terug beschikbaar gezet.",
"vehicle.manual_review_required": "De feiten voor dit voertuig spreken elkaar tegen (bijvoorbeeld een actieve boeking terwijl het voertuig in onderhoud staat, of een onderhoudsdrempel die samenvalt met een actieve verhuring). Dit vereist een menselijke beoordeling in plaats van een automatische statuswijziging.",
"vehicle.no_conflict": "De huidige status van dit voertuig komt al overeen met de feiten. Er is geen wijziging nodig."
}
}
}
}
+9 -1
View File
@@ -36,6 +36,13 @@
"odometerRegressionWarning": "De ingevoerde kilometerstand ({{submitted}} km) ligt onder de laatst bevestigde stand ({{canonical}} km). De laatst bevestigde kilometerstand wijzigt niet en er wordt een datakwaliteitsprobleem geopend.",
"nextBookingRisk": "Volgende boeking {{ref}} start {{when}} — mogelijk beïnvloed door deze retour.",
"nextBookingLowRisk": "Volgende boeking {{ref}} start {{when}} — laag risico.",
"reasonCodes": {
"returnBlockedDamageAndTechnical": "Zowel schade als een technische melding werden gemeld bij deze retour.",
"returnBlockedDamage": "Er werd schade gemeld bij deze retour.",
"returnBlockedTechnicalWarning": "Er werd een technische melding gemeld bij deze retour.",
"returnServiceThresholdReached": "De kilometerstand heeft de onderhoudsdrempel van {{threshold_km}} km bereikt.",
"returnRoutedToCleaning": "Geen schade, technische melding of onderhoudsdrempel; het voertuig gaat naar reiniging."
},
"commitList": {
"inspection": "Retourinspectie aanmaken",
"updateAtomic": "Boeking en voertuig atomair bijwerken",
@@ -66,8 +73,9 @@
"continueDemo": "Ga verder met de demo"
},
"scenario": {
"ariaLabel": "Demonstratiescenario",
"title": "Demonstratiescenario: afwijkende kilometerstand",
"body": "Dit voertuig staat momenteel op {{odometer}} km. Het onderstaande formulier is vooraf ingevuld met een retourstand die daaronder ligt — een teken van een foutieve invoer of een verwisseld voertuig. Bevestig de retour om te zien hoe Fleet Ops dit detecteert en afhandelt.",
"body": "Dit voertuig staat momenteel op {{odometer}} km. Het onderstaande formulier is vooraf ingevuld met een retourstand die daaronder ligt — een teken van een foutieve invoer of een verwisseld voertuig. Bevestig de retour om te zien hoe {{productName}} dit detecteert en afhandelt.",
"preparing": "Scenario voorbereiden…"
}
}