Files
MobilityOps/artifacts/fleet-ops-final-localization/final-summary.md
T
NuklearRabbit c0995b762e docs(release): final Fleet Ops localization correction evidence
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.
2026-08-04 04:01:55 +02:00

17 KiB
Raw Blame History

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.ts constant, 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.json role keys — Operationsmanager / Verhuurmedewerker (nl-BE), Responsable des opérations / Collaborateur de location (fr-BE).
  • audit.titleAuditgeschiedenis / Piste d'audit; columns.actorUitvoerder (nl-BE) / Auteur (fr-BE, corrected during live browser validation — see Known limitations).
  • list.statusOpenOpenstaand; ledger.filterRecentRecentste; scenarios.startScenarioScenario 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:0011:59 Goedemorgen Good morning Bonjour
afternoon 12:0017:59 Goedemiddag Good afternoon Bonjour
evening 18:0022:59 Goedenavond Good evening Bonsoir
night 23:0004: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's page.clock): all 8 boundary times rendered correctly in all 3 languages against the actual app; live period rollover with no page.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: linked docs/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 in pyproject.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 transient 0xC0000005 Chromium 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:

  1. docker compose build + up -d from empty volumes — all 4 containers healthy.
  2. alembic upgrade head799d8800e241 (head).
  3. 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.
  4. Backend gates: pytest 151 passed, ruff check . clean, mypy app clean (49 files).
  5. Frontend: npm ci clean, tsc --noEmit clean, vite build clean.
  6. 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.
  7. Final reset + scenario_integrity: all 5 scenarios ready: true.
  8. 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 archivescp → extract over the existing deployment dir → update .deploy/source-revisiondocker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web → confirm alembic currentseed --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.lang DOM-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.lang briefly 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 one i18n.changeLanguage() call site (LanguageSwitcher.tsx), which was not invoked in the clean repro, and t() / i18n.language are 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 the lang attribute 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," per PROJECT_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 commit 09173a4 and redeployed before the master merge.
  • The Fleet Ops brand mark (BrandMark in Icons.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).