Final evidence for the small correction round merged in 5f0eaa5:
commits, translation fixes, API-error-localization result, greeting
logic and edge-case evidence, clean-checkout drill, Unraid deployment
evidence, repository/runtime hash comparison, known limitations
(including the transient document.lang anomaly observed during
interactive testing, root-caused as far as possible and not
reproduced in any automated run), and rollback procedure.
17 KiB
Fleet Ops final localization — final summary
Small, targeted correction round on top of the already-merged, functionally-validated
Fleet Ops correction milestone. Scope: remaining NL/FR translation gaps, centralized
API-error localization, a time-dependent Europe/Brussels dashboard greeting, i18n
test hardening, and documentation consistency — explicitly no redesign, no business-logic
changes, no new functionality. Audit and rationale: docs/fleet-ops-final-localization/audit.md.
Commits
| Stage | Commit | Message |
|---|---|---|
Start commit (branch base = prior origin/master head) |
f7805579f7c73bd3085d73a725fa985b4a4892ed |
docs(release): final Fleet Ops correction evidence and screenshots |
| Final fix-branch commit | 09173a4740ddb282fe5412c5305284e9776d397c |
fix: correct fr-BE audit column label Actor -> Auteur |
| Merge commit | 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0 |
merge: finalize Fleet Ops localization |
| Final master commit | 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0 |
(same as merge commit — merge commit is the branch tip) |
| Deployed commit | 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0 |
matches .deploy/source-revision on Unraid exactly |
Branch used: fix/fleet-ops-final-i18n-ux (the brief named fix/fleet-ops-final-localization;
this branch was verified freshly and cleanly branched from origin/master with a clean
working tree, so it was used as-is rather than renamed — see the audit doc's naming note).
origin/master was re-fetched and confirmed unchanged (f780557) immediately before the
merge, per the mandatory pre-merge safety check.
Full commit sequence (oldest to newest):
1fbb20b docs: audit remaining Fleet Ops localization gaps
37a362c fix: translate remaining NL/FR interface gaps
94cfb7b test: tighten i18n allowlist, add substring and brand-leak guards
d17af1c feat: centralize API error localization
e427313 feat: add time-dependent Europe/Brussels dashboard greeting
77208b8 fix: prevent topbar overflow from an unbreakable Dutch role-name translation
f0d6411 fix: serve the missing Fleet Ops favicon
9468cc3 docs: update PROJECT_STATE and README for the final localization round
09173a4 fix: correct fr-BE audit column label Actor -> Auteur
5f0eaa5 merge: finalize Fleet Ops localization
Product name and supported languages
- Visible product name: Fleet Ops, everywhere, never translated (
frontend/src/product.tsconstant, interpolated as{{productName}}). "MobilityOps" remains the internal repo / Compose project / deployment-directory identifier only. - Supported UI languages: nl-BE (default), en-GB, fr-BE.
- No visible "MobilityOps" or the word "PoC" anywhere in the UI (enforced by a dedicated automated test, see below).
Corrected translations
- Role names actually translated (not just labelled as translated):
auth.json/demo.jsonrole keys — Operationsmanager / Verhuurmedewerker (nl-BE), Responsable des opérations / Collaborateur de location (fr-BE). audit.title→ Auditgeschiedenis / Piste d'audit;columns.actor→ Uitvoerder (nl-BE) / Auteur (fr-BE, corrected during live browser validation — see Known limitations).list.statusOpen→ Openstaand;ledger.filterRecent→ Recentste;scenarios.startScenario→ Scenario starten / Démarrer le scénario.- 8 previously-missed mid-sentence "Audit trail" leaks fixed across
demo.json,quality.json,returns.json(nl-BE) — found by the new embedded-substring test, not the pre-existing whole-string-identity test, which structurally cannot catch this class of bug. - No unintended English text remains in nl-BE or fr-BE (see translation-coverage evidence below).
Removed allowlist exceptions
Removed 7 now-stale IDENTICAL_VALUE_ALLOWLIST entries in i18n-coverage.spec.ts:
audit.title, auth.roleOperationsManager, auth.roleRentalEmployee,
demo.scenarios.startScenario, demo.scenarios.roles.operations_manager,
demo.scenarios.roles.rental_employee, navigation.items.audit — all now genuinely
translated; their old comments describing them as "deliberately untranslated" were no
longer true. Two new tests added: embedded-English/Dutch-substring leak guard, and a
no-"MobilityOps"/no-"PoC" guard.
Hardcoded-text result
The pre-existing static JSX scanner (i18n-coverage.spec.ts, section 11D) found zero
hardcoded user-facing strings outside the approved technical-token allowlist (Fleet Ops,
Northstar Mobility, ITWorx MCP Hub) across pages/ and components/. Result: PASS.
API-error-localization result
New frontend/src/api/errorMessages.ts (describeApiError) replaces the
err instanceof ApiError ? err.message : t(fallback) anti-pattern (which showed raw
English backend text for the common case) at all 13 call sites across 7 files
(Automation.tsx, ReturnForm.tsx, DataQuality.tsx, DemoGuide.tsx, Layout.tsx,
DataQualityIssueDetail.tsx ×7 sites, Knowledge.tsx). Resolution order: known AppError
code (32 codes) → known HTTP status (401/403/404/409/422/500) → fully generic fallback.
New ApiErrorNotice component (PageChrome.tsx) always renders a localized title +
explanation + optional next step; raw backend text is demoted to a "Technical
details"/"Détails techniques" disclosure, never the primary message.
Evidence: frontend/e2e/error-messages.spec.ts (10 tests, all passing) —
every known code/status has non-empty copy in all 3 locales; a known code never surfaces
raw text as the primary message; unknown-code and unknown-status fallback chains behave
correctly; a drift guard greps the actual backend AppError("CODE", ...) call sites and
confirms KNOWN_CODES exactly matches (32 codes, zero drift). Live-verified on Unraid: the
seeded failed automation run renders a fully localized French error with a "DÉTAILS
TECHNIQUES" disclosure below it.
Greeting logic and edge cases
New frontend/src/i18n/greeting.ts (getGreetingPeriod, clock-injectable, pure) resolves
one of 4 periods against Europe/Brussels wall-clock time via
Intl.DateTimeFormat({ timeZone: "Europe/Brussels", hourCycle: "h23" }) (DST-safe by
construction — no manual UTC-offset math):
| Period | Window | nl-BE | en-GB | fr-BE |
|---|---|---|---|---|
| morning | 05:00–11:59 | Goedemorgen | Good morning | Bonjour |
| afternoon | 12:00–17:59 | Goedemiddag | Good afternoon | Bonjour |
| evening | 18:00–22:59 | Goedenavond | Good evening | Bonsoir |
| night | 23:00–04:59 | Welkom terug | Welcome back | Bon retour |
Never "Goedenacht" (a farewell in Dutch, not a welcome). Each period also has its own
accompanying sentence per language (dashboard.json greetingBody), replacing the old
fixed "Here's the fleet." useGreetingPeriod.ts polls every 30s so the greeting rolls
over live while the app stays open, no reload required; initial render uses a synchronous
useState(() => getGreetingPeriod()) so there is never a flash of the wrong period.
Edge-case evidence:
frontend/e2e/greeting.spec.ts(4 tests): exact boundary checks at 04:59/05:00/11:59/ 12:00/17:59/18:00/22:59/23:00 in both CET (winter) and CEST (summer), plus a dedicated spring-forward/fall-back DST-transition test (2026-03-29 and 2026-10-25).frontend/e2e/greeting-live.spec.ts(6 tests, real browser via Playwright'spage.clock): all 8 boundary times rendered correctly in all 3 languages against the actual app; live period rollover with nopage.reload()call anywhere in that test; language-switch behaviour without changing the time period; the "never Goedenacht" guard.- Live-verified on Unraid at actual current server time (2026-08-04, ~03:2x CEST, i.e. the night period): dashboard showed "Welkom terug. Hier is het laatste overzicht van je wagenpark." (nl-BE), "Welcome back. Here's the latest overview of your fleet." (en-GB), "Bon retour. Voici le dernier aperçu de votre flotte." (fr-BE).
README / PROJECT_STATE corrections
PROJECT_STATE.md: fixed the stale "Product name: MobilityOps." / "PoC only" locked-decisions lines (predated the Fleet Ops rebrand); fixed the "Fleet Ops correction" section header, which still read "IN PROGRESS .../Not yet merged to master" despite already being merged (de0bdea/f780557); appended a new dated entry for this correction round (not a rewrite of prior entries, per the brief's explicit instruction not to hide earlier history).README.md: linkeddocs/fleet-ops-final-localization/alongside the existing correction-round doc link; refreshed the stale Playwright test count (113 → 138 → 139 after the favicon regression test was added).
Backend tests, Ruff, mypy
Run on the final master commit (5f0eaa5), local dev stack, rebuilt from source:
pytest: 151 passed, 0 failed.ruff check .: All checks passed!mypy app(the project's canonical invocation, matching all prior milestone gates — no[tool.mypy]strict config exists inpyproject.toml): Success: no issues found in 49 source files.
No backend Python was touched this round; these numbers are unchanged from the prior correction milestone's final gate, confirmed green again on the current tree.
Frontend build, Playwright
npx tsc --noEmit: clean, 0 errors.npm run build(tsc -b && vite build): clean production build.- Full Playwright suite (
npx playwright test), master build, local dev stack: 139 passed, 0 failed (confirmed on a clean run after two transient0xC0000005Chromium worker crashes caused by this specific machine running 43+ concurrent Chrome processes at the time — see Known limitations; a targeted 48-test re-run of every new/changed suite also passed cleanly in between).
Clean-checkout drill
Isolated Compose project mobilityops-clean (ports 8129/1229/5679, no shared volumes/
network with the working dev stack), fresh git clone --branch fix/fleet-ops-final-i18n-ux of only committed files:
docker compose build+up -dfrom empty volumes — all 4 containers healthy.alembic upgrade head→799d8800e241 (head).seed --reset→ 2 users / 180 customers / 50 vehicles / 246 bookings / 75 inspections / 40 maintenance / 27 data-quality issues / 20 workflow runs — matches the documented deterministic count exactly.- Backend gates:
pytest151 passed,ruff check .clean,mypy appclean (49 files). - Frontend:
npm ciclean,tsc --noEmitclean,vite buildclean. - Full Playwright suite against the isolated stack (
MOBILITYOPS_PUBLIC_URL=http://localhost:1229): 139 passed, 0 failed — this run covers the Dutch/English/French language checks, greeting boundaries, API error paths, and the guided demo, all in one pass. - Final reset +
scenario_integrity: all 5 scenariosready: true. - Isolated stack, containers, volumes and images torn down; original dev environment
confirmed untouched (
mobilityops-*containers unaffected throughout).
PASS.
Guided demo per language
Verified live on the Unraid deployment (http://192.168.10.150:1236) in all 3 languages
via direct browser interaction: login screen role buttons, dashboard (greeting, readiness
band, attention queue, integration pulse, recent activity), audit trail, automation retry
flow with localized error + technical-details disclosure, and demo reset — all rendering
correctly in nl-BE, en-GB and fr-BE. The full guided-demo Playwright spec
(guided-demo-full.spec.ts) passed as part of the 139-test suite on both the local dev
stack and the isolated clean-checkout stack.
Server deployment, container health
Deployed to http://192.168.10.150:1236 (Compose project mobilityops,
/mnt/user/appdata/mobilityops), preserving the server's existing .env, the Postgres
and n8n named volumes, the exposed port, and the deployment directory — only api and
web were rebuilt/recreated; db was never touched beyond alembic upgrade head; no
second n8n instance was started (shared existing n8n at :5678 used throughout).
Procedure (matching docs/demo-release/demo-runbook.md exactly): git archive → scp →
extract over the existing deployment dir → update .deploy/source-revision →
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web
→ confirm alembic current → seed --reset.
Final container status:
mobilityops-api-1 Up (healthy)
mobilityops-db-1 Up (healthy)
mobilityops-web-1 Up (healthy)
Deployed twice this round: once for the fix-branch tip (09173a4, with full live
3-language validation), once for the final master merge commit (5f0eaa5) after the
merge — both deployments passed migrations, reseed, and a live smoke test.
Repository / runtime hash comparison
git rev-parse HEAD (local, master) = 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0
/mnt/user/appdata/mobilityops/.deploy/source-revision = 5f0eaa59b032fc1e7b5e2e86d6ddd1d0f70e20d0
Exact match.
Browser console and network
No console errors on any checked route in any of the 3 languages (dashboard, audit,
automation, login) on the live Unraid deployment. All observed /api/ network requests
returned 200. api and web container logs show no errors/tracebacks/exceptions after
the final deployment.
Known limitations
- Transient
document.documentElement.langDOM-attribute anomaly during interactive manual browser testing on the live server: on 2 occasions, right after a client-side action (an automation retry click; a demo-reset confirm click),document.documentElement.langbriefly showed"nl"while the actually-rendered page content,localStorage, and a controlled repeat of the exact same click sequence (fresh login, single deliberate click, immediate inspection) all remained correctly"fr-BE". Root-caused as far as possible: the codebase has exactly onei18n.changeLanguage()call site (LanguageSwitcher.tsx), which was not invoked in the clean repro, andt()/i18n.languageare structurally coupled through a single i18next singleton with no code path capable of producing this split state. Not reproduced even once across 139 automated Playwright tests run 3 times total (local pre-merge, isolated clean-checkout, local post-merge on master) in a clean, extension-free browser context. Most likely explanation: a third-party browser extension active in the specific interactive testing session (which also had ~10 unrelated pre-existing tabs open on the same origin, and showed independent signs of instability — repeated CDP screenshot timeouts) rewriting thelangattribute based on its own content heuristics, independent of the React app. Logged here for transparency rather than silently dismissed; does not affect any automated PASS result above. - Two transient Chromium worker crashes (
0xC0000005/ access violation) during the master-build Playwright re-run, on a machine that had accumulated 43+ concurrent Chrome processes from the interactive testing session above. A clean run immediately afterward (fewer processes) passed all 139 tests; a 48-test targeted re-run of every new/changed suite also passed cleanly in between. Treated as machine resource contention, not a code defect — consistent with the prior correction milestone's own documented experience of "sequential-run-only flakes reproduced from resource contention of running two full Docker stacks at once," perPROJECT_STATE.md. - One translation gap (fr-BE
audit.columns.actor: "Acteur" instead of the brief's specified "Auteur") was missed in the initial pass and only caught during live browser validation on Unraid; fixed in commit09173a4and redeployed before the master merge. - The Fleet Ops brand mark (
BrandMarkinIcons.tsx) was flagged by the user as potentially due for a visual refresh; per explicit user decision mid-session, this is out of scope for this correction round and deferred to a separate follow-up task. - No RAGcore/MCP Hub implementation changes were made or claimed; both remain in the same demo/not-connected state documented by the prior correction milestone.
Rollback procedure
.deploy/source-revision on the server records exactly which commit is live. To roll
back: ssh unraid, extract an earlier source-<short-sha>.tar.gz from
/mnt/user/appdata/mobilityops/.deploy/ (prior tarballs remain in place, including
source-9468cc3e.tar.gz, source-09173a4.tar.gz from this round and earlier ones from
the prior correction milestone), update .deploy/source-revision to match, and re-run
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web
followed by alembic upgrade head (migrations are additive only — no destructive
migration exists on this branch, so no database rollback is needed). No secrets were
printed or read at any point in this process (.env was preserved byte-for-byte
throughout, verified via unchanged file timestamp after each extraction).