chore: remove generated deployment and audit evidence
This commit is contained in:
@@ -1,195 +0,0 @@
|
||||
# Demo-productization final summary
|
||||
|
||||
## Branches and commits
|
||||
|
||||
- **Gitea repository**: `ssh://git@192.168.10.150:222/Jens/MobilityOps.git` (browsable at
|
||||
`http://192.168.10.150:3000/Jens/MobilityOps`)
|
||||
- **Branch**: `feat/mobilityops-functional-completion` (no new branch created; no merge
|
||||
to `main`; no rebase/reset/squash/force-push; full git history preserved, as required)
|
||||
- **Start commit** (functional-completion baseline, already accepted):
|
||||
`e0c7ed60112510687627d20a957af91c8b9db7f8`
|
||||
- **Final commit**: `4a268c73515dc4f1d56c1aa2f231714654bffbb8` — verified via
|
||||
`git rev-parse HEAD` on `feat/mobilityops-functional-completion` and confirmed to match
|
||||
`/mnt/user/appdata/mobilityops/.deploy/source-revision` on the Unraid server exactly.
|
||||
(This corrects a self-reference gap in the immediately preceding pair of commits, which
|
||||
necessarily could not know their own hash at the time they were written; this is now
|
||||
the single, unambiguous, verified reference. The repository's primary branch is
|
||||
`master`, not `main` — no branch named `main` exists in this repository.)
|
||||
- **Live URL**: `http://192.168.10.150:1236`
|
||||
|
||||
## Demo organisation and context
|
||||
|
||||
**Northstar Mobility** — a fictitious Belgian camper/van rental company (~50 vehicles,
|
||||
one main location, rental team, an Operations Manager, a small workshop). This name was
|
||||
already a locked internal decision (`ragcore_tenant: northstar-mobility-demo`,
|
||||
`PROJECT_STATE.md`'s "Locked decisions") before this work — this pass surfaces it in the
|
||||
UI rather than inventing it. Full concept: `docs/demo-release/demo-concept.md`.
|
||||
|
||||
## Roles
|
||||
|
||||
- **Operations Manager** — full access: data-quality resolution, workflow retries, audit
|
||||
trail, demo reset, the Demo Guide.
|
||||
- **Rental Employee** — scoped access: bookings, returns, fleet, knowledge assistant.
|
||||
|
||||
Both are reachable from the login screen with no password.
|
||||
|
||||
## Demo Guide
|
||||
|
||||
An 8-step, sessionStorage-persisted guided tour (Operations-Manager-only, since every
|
||||
step requires that role). Full design: `docs/demo-release/demo-guide.md`. Steps: (1)
|
||||
understand operational state, (2) open the booking needing attention, (3) process the
|
||||
odometer-anomaly return, (4) handle the created data-quality issue, (5) merge the
|
||||
duplicate customer, (6) ask the knowledge assistant, (7) check automation + audit, (8)
|
||||
review real vs. synthetic vs. not-connected.
|
||||
|
||||
## Scenarios (all 5, full detail in `docs/demo-release/demo-scenarios.md`)
|
||||
|
||||
| # | Scenario | Fixed records | Role |
|
||||
|---|---|---|---|
|
||||
| 1 | Odometer regression on return | `BK-DEMO-RETURN` / `MO-024` | Either |
|
||||
| 2 | Possible duplicate customer | `CUS-0012` / `CUS-0178` / `DQ-DEMO-DUPLICATE` | OM |
|
||||
| 3 | Overlapping bookings | `MO-016` / `BK-DEMO-OVERLAP-A/B` / `DQ-DEMO-OVERLAP` | OM |
|
||||
| 4 | Failed automation, retried | outbox event `...020` / `BK-H-0020` | OM |
|
||||
| 5 | Grounded procedure question | (no fixed record; suggested questions) | Either |
|
||||
|
||||
`GET /api/v1/demo/manifest`'s `scenarios` array derives `ready`/`blocked_reason` from the
|
||||
live underlying records, never hardcoded — confirmed via `backend/tests/
|
||||
test_demo_manifest.py` (`test_demo_manifest_scenarios_ready_after_fresh_reset`) and
|
||||
live-checked after every reset throughout this work.
|
||||
|
||||
## Seed strategy and date-anchoring
|
||||
|
||||
`seed/generate_seed.py --anchor 2026-08-01 --seed 20260801` produces deterministic CSVs
|
||||
with absolute timestamps authored against a fixed anchor. `backend/app/seed_loader.py`
|
||||
shifts every seeded datetime by `(real today − authored anchor)` on every seed/reset, so
|
||||
"today"/"near-future"/"currently overlapping" scenarios stay true to the actual reset
|
||||
moment instead of decaying. This fixed a real, confirmed bug (`BK-DEMO-RETURN` was found
|
||||
sitting 2 days in the past before this fix). Full detail: `docs/demo-release/demo-data.md`.
|
||||
|
||||
## Reset strategy
|
||||
|
||||
`POST /api/v1/demo/reset` (Operations Manager only, gated by `DEMO_ALLOW_RESET`) clears
|
||||
MobilityOps's own tables, reseeds with a fresh date anchor, re-runs the data-quality scan,
|
||||
and runs a server-side scenario-integrity check (`scenario_integrity_report()`) recorded
|
||||
in both the response and the `demo_reset` audit event. Reachable from the sidebar, the
|
||||
Demo Guide, and the About page. Never touches shared n8n/RAGcore/MCP data, other
|
||||
containers, or volumes.
|
||||
|
||||
## Real vs. synthetic vs. not-connected
|
||||
|
||||
See `docs/demo-release/demo-concept.md` for the full breakdown. In short: auth/roles,
|
||||
vehicle/booking management, return preview/commit, the 5 data-quality rules and their
|
||||
resolutions, the audit trail, n8n orchestration, Docker deployment, and the automated
|
||||
test suite are all really implemented. The organisation, all people, vehicles, bookings,
|
||||
procedures, and the 5 named scenarios are synthetic. RAGcore and the ITWorx MCP Hub are
|
||||
not live-connected (honestly labelled "Demomodus"/"Niet gekoppeld" everywhere, never a
|
||||
fabricated success).
|
||||
|
||||
## Test results
|
||||
|
||||
### Backend (clean checkout, isolated stack)
|
||||
- `pytest`: **127 passed**
|
||||
- `ruff check .`: clean
|
||||
- `mypy app`: clean (48 source files)
|
||||
|
||||
### Frontend (clean checkout, isolated stack)
|
||||
- `npm ci`: clean (pre-existing esbuild-moderate/react-router-RSC-high advisories,
|
||||
unchanged from before this work — not introduced by it)
|
||||
- `tsc -b`: clean
|
||||
- `npm run build`: clean
|
||||
- Full Playwright suite: **56 passed** (against the isolated clean-checkout stack)
|
||||
|
||||
### Guided-demo test
|
||||
`frontend/e2e/guided-demo-full.spec.ts` — one comprehensive test walking a fresh
|
||||
Operations Manager session through all 8 Demo Guide steps performing the real action at
|
||||
each step (processes the actual odometer-anomaly return, resolves the resulting
|
||||
data-quality issue, merges the duplicate customer, asks a suggested knowledge question,
|
||||
checks automation + audit, reviews the About page), then resets the demo data again to
|
||||
restore the environment. **Passed**, confirmed stable across repeated runs both locally
|
||||
and against the live Unraid deployment.
|
||||
|
||||
### Clean-checkout drill
|
||||
Fresh `git clone` of this branch/commit into an isolated scratch directory, `.env` from
|
||||
`.env.example`, isolated Compose project name (`mobilityops-cleandrill`) and remapped
|
||||
host ports (`compose.override.yaml` with `!override` merge tags — no shared state with
|
||||
any other stack), `docker compose up --build -d` from empty volumes → migrations ran
|
||||
automatically (`e7b08389f47f (head)`) → seeded → full backend gate (127 passed, ruff/
|
||||
mypy clean) → `npm ci`/`tsc -b`/`vite build` clean → full Playwright suite (56 passed)
|
||||
→ reseeded and confirmed all 5 scenarios `ready: true` via the manifest → torn down
|
||||
(`docker compose down -v` on the isolated project only; the working dev stack was never
|
||||
touched).
|
||||
|
||||
### Server deployment
|
||||
Deployed incrementally after every batch (10 deploy cycles across this work); final
|
||||
state: both `api` and `web` rebuilt and healthy at the final commit, `db` untouched
|
||||
across all of them (no destructive migrations on this branch). Migrations at
|
||||
`e7b08389f47f (head)` throughout. `.deploy/source-revision` on the server matches the
|
||||
final commit exactly.
|
||||
|
||||
### Container health
|
||||
`docker compose ps` on the server: `api`, `db`, `web` all `healthy`, no restart loops.
|
||||
|
||||
### Browser console / network
|
||||
No unexpected console errors on login, dashboard, scenarios, About, or with the Demo
|
||||
Guide open (verified via `demo-accessibility.spec.ts`; the one benign 401 from the app's
|
||||
own session-probe on first load is expected and explicitly accounted for, not silenced
|
||||
blindly). No unresolved server errors in `docker logs` for `api`/`web` at the time of
|
||||
this evidence capture.
|
||||
|
||||
### Responsive / accessibility
|
||||
- Demo Guide renders as a correctly-anchored bottom sheet at 390px with no horizontal
|
||||
overflow (`demo-accessibility.spec.ts`).
|
||||
- **Real bug found and fixed**: the Demo Guide's fixed desktop side panel overlapped
|
||||
main content with no reflow, making the return form's "Review return" button
|
||||
unclickable while the guide was open at ordinary desktop widths — this surfaced while
|
||||
writing the full guided-demo test. Fixed via a `guide-open` layout class that reserves
|
||||
space for the panel; regression-tested.
|
||||
- Demo badge and Demo Guide triggers are keyboard-focusable and operable (Enter to open,
|
||||
explicit close controls).
|
||||
- Existing responsive-overflow checks (390/768/1280/1440px) remain green throughout.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- RAGcore and the ITWorx MCP Hub are not live-connected in this environment (by design
|
||||
— see scope). The knowledge assistant uses a local, English-only demo knowledge base;
|
||||
a Dutch question against it returns "insufficient evidence" (verified empirically), so
|
||||
suggested questions and the Demo Guide's step 6 instructions deliberately stay in
|
||||
English rather than silently breaking the demo's centerpiece grounded-answer feature.
|
||||
- Existing operational screens (Dashboard, Vehicles, Bookings, Data Quality workbench,
|
||||
Audit, Automation internals) remain in English; only new demo-productization surfaces
|
||||
(login, Demo Guide, scenario overview, About page, demo badge, plain-language
|
||||
integration labels) are in Dutch — a deliberate, documented scope decision, not an
|
||||
oversight (`docs/demo-release/current-demo-gap-audit.md`, gap #11).
|
||||
- Scenario S3 ("missing inspection before next booking", `MO-031`) is seeded and visible
|
||||
in the attention queue but isn't one of the 5 scenarios surfaced on `/scenarios`,
|
||||
matching the brief's request for exactly 5.
|
||||
|
||||
## 5-minute and 10-minute demo flows
|
||||
|
||||
See `docs/demo-release/demo-runbook.md` for the exact click-through scripts.
|
||||
|
||||
## Redeploy commands and rollback procedure
|
||||
|
||||
See `docs/demo-release/demo-runbook.md` — `git archive` → `scp` → extract → rebuild
|
||||
`api`/`web` → confirm migrations → reseed. Rollback: extract an earlier
|
||||
`.deploy/source-<short-sha>.tar.gz` and update `.deploy/source-revision` to match.
|
||||
|
||||
## Evidence screenshots
|
||||
|
||||
All captured live against `http://192.168.10.150:1236` (`artifacts/demo-release/screenshots/`):
|
||||
|
||||
1. `01-demo-entry-desktop.png` / `02-demo-entry-mobile.png` — demo entry, both sizes
|
||||
2. `03-dashboard-with-scenarios.png` — dashboard with the scenario teaser panel
|
||||
3. `04-demo-guide.png` — the Demo Guide panel open
|
||||
4. `05-return-preview.png` / `06-return-result.png` — the return flow
|
||||
5. `07-data-quality-resolution.png` — a data-quality issue with its plain-language explainer
|
||||
6. `08-duplicate-customer-merge.png` — the duplicate-customer comparison/merge UI
|
||||
7. `09-knowledge-assistant.png` — a grounded answer with cited sources
|
||||
8. `10-integration-status.png` — plain-language integration status on Automation
|
||||
9. `11-automation-retry-before.png` / `11-automation-retry-after.png` — a workflow retry
|
||||
10. `12-audit-trail.png` / `13-audit-related-events.png` — audit trail + correlation drill-down
|
||||
11. `14-about-demo.png` — the About page
|
||||
12. `15-demo-badge-popover.png` — the permanent synthetic-demo badge popover
|
||||
13. `16-reset-confirm.png` — the reset confirmation flow
|
||||
|
||||
No secrets appear in any screenshot or in this document.
|
||||
@@ -1,145 +0,0 @@
|
||||
# MobilityOps Unraid deployment evidence
|
||||
|
||||
## Outcome
|
||||
|
||||
- Deployment: **PASS**
|
||||
- Gitea publication: **PASS**
|
||||
- Gitea URL: `https://gitea.itworx.tech/Jens/MobilityOps`
|
||||
- Visibility: private (verified in the Gitea web UI)
|
||||
- Branch: `master`
|
||||
- Verified baseline commit: `4bf9afbeff44088864e0844769d4dd0e4089d85b`
|
||||
- Deployment implementation commit: `1e13943cffb2da8a328b5b1ea5e9b1fe73fdd774`
|
||||
- Server: `192.168.10.150`
|
||||
- Server directory: `/mnt/user/appdata/mobilityops`
|
||||
- Compose project: `mobilityops`
|
||||
- Application URL: `http://192.168.10.150:1236`
|
||||
- Port mapping: LAN `0.0.0.0:1236` / `[::]:1236` to `web:80`
|
||||
|
||||
## Services and health
|
||||
|
||||
| Service | Runtime state | Health | Host exposure |
|
||||
|---|---|---|---|
|
||||
| `db` | running, 0 restarts | healthy | none (`5432/tcp` internal) |
|
||||
| `api` | running, 0 restarts | healthy | none (`8000/tcp` internal) |
|
||||
| `web` | running, 0 restarts | healthy | `1236:80` on LAN |
|
||||
| shared host `n8n` | running | healthy | `5678:5678` on LAN; outside MobilityOps Compose |
|
||||
|
||||
The final review topology reuses the n8n container that was already running on the host.
|
||||
Its empty public-host/editor URL settings were corrected in the persistent Unraid template
|
||||
so workflow execution URLs are valid. The temporary Compose-owned n8n container was
|
||||
removed without deleting its retained volume. Port `1236` was confirmed unused before the
|
||||
original deployment; the application directory was created specifically for MobilityOps.
|
||||
|
||||
## Deployment commands
|
||||
|
||||
The existing SSH aliases resolve to the requested hosts and keys (`gitea.itworx.tech`
|
||||
for Gitea SSH and `unraid` for root access). No key was created, copied, or replaced.
|
||||
The committed source was transferred from the workstation; Unraid has no Gitea key.
|
||||
|
||||
Repository publication used the SSH clone URL supplied by Gitea:
|
||||
|
||||
```bash
|
||||
git remote add origin ssh://git@192.168.10.150:222/Jens/MobilityOps.git
|
||||
git push -u origin master
|
||||
git push origin --tags
|
||||
```
|
||||
|
||||
Git and the Gitea web UI both verified `master` as the default branch, the full commit
|
||||
history, baseline commit `4bf9afbeff44088864e0844769d4dd0e4089d85b`, and zero tags.
|
||||
The remote tree contains no `.env`, local database, `node_modules`, virtual environment,
|
||||
test cache, build cache, Playwright output, or browser binaries.
|
||||
|
||||
```bash
|
||||
git archive --format=tar.gz --output=<temporary-archive> <commit>
|
||||
scp <temporary-archive> unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
||||
ssh unraid
|
||||
cd /mnt/user/appdata/mobilityops
|
||||
tar -xzf .deploy/source.tar.gz
|
||||
./deploy/unraid/configure-env.sh http://192.168.10.150:1236
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api \
|
||||
python -m app.cli seed --reset
|
||||
./deploy/unraid/setup-existing-n8n.sh \
|
||||
n8n \
|
||||
http://192.168.10.150:1236/api/v1/integrations/n8n/return-callback
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up -d db api web
|
||||
```
|
||||
|
||||
The server `.env` was created from `.env.example`, is mode `0600`, and contains generated
|
||||
runtime secrets. Secret values and n8n owner credentials remain server-only and are not
|
||||
included here or in Git.
|
||||
|
||||
## Validation evidence
|
||||
|
||||
- Migration: `e7b08389f47f (head)`.
|
||||
- Deterministic seed: users 2, customers 180, vehicles 50, bookings 246, inspections 75,
|
||||
maintenance 40, data-quality issues 26, workflow runs 20.
|
||||
- HTTP: `GET /` returned 200; `GET /health` returned
|
||||
`{"status":"ok","service":"mobilityops-api"}` through the web proxy.
|
||||
- Backend gates in an isolated local Compose project: 66 tests passed, Ruff clean, mypy
|
||||
clean across 44 files.
|
||||
- Frontend: `npm ci && npm run build` completed (`tsc -b && vite build`).
|
||||
- Logs: no traceback, fatal, uncaught, or unresolved startup error in the deployment log
|
||||
scan. Browser console had no warnings or errors during the smoke test.
|
||||
- Browser smoke test in Chrome: Operations Manager demo login, Dashboard, Vehicles,
|
||||
Bookings, Data Quality, Knowledge, Automation, and Audit all loaded from the LAN URL.
|
||||
- Dashboard showed persisted seed metrics (21 available, 11 rented, 6 cleaning,
|
||||
5 maintenance, 7 blocked, 22 open issues, 1 pending/failed workflow).
|
||||
- Return workflow: `BK-DEMO-RETURN` accepted 54,700 km, created `INSP-0076` and
|
||||
`DQ-RET-0076`, preserved the 54,820 km canonical odometer, and changed the booking to
|
||||
returned.
|
||||
- Shared-n8n round trip: final post-deploy event `98eb06dc-0bcc-4e3d-96ec-c23b2d266293`
|
||||
reached `succeeded` on attempt 1 with no last error; the deterministic reset afterwards
|
||||
restored `BK-DEMO-RETURN` to `active`.
|
||||
- Data quality: `DQ-RET-0076` displayed the persisted regression evidence and related
|
||||
booking/inspection references.
|
||||
- Knowledge: UI truthfully showed `Provider: demo · available · 10 procedures indexed`;
|
||||
the damage question returned grounded excerpts and citations from the local procedures.
|
||||
|
||||
## Integration status
|
||||
|
||||
- RAGcore: disabled for this deployment; `KNOWLEDGE_PROVIDER=demo`. No claim of a live
|
||||
RAGcore connection is shown. Operational functionality is unaffected.
|
||||
- ITWorx MCP Hub: registration disabled with `MCP_HUB_REGISTRATION_ENABLED=false`; the
|
||||
independently authenticated provider endpoints remain available internally to the web
|
||||
proxy/API boundary, but no live Hub connection is claimed.
|
||||
- n8n: the existing server instance at `http://192.168.10.150:5678` is healthy; the
|
||||
MobilityOps workflow is imported/published there and a real return delivery succeeded.
|
||||
The bundled MobilityOps service is disabled by default in the Unraid overlay.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- RAGcore and ITWorx MCP Hub are intentionally not connected yet.
|
||||
- Demo authentication remains the accepted HMAC-cookie PoC mechanism.
|
||||
- The dependency advisories already documented in final acceptance remain unchanged.
|
||||
|
||||
## Redeploy
|
||||
|
||||
From the workstation, create an archive of the desired committed revision and transfer it
|
||||
to `.deploy/source.tar.gz`. On Unraid, preserve `.env` and the named volumes, then run:
|
||||
|
||||
```bash
|
||||
cd /mnt/user/appdata/mobilityops
|
||||
tar -xzf .deploy/source.tar.gz
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api alembic current
|
||||
curl -fsS http://127.0.0.1:1236/health
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
```bash
|
||||
cd /mnt/user/appdata/mobilityops
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml ps
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml logs --tail=200
|
||||
docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml logs -f api web
|
||||
docker logs -f n8n
|
||||
```
|
||||
|
||||
## Safe rollback
|
||||
|
||||
Choose a known-good commit on the workstation, archive and transfer it as above, then on
|
||||
Unraid extract it over the identifiable MobilityOps source directory and run the same
|
||||
`up --build -d` command. Preserve `.env` and both named volumes; do not use `down -v`,
|
||||
remove volumes, prune Docker, or modify unrelated containers. Check the target commit's
|
||||
Alembic compatibility before rolling application code behind the current database schema.
|
||||
@@ -1,89 +0,0 @@
|
||||
# MobilityOps premium UI evidence summary
|
||||
|
||||
Date: 2026-08-02
|
||||
Branch: `design/mobilityops-premium-ui`
|
||||
Baseline revision: `dfabb41582e302f45a3de826f85f531bf23dfc8b`
|
||||
Final design implementation commit: `1f292e14bb6a2e8ded5dc675b1b3360307d8a9ae`
|
||||
Review URL: `http://192.168.10.150:1236`
|
||||
|
||||
## Outcome
|
||||
|
||||
The working PoC was transformed into the Control Rail operational interface without
|
||||
changing backend contracts or adding scope. All existing journeys remain functional;
|
||||
return registration gained an evidence-based review boundary before commit.
|
||||
|
||||
## Evidence index
|
||||
|
||||
- Baseline audit: `docs/design/current-ux-audit.md`
|
||||
- Three directions and decision: `docs/design/design-directions.md`
|
||||
- Design tokens and component rules: `docs/design/design-system.md`
|
||||
- Stitch resource IDs: `docs/design/stitch-manifest.md`
|
||||
- Implemented visual validation: `docs/design/implementation-validation.md`
|
||||
- Baseline captures: `artifacts/design-validation/current/`
|
||||
- Stitch captures: `artifacts/design-validation/stitch/`
|
||||
- Final responsive captures: `artifacts/design-validation/implementation/`
|
||||
|
||||
## Major implementation changes
|
||||
|
||||
- Responsive Control Rail shell with compact top bar, desktop rail, off-canvas menu and
|
||||
labelled mobile bottom navigation.
|
||||
- Live readiness band, filterable Attention queue, movement timeline, honest integration
|
||||
pulse and persisted activity on the operations dashboard.
|
||||
- Searchable fleet and booking registries; booking client pagination limits the DOM to 25
|
||||
operational rows; responsive tables retain field labels.
|
||||
- Capture → review → result return workflow with calculated consequence preview and no
|
||||
write request before confirmation.
|
||||
- Match/conflict duplicate comparison, evidence-first knowledge, system-health cards and
|
||||
expandable audit metadata.
|
||||
- Inline SVG product mark, Feather-like line icon set, CSS control-centre illustration,
|
||||
timeline/status motion and reduced-motion fallback; no image or motion dependency.
|
||||
|
||||
## Validation
|
||||
|
||||
| Gate | Result |
|
||||
|---|---|
|
||||
| Backend tests | 66 passed |
|
||||
| Backend lint | ruff passed |
|
||||
| Backend types | mypy: 0 issues in 44 files |
|
||||
| Frontend types/build | passed; 59 modules; 240.24 kB JS and 36.63 kB CSS before gzip |
|
||||
| Browser journeys | 19 passed locally |
|
||||
| Horizontal overflow | none at 390/768/1280/1440 px |
|
||||
| Accessibility | named landmarks, skip link, visible focus, text-plus-shape status, labelled mobile rows, reduced-motion support |
|
||||
| Deployed browser smoke | passed on all 10 authenticated routes plus login at desktop and mobile sizes |
|
||||
| Console/network | 0 browser warnings/errors; 7 authenticated API paths returned HTTP 200 |
|
||||
| Deployed global search | Ctrl+K plus `MO-024` navigation passed against the review URL |
|
||||
|
||||
All displayed operational counts remain derived from the existing persisted API data.
|
||||
Synthetic-data labelling is persistent on login and authenticated surfaces.
|
||||
|
||||
Deployed evidence is stored in `artifacts/design-validation/implementation/deployed/`.
|
||||
The review stack reports healthy PostgreSQL/API state, HTTP 200 from the web application,
|
||||
and healthy state from the server's existing n8n at port 5678. A synthetic return reached
|
||||
`succeeded` on attempt 1 through that shared n8n and its MobilityOps callback; the demo was
|
||||
then reset to its deterministic state.
|
||||
|
||||
## Performance observations
|
||||
|
||||
No runtime font, image or animation dependency was added. The application uses inline SVG
|
||||
and CSS visuals, and the production bundle remains appropriate for this internal PoC.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Global search resolves Control Rail sections and `MO-*`, `BK-*`, `DQ-*` public
|
||||
references. It intentionally does not offer customer lookup because the locked PoC has
|
||||
no customer detail route or cross-entity search API.
|
||||
- The repository retains a bundled n8n service for standalone local clean-checkout demos.
|
||||
The Unraid overlay keeps it behind the opt-in `bundled-n8n` profile; the live review
|
||||
deployment uses the server's existing shared n8n instead.
|
||||
- The MCP Hub state is correctly shown as not configured in the current PoC rather than
|
||||
simulated as healthy.
|
||||
- Live RAGcore and MCP Hub round trips remain subject to the existing environment limits
|
||||
documented in `PROJECT_STATE.md`; their degradation behavior is unchanged.
|
||||
|
||||
## Rollback
|
||||
|
||||
The accepted baseline remains reachable at commit
|
||||
`dfabb41582e302f45a3de826f85f531bf23dfc8b`. To roll back the review deployment without
|
||||
rewriting git history, archive that revision, extract it over the application source on
|
||||
Unraid while preserving `.env` and Docker volumes, and run
|
||||
`docker compose -p mobilityops up -d --build`. Verify `/health` and port 1236 afterwards.
|
||||
@@ -1,129 +0,0 @@
|
||||
# Fleet Ops — final acceptance evidence
|
||||
|
||||
## Accepted implementation
|
||||
|
||||
- Deployment source marker: current `master`; the application image was built from runtime commit `cb7edb0` and the following commits contain acceptance/evidence only.
|
||||
- Live demo: `http://192.168.10.150:1236` — public passwordless synthetic demo.
|
||||
- Deployment: Unraid `/mnt/user/appdata/mobilityops`, Compose project `mobilityops`.
|
||||
- Database migration: `b913a72e8c14 (head)`.
|
||||
- Product name: Fleet Ops; MobilityOps remains the technical repository/deployment ID.
|
||||
|
||||
## Clean acceptance — 2026-08-10
|
||||
|
||||
```text
|
||||
Backend, isolated PostgreSQL: 241 passed, zero warnings
|
||||
Playwright, fresh local build: 152 passed (5.4 min)
|
||||
Playwright, live production: 152 passed (4.7 min)
|
||||
Ruff: clean
|
||||
Mypy: clean across 58 source files
|
||||
Frontend lint/build: passed
|
||||
npm audit, full and prod: 0 vulnerabilities
|
||||
Alembic: b913a72e8c14 (head)
|
||||
```
|
||||
|
||||
The browser suite covers both roles, protected routes, booking/return/maintenance/user/privacy workflows, five data-quality resolution types, correlated audit, retryable orchestration, grounded knowledge in three languages, the complete guided demo, the recruiter highlights, keyboard behaviour and responsive layouts from 360 to 1440 px.
|
||||
|
||||
The provider matrix explicitly covers both valid title contracts: the deterministic provider uses Markdown frontmatter titles, while production RAGcore returns curated localized presentation titles. Both must include the language-specific source fragment. The focused live matrix passed 3/3, followed by the complete 152/152 green production run.
|
||||
|
||||
## Production hand-off state
|
||||
|
||||
The final reset completed immediately before the verified Hub call at `2026-08-10T19:16Z` and restored:
|
||||
|
||||
```text
|
||||
users 2 · customers 180 · vehicles 50 · bookings 254 · inspections 75
|
||||
maintenance 40 · data-quality issues 33 · workflow runs 20
|
||||
scenario_integrity.all_ready = true (5/5 scenarios)
|
||||
```
|
||||
|
||||
API readiness is `ready`, PostgreSQL is `up`, and API, web, database, scheduled backup, Prometheus and Grafana are running healthy/current. No traceback or critical error appears in post-deployment API/backup logs.
|
||||
|
||||
A pre-deployment custom-format backup was created and independently verified by SHA-256 plus `pg_restore --list`:
|
||||
|
||||
`/mnt/user/appdata/mobilityops/backups/postgres/mobilityops-20260810T185833Z.dump`
|
||||
|
||||
## External integration evidence
|
||||
|
||||
- **Existing central n8n:** configured, dispatch enabled and operational. All four expected workflows have recent healthy heartbeats. The reset state contains 19 successful runs, zero pending, zero unexpected failures and one explicitly synthetic failed-retry scenario.
|
||||
- **RAGcore:** reachable and ready for tenant `northstar-mobility-demo`, workspace `mobilityops`, collection `internal-procedures`. Exact identity lookups independently confirm 11/11 active published documents in each of NL/EN/FR; the latest n8n report separately records 33 synchronized documents and zero failures. The canonical parsed-artifact hash is not misrepresented as a raw-source hash.
|
||||
- **ITWorx MCP Hub:** registration is enabled, the Hub is reachable and Fleet Ops is **operational**. Three real `mobilityops.operations.summary` verification calls are audited under the Hub's tenant-bound client identity; the final short-lived bearer produced HTTP 200/JSON and HTTP 401 after immediate revocation. All exposed Fleet Ops tools remain read-only.
|
||||
|
||||
## Recruiter and visual acceptance
|
||||
|
||||
The public entry now offers a direct **90-second Highlights** route. It links three product actions to their engineering proof, while the **Engineering** workspace explains domain ownership, commit-before-orchestration, citation-bound AI, security and explicit scope.
|
||||
|
||||
Interactive live inspection confirmed:
|
||||
|
||||
- no horizontal overflow on Highlights or Engineering at desktop; automated coverage confirms 360/390 px mobile layouts;
|
||||
- clear hierarchy, consistent primary actions and readable proof cards;
|
||||
- compact remaining-attention action (count badge, two-line label, small directional icon) without the former stretched empty panel;
|
||||
- truthful live n8n/RAGcore/MCP labels after reset, including 11 verified RAG documents and the real Hub client/tool evidence;
|
||||
- no dead links, placeholder numbers or unexplained raw data-quality references;
|
||||
- complete synthetic-data disclosure on login and in the persistent shell.
|
||||
|
||||
Current evidence images are in `artifacts/evidence/screenshots/`:
|
||||
|
||||
- `1-login.png`
|
||||
- `2-highlights.png`
|
||||
- `3-engineering-story.png`
|
||||
- `4-dashboard.png`
|
||||
- `5-knowledge-evidence.png`
|
||||
- `6-highlights-mobile.png`
|
||||
|
||||
## Deliberate boundary
|
||||
|
||||
This is a completed, production-shaped public demo—not claimed customer adoption and not a general ERP. Accounting, payments, public reservations, CRM, inventory, HR, a second RAG stack, a separate MCP server and autonomous write agents remain intentionally excluded. A real personal-data rollout would additionally require the adopting organisation's identity provider, retention approvals, secrets lifecycle, alert ownership and disaster-recovery governance.
|
||||
|
||||
## Local M54 upgrade candidate — 2026-08-23 (not released)
|
||||
|
||||
This evidence supplements, but does not replace, the production hand-off above. The local
|
||||
working tree contains an intentionally uncommitted recruiter UX and resilience upgrade;
|
||||
production, its database and the production screenshots were not changed.
|
||||
|
||||
Implemented evidence includes the interactive five-step system flow, hardened booking/return/
|
||||
data-quality invariants, atomic cross-replica demo reset, exact n8n callback and dispatcher
|
||||
leases, citation-bound RAGcore answers, deterministic seed regeneration and abortable,
|
||||
race-safe frontend data loading. Keyboard, forced-colours, reduced-motion and responsive
|
||||
behaviour remain part of the established Fleet Ops design system.
|
||||
|
||||
Exact local validation commands and results:
|
||||
|
||||
```text
|
||||
docker compose -p mobilityops-m54-final -f compose.yaml -f compose.test.yaml run --build --rm api pytest
|
||||
331 passed in 94.19s
|
||||
focused DQ-03 edge/concurrency suite
|
||||
44 passed
|
||||
combined RAGcore, dispatcher and data-quality suite
|
||||
121 passed
|
||||
docker compose -p mobilityops-lint -f compose.yaml -f compose.test.yaml run --build --rm --no-deps api ruff check app tests
|
||||
All checks passed
|
||||
docker compose -p mobilityops-lint -f compose.yaml -f compose.test.yaml run --build --rm --no-deps api mypy app
|
||||
Success: no issues found in 62 source files
|
||||
cd frontend && npm run lint
|
||||
passed
|
||||
cd frontend && npm run build && npm run budget
|
||||
passed
|
||||
python scripts/check-source-budgets.py
|
||||
passed; data-quality service 799/900 lines; main CSS 76342/78000 bytes;
|
||||
architecture CSS 8057/9000 bytes; data-quality CSS 6525/8000 bytes
|
||||
cd frontend && npx playwright test
|
||||
171 passed (6.9m) in one uninterrupted run against the rebuilt release container
|
||||
global-search shortcut regression, repeated against the release container
|
||||
10 passed in 10 consecutive repetitions
|
||||
docker compose -p mobilityops-contracts -f compose.yaml -f compose.test.yaml run --build --rm --no-deps --volume "C:\Projects\MobilityOps:/repo" --workdir /repo/backend api python ../scripts/check-contracts.py
|
||||
OpenAPI, event, MCP and n8n contracts are synchronized
|
||||
cd frontend && npm audit --omit=dev
|
||||
0 vulnerabilities
|
||||
```
|
||||
|
||||
The final local reset restored `2 users / 180 customers / 50 vehicles / 254 bookings / 75
|
||||
inspections / 40 maintenance records / 33 data-quality issues / 20 workflow runs`, with all
|
||||
five synthetic scenarios ready. API readiness is `ready`, PostgreSQL is `up`, the rebuilt
|
||||
API/web stack remains available, and the final 20-minute log scan contains no traceback,
|
||||
critical, unhandled, panic, fatal or emergency hit. `git diff --check` is clean.
|
||||
|
||||
In-app browser inspection covered the login, Dashboard and Engineering surfaces at desktop
|
||||
and 390×844. It confirmed zero horizontal overflow, no alert or console errors, the intended
|
||||
one-column mobile flow, readable mobile cards and consistent selected-step feedback. The local
|
||||
Compose candidate is healthy at `http://localhost:1228`. A release commit/tag, production
|
||||
backup and deployment remain deferred until explicit authorization; production and its data
|
||||
were not touched by M54.
|
||||
@@ -1,161 +0,0 @@
|
||||
# Fleet Ops final integrations — evidence summary
|
||||
|
||||
Session date: 2026-08-05. Branch `feat/fleet-ops-final-integrations`.
|
||||
|
||||
## Repository state
|
||||
|
||||
| Repo | Start | End | Branch | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Fleet Ops (MobilityOps) | `3ebca9e` (from `feat/live-n8n-ragcore-integration`) | `727c19a` (+ e2e test fixes, uncommitted at write time) | `feat/fleet-ops-final-integrations`, pushed to `origin` | 3 commits: `34df66d`, `2ae2044`, `727c19a` |
|
||||
| RAGcore | `64a908a` | `64a908a` (+1 isolated commit `ce0ad56`) | `main` | Only a backlog handoff entry committed; no code changes (36-file concurrent-session collision — see below) |
|
||||
| ITWorx MCP Hub | not modified this session | — | `feature/wp240-final-acceptance` | Connector already live in production before this session started; not touched |
|
||||
|
||||
## Deployed revisions
|
||||
|
||||
- Fleet Ops: `http://192.168.10.150:1236`, redeployed twice this session (after Batches
|
||||
1-3 and after Batch 4), `docker compose -p mobilityops -f compose.yaml -f
|
||||
compose.unraid.yaml up --build -d db api web`, `.deploy/source-revision` = `727c19a...`.
|
||||
- RAGcore: `http://192.168.10.150:1237`, `ragcore-app-1`. No image redeploy — the two live
|
||||
fixes (filesystem permissions, reranker model pull) were applied directly to the
|
||||
running container/Ollama instance, not via a code deploy.
|
||||
- ITWorx MCP Hub: `http://192.168.10.150:1100` (Tower), unchanged, already live before
|
||||
this session at commit `c4a0f6d` per the Hub's own state.
|
||||
|
||||
## GUI polish (Batch 1)
|
||||
|
||||
- Dashboard Attention Queue: curated severity mix (grouped "Handle now / Follow up
|
||||
today / Review later"), replacing pure severity-sort that let `high` crowd out
|
||||
everything else.
|
||||
- Today's Movements: seed data curated (`seed/bookings.csv`) so a fresh reset shows ≥2
|
||||
departures and ≥2 returns; new `test_seed_today_movements_are_a_credible_mix` test.
|
||||
Live-verified after a real demo reset: 2 returns + 2 departures shown.
|
||||
- About Demo: restructured into a compact grid with `<details>` progressive disclosure
|
||||
for architecture/security/testing sections.
|
||||
- Duplicate Customer Merge: match/conflict counts shown, matching fields hidden by
|
||||
default (toggle to reveal), compact preview of the merged record before confirmation.
|
||||
- Repo hygiene: removed a stray empty `backend;C` dir and an untracked 31MB zip export;
|
||||
`.gitignore` now excludes future archive exports.
|
||||
- All four live-verified via browser against the deployed instance (see screenshots
|
||||
taken during the session — not separately saved to disk).
|
||||
|
||||
## n8n (Batch 2)
|
||||
|
||||
- 4 canonical workflows confirmed live: Vehicle Return Orchestration, Scheduled Data
|
||||
Quality Scan, RAGcore Procedure Sync, Workflow Error Handler.
|
||||
- Fixed genuinely invalid JSON in the committed `fleet-ops-vehicle-return.json` (a
|
||||
missing `},` between two node objects — the file could not be parsed).
|
||||
- Workflow 3 (RAGcore Procedure Sync): confirmed 6 real nodes built and saved. Found and
|
||||
fixed two real defects via the safe `n8n import:workflow` CLI path (not the REST API,
|
||||
which caused a documented wipe incident in an earlier session): three body-parameter
|
||||
expressions had a stray trailing `}}`, and `settings.errorWorkflow` was unset. Exported
|
||||
the corrected definition to `n8n/workflows/fleet-ops-ragcore-procedure-sync.json`,
|
||||
added to `MANIFEST.md` and `check_drift.py`.
|
||||
- **Not published** — the Schedule Trigger runs daily at midnight; activating it starts
|
||||
real unattended production runs, deliberately left as a separate go-live decision.
|
||||
- No no-op/sync/error-handler live-execution smoke test was run this session beyond the
|
||||
structural CLI-export verification above (workflow remains unpublished).
|
||||
|
||||
## RAGcore (Batch 3)
|
||||
|
||||
- **Root cause found and fixed, live, user-approved**: the "zero retrieval candidates"
|
||||
bug was a filesystem permission bug (`/workspace/.state/models/embedding_profiles.json`
|
||||
was `root:root` mode `600` on the host bind mount, unreadable by the app's actual
|
||||
runtime uid 10001) — not authorization, not Qdrant, not embeddings, all independently
|
||||
verified healthy first. Fixed via `chown`/`chmod`; re-verified in-process (5 real hits,
|
||||
up from 0).
|
||||
- **Second, deeper gap found, not fixed**: the reranker adapter calls
|
||||
`{ollama}/api/rerank`, a route this Ollama version (`0.32.5`) does not serve (404).
|
||||
Pulled a working model (`xitao/bge-reranker-v2-m3:latest`, 1.2GB, approved) — did not
|
||||
fix it, since the problem is the HTTP route, not the model. `/v1/answers` still returns
|
||||
`not_answerable`/0 citations for real questions against real matching content.
|
||||
- User decision: leave `KNOWLEDGE_PROVIDER=demo`; hand the reranker fix off to RAGcore's
|
||||
own backlog (`docs/ai/BACKLOG.yaml`, task `M8-01`, committed in that repo as `ce0ad56`
|
||||
— the only commit made in RAGcore this session) rather than editing RAGcore code amid
|
||||
its own 36-file concurrent-session collision.
|
||||
- Side effect: minting the live-verification credential rotated the existing "Fleet Ops
|
||||
Knowledge Assistant (production)" service account's credential (2-active-credential cap
|
||||
reached). A fresh credential must be issued before actually flipping the provider live.
|
||||
|
||||
## MCP Hub (Batch 4)
|
||||
|
||||
- Confirmed the Fleet Ops connector is already live in production on the Hub side
|
||||
(Tower, commit `c4a0f6d`), with a real contract fix already applied there
|
||||
(`vehicle.get`'s wire parameter normalized to camelCase `vehicleRef`).
|
||||
- Fixed two concrete gaps in Fleet Ops's own `search-knowledge` endpoint: no `locale`
|
||||
field existed at all (now `nl-BE`/`en-GB`/`fr-BE`, wired to the knowledge provider's
|
||||
existing `language` param), and the correlation ID was always freshly minted, ignoring
|
||||
any inbound `X-Correlation-Id` header. Added `get_correlation_id`, applied to all four
|
||||
MCP endpoints.
|
||||
- `MCP_HUB_BASE_URL` was dead config (declared, never read); wired it for a real,
|
||||
bounded Hub-reachability health check instead of an unneeded self-registration push
|
||||
(the Hub's own registration is catalog-driven).
|
||||
- Renamed Fleet Ops's own internal audit tool labels `mobilityops_*` → `fleet_ops_*`
|
||||
(mirrored in `contracts/mcp-tools.json`, `mobilityops_*` kept as deprecated aliases).
|
||||
The live Hub connector's own dotted tool namespace (`mobilityops.operations.summary`
|
||||
etc.) is a separate, Hub-owned naming layer, deliberately not touched.
|
||||
- Automation page's MCP card now shows real evidence (last tool/client/count/timestamp)
|
||||
instead of only the registration-enabled boolean.
|
||||
|
||||
## AI Operations Brief (Batch 5)
|
||||
|
||||
Real MCP-client-shaped run via the live ITWorx MCP Hub connector's own
|
||||
`MobilityOpsClient` class against production Fleet Ops. Full runbook and live output in
|
||||
`docs/final-integrations/ai-operations-brief-runbook.md`. Summary:
|
||||
|
||||
- Real operations summary (21 available / 11 rented / 6 cleaning / 5 maintenance /
|
||||
7 blocked; 23 open quality issues).
|
||||
- Real most-pressing vehicle identified (`MO-031`, missing operational inspection).
|
||||
- Real vehicle detail lookup.
|
||||
- Real grounded knowledge answer (English damage-handling question): 2 real citations,
|
||||
`evidence_state: grounded`.
|
||||
- Dutch/French variants of the same question honestly returned `insufficient` (no
|
||||
fabrication) — root cause: the live Hub connector doesn't yet send the new `locale`
|
||||
field, a Hub-side follow-up, not silently worked around.
|
||||
- Correlation IDs verified end-to-end in Fleet Ops's own audit log
|
||||
(`GET /api/v1/audit?action=mcp_tool_request`), matching the response payloads exactly.
|
||||
- No write actions performed at any point.
|
||||
|
||||
## Testing per batch
|
||||
|
||||
- Backend: **176 passed**, `ruff check .` clean, `mypy app` clean (50 source files) —
|
||||
verified against a freshly rebuilt image after discovering mid-session that
|
||||
`docker compose run --rm api` (no bind mount on the `api` service) silently tests a
|
||||
stale image otherwise. One genuinely stale test assertion found and fixed as a result.
|
||||
- Frontend: `tsc -b && vite build` clean.
|
||||
- E2e (Playwright, against the live deployed instance,
|
||||
`MOBILITYOPS_PUBLIC_URL=http://192.168.10.150:1236`): every spec file run this
|
||||
session passed — `demo.spec.ts`, `interactive-elements.spec.ts` (26),
|
||||
`responsive-i18n.spec.ts` + `demo-accessibility.spec.ts` + `guided-demo-full.spec.ts`
|
||||
(28), `i18n-coverage.spec.ts` + `error-messages.spec.ts` + `clickable-rows.spec.ts` +
|
||||
`demo-guide.spec.ts` + `demo-entry.spec.ts` + `demo-legibility.spec.ts` +
|
||||
`fleet-ops-correction.spec.ts` + `ui-redesign.spec.ts` + `greeting.spec.ts` +
|
||||
`greeting-live.spec.ts` (28, after fixing 2 pre-existing fragile locators unrelated to
|
||||
this session's feature work — a `.data-table` ambiguity now that Automation has two
|
||||
tables, and a `Technische details` toggle ambiguity for the same reason; plus one
|
||||
pre-existing untranslated-loanword false positive in `i18n-coverage.spec.ts`).
|
||||
|
||||
## Known limitations, stated plainly
|
||||
|
||||
- `KNOWLEDGE_PROVIDER` is still `demo`, not `ragcore` — blocked on RAGcore's own
|
||||
reranker gap (handed off, not fixed this session).
|
||||
- n8n workflow 3 is built and correct but not published (deliberate, separate decision).
|
||||
- The live MCP Hub connector doesn't yet send the new `locale` field, so
|
||||
locale-aware knowledge search only works when called directly against Fleet Ops (as
|
||||
proven by the backend tests), not yet through the live Hub connector as deployed.
|
||||
- No public-demo-readiness checklist, About Demo Guide "completed" end-state polish
|
||||
(section 4E), or dashboard MCP "activity showcase after Demo Complete" gating were
|
||||
built this session — the MCP evidence display exists on the Automation page
|
||||
unconditionally rather than gated behind guided-demo completion.
|
||||
- No security-review pass was run separately this session (existing gates: ruff, mypy,
|
||||
the repo's own auth/audit test coverage).
|
||||
|
||||
## Rollback
|
||||
|
||||
- Fleet Ops: prior working revision `0571a40` remains in `.deploy/` as
|
||||
`source-0571a40.tar.gz` on the Unraid host; redeploy by re-extracting and re-running
|
||||
the same `docker compose up --build -d` sequence with that archive.
|
||||
- RAGcore: `chown`/`chmod` change is trivially reversible (`chown 0:0` +
|
||||
`chmod 600` on the same path) if needed, though there is no reason to revert a
|
||||
permission fix. Ollama model pull (`xitao/bge-reranker-v2-m3:latest`) can be removed
|
||||
with `ollama rm` if unwanted; it is inert until RAGcore's own code is changed to use it.
|
||||
- MCP Hub: not modified this session.
|
||||
@@ -1,284 +0,0 @@
|
||||
# Fleet Ops correction and release — final evidence
|
||||
|
||||
**Result: PASS**
|
||||
|
||||
## Commits
|
||||
|
||||
- Source branch / commit (verified pre-correction baseline): `master` @ `18344bc8b7a75a2f868bf15bf498fc030ac6c34c`
|
||||
- Fix branch: `fix/fleet-ops-i18n-status-flow`
|
||||
- Final fix-branch commit: `284b3c7` (merged content identical to `2e4fb43`, which carries the evidence-summary localization fix)
|
||||
- Main-before-merge: `18344bc8b7a75a2f868bf15bf498fc030ac6c34c` (confirmed unchanged via `git fetch` + `git rev-parse origin/master` immediately before merging — no unexpected commits landed on master while this branch was in progress)
|
||||
- Merge commit: `de0bdea84fea01b4501deb7099107bc753c2e6d7` (`git merge --no-ff fix/fleet-ops-i18n-status-flow -m "merge: complete Fleet Ops localization and status resolution"`, zero conflicts)
|
||||
- Final main commit: `de0bdea84fea01b4501deb7099107bc753c2e6d7`
|
||||
- Deployed commit: `de0bdea84fea01b4501deb7099107bc753c2e6d7` (`.deploy/source-revision` on Unraid)
|
||||
- Gitea main branch: `master` (confirmed via `git fetch origin && git rev-parse origin/master` matching local `master` after push)
|
||||
- Live URL: `http://192.168.10.150:1236`
|
||||
|
||||
Fix-branch commit history: `6deb955`, `e6539d1`, `ac4b163`, `1fdd2b3`, `1e40775`, `a7ac5ed`, `7851e80`, `cda2c32`, `2e4fb43`, `284b3c7`.
|
||||
|
||||
## What this correction fixed
|
||||
|
||||
1. **Status-recommendation flow redesigned** (sections 8A–8F). The old single opaque
|
||||
"calculate and apply recommended status" action is replaced by a single shared, pure
|
||||
evaluator (`backend/app/services/vehicle_status.py::evaluate_vehicle_status`,
|
||||
documented in `docs/fleet-ops-correction/vehicle-status-decision-table.md`) used
|
||||
identically by the scanner, a non-mutating preview endpoint
|
||||
(`POST /api/v1/data-quality/issues/{ref}/status-recommendation`), and a
|
||||
transactional apply endpoint (`POST .../apply-recommended-status`) that locks the
|
||||
row, recomputes facts, rejects a stale `recommendation_token`, refuses unsafe/manual-
|
||||
review recommendations, and re-validates post-write before resolving the issue.
|
||||
- Forbidden shortcuts eliminated: "maintenance + active booking" no longer
|
||||
auto-recommends "rented" (being in maintenance is itself now a blocking fact);
|
||||
"maintenance with nothing else wrong" no longer auto-clears to "available" (no
|
||||
fact proves maintenance is actually finished — release stays a manual decision).
|
||||
- Frontend: "Review recommendation" → a localized decision panel (current/
|
||||
recommended status, why, evidence, consequences) → an exact "Change status to
|
||||
<status>" confirm action → result, or a distinct "Manual review required"
|
||||
state offering no generic apply button.
|
||||
2. **MO-016 order independence** (section 9). Order independence does not mean "same
|
||||
final status regardless of order" — resolving the booking overlap first genuinely
|
||||
removes the conflict, correctly leaving nothing to apply. What holds either way: the
|
||||
recommendation always reflects real current facts (never a stale proxy), and nothing
|
||||
unsafe is ever applied (never "rented"). Proven by a backend test explicitly scoped
|
||||
to MO-016/DQ-DEMO-STATUS (the original version wasn't — `_first_open()` returned
|
||||
whichever of ~14 open `vehicle_status_conflict` issues was most recent, not
|
||||
necessarily MO-016's) and a browser-level Playwright test covering both orders.
|
||||
3. **"Fleet Ops" is a non-localizable brand constant** (`frontend/src/product.ts`,
|
||||
backend `PRODUCT_NAME`), wired via `{{productName}}` interpolation everywhere the
|
||||
brand appeared in locale prose. A permanent test fails the build if any locale file
|
||||
ever defines the brand name or an `appName` key again.
|
||||
4. **Dynamic backend prose converted to message codes + params** (sections 5/6/10):
|
||||
return status reasons, audit field/actor-type labels, automation `last_error` (new
|
||||
`last_error_code` column, migration `799d8800e241`), search results (sections/
|
||||
vehicles/bookings/issues), and — found live on Unraid — the data-quality evidence
|
||||
summary. Raw technical text is demoted to a "Technical details" disclosure
|
||||
everywhere.
|
||||
5. **Knowledge-base fixes**: the demo provider's tokenizer silently dropped accented
|
||||
characters (`[a-z0-9]+` split "véhicule" into "v"+"hicule"), breaking French
|
||||
retrieval broadly — fixed to include the Latin-1 accented range. Reweighted section
|
||||
scoring so a body match (real substance) outranks a heading/title match (a shallow
|
||||
structural hint) — the old weighting misranked the damage procedure behind an
|
||||
unrelated document for the brief's exact validation question in all 3 languages.
|
||||
Removed leftover "MobilityOps"/"PoC" mentions from 9 procedure documents.
|
||||
6. **Search, audit, automation, maintenance/inspections localized** (section 10):
|
||||
backend returns stable codes + params only; the frontend localizes section labels,
|
||||
vehicle summaries, booking/issue statuses, audit action/field/actor labels,
|
||||
automation error explanations, and maintenance/inspection type labels.
|
||||
7. **i18n test suite strengthened** (section 11): key parity, brand invariant,
|
||||
translation-quality (cross-locale identical-value detection), a hardcoded-JSX-text
|
||||
static scan (had to anchor on backreferenced closing-tag names — a naive `>text<`
|
||||
regex misread TypeScript generics as JSX), and a 3-language route matrix (every main
|
||||
route, no console errors, correct `html[lang]`, real page headings).
|
||||
|
||||
## Live-caught bug (the deployment validation earning its keep)
|
||||
|
||||
Live validation on the freshly-deployed fix branch directly caught a real defect: every
|
||||
data-quality issue's top-of-page evidence summary was unconditionally showing raw,
|
||||
always-English text (e.g. *"vehicle marked available while reserved bookings
|
||||
conflict"*) in **all three languages**, because the frontend never finished the
|
||||
`evidence.signals` localization the backend had already been emitting (the backend code
|
||||
even had a comment describing the intended design that the frontend didn't implement).
|
||||
Fixed in commit `2e4fb43`:
|
||||
- `DataQualityIssueDetail.tsx` now renders `evidence.signals` through the operator's
|
||||
locale as the primary evidence text.
|
||||
- The four `DQ-DEMO-*` seed rows that anchor the guided demo's scripted scenarios now
|
||||
carry real, accurate signals computed at seed time (the duplicate-customer similarity
|
||||
score is the actual `SequenceMatcher` ratio on the seeded names, not invented).
|
||||
- Rows with no structured signals fall back to raw text rather than showing a blank
|
||||
summary; the one known filler placeholder gets its own localized rendering.
|
||||
- A regression test locks this in: the vehicle-status-conflict evidence summary must
|
||||
show localized text and must never contain the specific raw English sentence that was
|
||||
live-visible before the fix, in all 3 languages.
|
||||
|
||||
Also found and fixed along the way: a frontend logic bug conflating "no conflict" with
|
||||
"manual review required" (both carry `safe_to_apply: false`), which showed a false
|
||||
"manual review required" panel for MO-016 after its booking overlap was resolved
|
||||
instead of the correct "no change needed" state (fixed in `1fdd2b3`).
|
||||
|
||||
## Translation coverage
|
||||
|
||||
- All three locale files (`nl-BE`, `en-GB`, `fr-BE`) define exactly the same key set
|
||||
for every namespace (`i18n-coverage.spec.ts`, structural guarantee).
|
||||
- No locale file contains an empty string value.
|
||||
- No locale file defines the brand name or an `appName` key (brand-invariant test).
|
||||
- Cross-locale translation-quality check: for every string ≥8 characters of real prose,
|
||||
nl-BE ≠ en-GB, fr-BE ≠ en-GB, fr-BE ≠ nl-BE, with a precise, audited allowlist for
|
||||
genuine proper nouns/cognates (23 entries, each with a documented reason).
|
||||
- Hardcoded-JSX-text static scan: zero findings against the current codebase (verified
|
||||
against both false positives — TypeScript generics — and a deliberately-injected-
|
||||
then-reverted false negative).
|
||||
- 3-language route matrix: every main route (dashboard, vehicles, vehicle detail,
|
||||
bookings, booking detail, data quality, issue detail, automation, knowledge, audit,
|
||||
scenarios, about) opens cleanly in all 3 languages with no console errors, correct
|
||||
`html[lang]`, and a real page heading.
|
||||
- **Remaining visible wrong-language text**: none found. The one gap that existed (the
|
||||
data-quality evidence summary) was found live and fixed before merge.
|
||||
|
||||
## Branding
|
||||
|
||||
- Visible product name: **Fleet Ops**, exactly, in all 3 languages, everywhere (login,
|
||||
topbar, footer "Fleet Ops Demo", document title, About page, Demo Guide, knowledge
|
||||
base). Verified structurally (brand-invariant test) and live (branding test across
|
||||
dashboard/vehicles/data-quality/audit/automation/knowledge pages in all 3 languages;
|
||||
visual screenshots of the login screen in nl-BE and fr-BE).
|
||||
- Technical identifier retained (by design, per the brief): repository name, local
|
||||
directory, package/module names, Compose project, deployment directory, database
|
||||
name, and the `/health` endpoint's `service: "mobilityops-api"` field remain
|
||||
"mobilityops" — none of these are visible UI text.
|
||||
- No visible "MobilityOps" or "PoC" anywhere in the UI or the demo knowledge base
|
||||
(9 procedure documents cleaned up; regression test in `test_knowledge.py` scans every
|
||||
procedure file for both strings).
|
||||
|
||||
## Status-preview / apply / manual-review / MO-016 ordering
|
||||
|
||||
- **Preview**: verified non-mutating — the issue's `status` stays `"open"` after
|
||||
calling the preview endpoint and re-fetching it via a fresh request.
|
||||
- **Apply**: the confirm button names the exact target status ("Change status to
|
||||
Blocked" / "Status wijzigen naar Geblokkeerd" / "Changer le statut vers Bloqué");
|
||||
applying resolves the issue and updates the vehicle atomically.
|
||||
- **Manual review**: MO-024 (active rental + service-threshold reached, a genuine fact
|
||||
contradiction) shows "Manual review required" with no generic apply button rendered
|
||||
at all.
|
||||
- **Stale token**: simulated by resolving the underlying booking overlap after the
|
||||
preview was fetched but before applying — the apply call is correctly rejected
|
||||
(`RECOMMENDATION_STALE`), the UI shows the "situation has changed" message, and the
|
||||
user must review again before a new apply is possible.
|
||||
- **MO-016 ordering**: both orders tested. Resolving the overlap first correctly leaves
|
||||
nothing to apply (vehicle stays "available", genuinely correct). Resolving the status
|
||||
conflict first safely blocks the vehicle; resolving the now-redundant overlap
|
||||
afterwards does not disturb it. Neither order ever produces "rented".
|
||||
|
||||
## Knowledge (per language)
|
||||
|
||||
The brief's exact validation question, in each language, grounds on the damage
|
||||
procedure as the **primary** (not just top-3) source:
|
||||
- nl-BE: *"Wat moet ik doen wanneer een voertuig beschadigd terugkomt?"* → damage
|
||||
procedure, Dutch source, Dutch excerpt.
|
||||
- en-GB: *"What should I do when a vehicle returns with damage?"* → damage procedure,
|
||||
English source, English excerpt.
|
||||
- fr-BE: *"Que dois-je faire lorsqu'un véhicule revient endommagé ?"* → damage
|
||||
procedure, French source, French excerpt.
|
||||
|
||||
This required two real fixes: a tokenizer bug that silently dropped accented
|
||||
characters (breaking French retrieval broadly) and a scoring-weight rebalance (body
|
||||
matches now outrank heading/title matches).
|
||||
|
||||
## Audit / automation
|
||||
|
||||
- Audit: action labels localized (`workflow_retry` → "automatisering opnieuw
|
||||
geprobeerd" / "automation retried" / "automatisation relancée", etc.), field names
|
||||
localized (`operational_status` → "Operationele status" / "Operational status" /
|
||||
"Statut opérationnel"), actor types localized, raw technical codes only inside
|
||||
"Technical details". Verified live and via a dedicated Playwright test.
|
||||
- Automation: the seeded synthetic failure shows a localized primary explanation
|
||||
("De workflowdienst was tijdelijk niet bereikbaar…") with the raw technical message
|
||||
("Synthetic connection timeout to n8n") only under "Technical details". Verified live
|
||||
and via a dedicated Playwright test.
|
||||
|
||||
## Backend tests / lint / types
|
||||
|
||||
- `pytest`: **151 passed**, 0 failed (clean checkout, local dev, and post-merge master
|
||||
— run four times across this correction, always 151/151).
|
||||
- `ruff check .`: all checks passed, every run.
|
||||
- `mypy app` (strict): no issues found in 49 source files, every run.
|
||||
- Alembic: `alembic upgrade head` from empty database lands on `799d8800e241`
|
||||
(the new `outbox_events.last_error_code` column); `downgrade -1` / `upgrade head`
|
||||
round-trip verified.
|
||||
|
||||
## Frontend build / Playwright
|
||||
|
||||
- `npm ci`, `tsc -b`, `vite build`: clean, every run.
|
||||
- Full Playwright suite: **116 tests**, run repeatedly against the local dev stack, an
|
||||
isolated clean-checkout stack, the live fix-branch deployment, and the live
|
||||
post-merge master deployment — **116/116 passed** on the final master-deployment run
|
||||
and on the final local run. A handful of transient, sequential-run-only flakes
|
||||
occurred at various points across ~10 full-suite runs today (different test each
|
||||
time, e.g. a pre-existing logout-timing race in `AuthContext.logout()` unrelated to
|
||||
this branch); every single one was confirmed to pass cleanly in isolation.
|
||||
- Guided demo covered indirectly via `guided-demo-full.spec.ts`,
|
||||
`demo-guide.spec.ts`, and the route matrix across all 3 languages — no dedicated
|
||||
"run the guided tour end-to-end in French" script exists beyond what those specs plus
|
||||
the branding/route-matrix tests already exercise, since the guided tour's steps route
|
||||
through the same pages already covered per-language.
|
||||
|
||||
## Clean-checkout drill
|
||||
|
||||
Fresh `git clone --branch fix/fleet-ops-i18n-status-flow` of only committed files into
|
||||
an isolated Compose project (`cleancheckfleetops`, ports 8129/1229/5679 to avoid
|
||||
colliding with the working dev stack). From empty volumes: build → up → `alembic
|
||||
upgrade head` → `reset_and_seed` (50 vehicles / 180 customers / 246 bookings / 27
|
||||
data-quality issues / 20 workflow runs) → 151 backend tests + Ruff + mypy green →
|
||||
frontend build green → full Playwright suite green → final reset →
|
||||
`scenario_integrity.all_ready: true`. Isolated stack, containers, volumes, and images
|
||||
torn down afterward; working dev environment confirmed untouched.
|
||||
|
||||
## Unraid deployment
|
||||
|
||||
Deployed via `git archive` → `scp` → extract into `/mnt/user/appdata/mobilityops`
|
||||
(preserving `.env` and persistent volumes) → `.deploy/source-revision` → rebuild
|
||||
`api`+`web` → `alembic upgrade head` → reset/reseed. Done twice: once for the fix
|
||||
branch (caught the evidence-summary bug), once for the final merged master. Both times:
|
||||
containers healthy, no errors in `api`/`web` container logs, full Playwright suite
|
||||
green against the live server, `scenario_integrity.all_ready: true` after final reset.
|
||||
RAGcore and MCP Hub were not activated (the demo `KnowledgeProvider` — deterministic
|
||||
local retrieval — remains what's live, per the brief's constraint against activating
|
||||
unvalidated live integrations).
|
||||
|
||||
## Responsive / accessibility
|
||||
|
||||
- Breakpoint matrix (1440×1000, 1280×800, 1024×768, 768×1024, 430×932, 390×844,
|
||||
360×800) × 3 languages: no horizontal overflow, localized headings visible
|
||||
(`responsive-i18n.spec.ts`).
|
||||
- Status-recommendation panel: keyboard-only activation of "Review recommendation" and
|
||||
"Change status to X" verified via focus assertions (not just click); reduced-motion
|
||||
emulated during the flow; status never conveyed by colour alone (the badge always
|
||||
carries its own localized text); `aria-live="polite"` added so the applied
|
||||
confirmation is announced to screen readers.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- A pre-existing, narrow timing race in `AuthContext.logout()` (clears local state and
|
||||
redirects before awaiting the server-side cookie-clearing POST) occasionally flakes
|
||||
one specific Playwright test only under heavy sequential load; not introduced by this
|
||||
branch, not fixed (out of this branch's scope), always passes in isolation.
|
||||
- The 11 generic `DQ-0xxx` filler seed rows (not tied to a named demo scenario) show a
|
||||
localized generic placeholder rather than rich structured evidence, since they carry
|
||||
no real underlying data gap to describe accurately (the CSV's placeholder text
|
||||
doesn't correspond to an actually-missing field on the referenced vehicles).
|
||||
- No dedicated "full guided demo in French, screenshot every step" script exists as a
|
||||
single artifact; coverage is composed from the route matrix, branding, and existing
|
||||
guided-demo specs, each run across all 3 languages.
|
||||
|
||||
## Screenshots
|
||||
|
||||
`artifacts/fleet-ops-correction/screenshots/`, all captured live against
|
||||
`http://192.168.10.150:1236`:
|
||||
|
||||
- `login-nl-BE.jpg` — login screen, Dutch (default), "Fleet Ops" brand + "Bedieningscentrum" subtitle.
|
||||
- `login-fr-BE.jpg` — login screen switched to French, "Fleet Ops" brand + "Centre de contrôle" subtitle, "Organisation de démo : Northstar Mobility (fictive)".
|
||||
- `dq-demo-status-fr-BE-collapsed.jpg` — DQ-DEMO-STATUS in French: the localized evidence summary ("Ce véhicule a deux réservations qui se chevauchent…") replacing the raw English sentence, in its collapsed pre-review state.
|
||||
- `dq-demo-status-fr-BE-clean-reload.jpg` — the same page after a clean reload, confirming the fix is stable across navigation.
|
||||
|
||||
One capture attempt mid-session showed the brand rendered as "Vlootoperaties" instead
|
||||
of "Fleet Ops" — investigated immediately via `document.documentElement` inspection and
|
||||
confirmed to be **Chrome's own built-in page-translate feature** auto-triggering on the
|
||||
automation browser profile (`class="translated-ltr"`, `lang` rewritten to bare `"nl"`
|
||||
by Google Translate, not the app), re-triggering specifically on React DOM mutations
|
||||
from clicking through the panel. Not an application defect: a clean reload immediately
|
||||
after showed the correct "Fleet Ops" brand and correctly localized French content
|
||||
again, and none of the 116 Playwright tests (which run in a clean automated browser
|
||||
context without this extension behaviour) ever observed it.
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
1. `ssh unraid`, `cd /mnt/user/appdata/mobilityops`.
|
||||
2. `git archive --format=tar 18344bc -o` (from a local clone) → `scp` → extract, or
|
||||
restore from the previous `.deploy/source-revision` (`18344bc8b7a75a2f868bf15bf498fc030ac6c34c`).
|
||||
3. `echo 18344bc8b7a75a2f868bf15bf498fc030ac6c34c > .deploy/source-revision`.
|
||||
4. `docker compose -f compose.yaml -f compose.unraid.yaml build api web && ... up -d api web`.
|
||||
5. `alembic downgrade e7b08389f47f` if the `last_error_code` column must also be
|
||||
rolled back (not required for a same-schema rollback within this correction's own
|
||||
history, only if reverting past the whole correction).
|
||||
6. Re-seed and re-verify `scenario_integrity.all_ready: true`.
|
||||
|
||||
The fix branch `fix/fleet-ops-i18n-status-flow` was not deleted.
|
||||
@@ -1,291 +0,0 @@
|
||||
# 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.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'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 head` → `799d8800e241 (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 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.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).
|
||||
@@ -1,155 +0,0 @@
|
||||
# Fleet Ops release — final-product-polish evidence
|
||||
|
||||
## Result: PASS
|
||||
|
||||
## Commits
|
||||
|
||||
- Original feature-branch baseline before this task: `257a4cf` (`docs(polish): audit finale demo-afwerking`)
|
||||
- Feature-branch commits added this task, on `feat/mobilityops-functional-completion`:
|
||||
- `337f871` — polish: rebrand to Fleet Ops, add trilingual i18n, adaptive demo guide, and UX overhaul
|
||||
- `845db14` — fix: mobile topbar overflow at 421-440px and add trilingual responsive coverage
|
||||
- Feature branch final commit: `845db14e172539b1d10e40f6a3249a72122deb41`
|
||||
- `master` before merge (verified against the previously recorded baseline): `e0c7ed60112510687627d20a957af91c8b9db7f8` — unchanged, no unexpected commits, no conflicts (confirmed via `git merge-tree` dry run before merging)
|
||||
- Merge commit on `master`: `18a765d62345ea9a6660d04fb868f218cf4d0b6e` (`merge: release Fleet Ops multilingual demo`, `--no-ff`)
|
||||
- Final `master` commit (pushed and deployed): `18a765d62345ea9a6660d04fb868f218cf4d0b6e`
|
||||
- Deployed commit on Unraid (`.deploy/source-revision`): `18a765d62345ea9a6660d04fb868f218cf4d0b6e`
|
||||
- Feature branch was **not** deleted, per instruction.
|
||||
|
||||
## URL
|
||||
|
||||
- Live review deployment: `http://192.168.10.150:1236`
|
||||
|
||||
## Visible branding
|
||||
|
||||
- Product name "Fleet Ops" (with a space) visible in: sidebar brand lockup, browser tab title, login screen, footer product line, About page heading ("What Fleet Ops is and isn't" / "Wat Fleet Ops wel en niet is" / "Ce que Fleet Ops est et n'est pas"), demo badge popover, dashboard copy, all 3 languages.
|
||||
- No visible "MobilityOps" or "PoC"/"proof of concept" wording remains in user-facing copy (verified by full-page inspection of all main routes in all 3 languages plus a targeted source grep for stray hardcoded strings). The repository, Docker image names, and internal git history retain "MobilityOps" (out of scope; not user-visible).
|
||||
- Retained technical identifiers (unchanged, as instructed): API paths (`/api/v1/...`), Docker Compose project name (`mobilityops`), internal vehicle/customer reference prefixes (`MO-`, `CUS-`), Gitea repository name.
|
||||
|
||||
## Supported locales
|
||||
|
||||
- `nl-BE` (default for a fresh session, unauthenticated visitor)
|
||||
- `en-GB`
|
||||
- `fr-BE`
|
||||
- Persisted via `localStorage` key `fleetops.language`; survives refresh, logout/login, and demo reset. No flags used — accessible `<select>` language picker (visible name/code) in the topbar (desktop/tablet) and inside the mobile navigation drawer (≤960px, to avoid topbar overflow). `document.documentElement.lang` kept in sync. All dates/numbers rendered via `Intl.DateTimeFormat`/`Intl.NumberFormat` (`Europe/Brussels` timezone).
|
||||
|
||||
## Translation coverage
|
||||
|
||||
- `frontend/e2e/i18n-coverage.spec.ts`: recursively compares every key path across all 3 locale files for all 14 namespaces (`common, auth, navigation, dashboard, fleet, bookings, returns, quality, knowledge, integrations, audit, demo, errors, accessibility`) and fails the build on any missing key or empty string value. **2/2 passed** in every gate run this task (local, clean-checkout, and live-deployment runs).
|
||||
- Command: `npx playwright test e2e/i18n-coverage.spec.ts --project=chromium`
|
||||
|
||||
## Knowledge-base locales
|
||||
|
||||
- `knowledge/procedures/{nl-BE,en-GB,fr-BE}/` — 11 procedure documents per language (same `document_id`s across languages so citations stay stable): vehicle checkout, vehicle return, damage handling, odometer anomalies, cleaning checklist, maintenance escalation, customer documents, privacy, booking conflicts, roles/escalation, and a new **vehicle availability** procedure (added this task to cover the "vehicle-available-again" guided-demo step explicitly).
|
||||
- `DemoKnowledgeProvider` now retrieves per-language (only searches the UI-selected language's corpus), with localized "no match"/"low confidence" boilerplate text per language; the frontend passes the active UI language on every `/api/v1/knowledge/questions` and `/api/v1/knowledge/status` call.
|
||||
- Verified live in all 3 languages this task (see Browser evidence below): NL/EN/FR suggested questions each return grounded, correctly-cited, same-language answers.
|
||||
- Backend unit tests: `test_demo_provider_grounds_damage_question_in_dutch`, `test_demo_provider_grounds_damage_question_in_french`, `test_demo_provider_health_reports_document_count_per_language`, `test_demo_provider_insufficient_evidence_message_is_localized` — all passing.
|
||||
|
||||
## Demo Guide — adaptive per breakpoint
|
||||
|
||||
- **Extra-wide desktop (≥1440px)**: docked rail (`.demo-guide-panel.is-wide`), fixed 420px minimum width, no drop shadow (reads as part of the layout), never auto-collapses. Verified: `demo-guide.spec.ts` → "wide desktop viewport docks the guide as a rail that never collapses to a chip".
|
||||
- **Standard desktop/tablet (701–1439px)**: floating non-modal panel that auto-collapses to a persistent, closable progress chip ("Demo-gids · stap X van Y") the instant "Ga naar deze stap" is used; chip has its own expand action and a separate close (×) control; reopens on one click; content reflow padding shrinks to 0 while collapsed so nothing is permanently blocked. Verified: 3 dedicated tests in `demo-guide.spec.ts`.
|
||||
- **Mobile (≤700px)**: bottom sheet with collapsed / half / full states, a drag-handle button that cycles states, no horizontal overflow, primary actions (Volgende/Ga naar deze stap) reachable in the half state. Verified: `demo-guide.spec.ts` → "mobile viewport shows a bottom sheet with collapsed/half/full states and no horizontal overflow", plus `demo-accessibility.spec.ts` → "demo guide is usable as a mobile bottom sheet".
|
||||
- **Cross-cutting (4D)**: "Ga naar deze stap" scrolls the on-page target into view, moves programmatic focus to it (`tabindex=-1` + `.focus()`), and applies a 2.2s outline pulse (`.demo-guide-highlight`, disabled under `prefers-reduced-motion`); Escape collapses the standard-tier panel first, then closes it on a second press; progress (`currentIndex`/`completed`) persists in `sessionStorage` across navigation and reload. Verified: `demo-guide.spec.ts` → "Escape collapses the standard-tier panel, then closes it" and "going to a step scrolls, focuses and highlights the on-page target".
|
||||
- Fixed along the way: two dangling `aria-labelledby` references (`SectionHeading` never actually set the referenced `id`) on Dashboard and Data Quality Issue Detail panels.
|
||||
|
||||
## Data Quality Workbench improvements
|
||||
|
||||
- Replaced plain radio rows with accessible `.choice-card` selectable tiles (title, consequence detail, `:has(input:checked)`/`.is-selected` state, visible focus ring, hover state) across the duplicate-customer survivor choice, odometer-regression decision, and booking-overlap block choice.
|
||||
- Clear action hierarchy: primary resolve/apply/merge action uses `.button-primary`; defer uses a de-emphasized `.button-tertiary`; reject uses `.button-tertiary-destructive` (muted, turns critical-red only on hover) — no longer visually competing with the recommended resolution.
|
||||
- Technical evidence (`evidence_json`) collapsed by default behind a localized "Technical details" `<details>` disclosure.
|
||||
- Contrast/opacity audited: no unintended overlays, disabled-looking text, or weak borders found beyond the (fixed) dangling-aria-labelledby issue.
|
||||
|
||||
## Terminology mapping
|
||||
|
||||
- Achieved via the i18next namespace architecture itself rather than a separate module: technical codes (rule types, statuses, action codes, integration states) resolve through dedicated JSON keys (`quality:ruleTypes.*`, `quality:list.status*`, `audit:actions.*`, `integrations:statusLabels.*`, `fleet:statuses.*`, `bookings:statuses.*`) with a human label in all 3 languages; raw technical values (correlation IDs, full UUIDs, raw evidence JSON) are confined to "Technical details" disclosures. Example mappings implemented: `possible_duplicate_customer` → "Possible duplicate customer"/"Mogelijke dubbele klant"/"Client peut-être en double"; `demo_login` → "Logged in"/"Ingelogd"/"Connecté"; n8n `degraded` → "Retry available"/"Opnieuw proberen mogelijk"/"Nouvelle tentative possible"; `not_configured`/`disabled` → "Not connected"/"Niet gekoppeld"/"Non connecté".
|
||||
|
||||
## Automation / audit improvements
|
||||
|
||||
- Automation ledger: succeeded events group and collapse when >3 in view ("Show N succeeded jobs"/"Hide individual jobs"), filter chips (needs-attention/recent/succeeded/all), meaningful short refs (`AUT-RET-####` derived from the aggregate ref, full UUID behind a `<details>`), localized event types and statuses.
|
||||
- Audit trail: events grouped by `correlation_id` into one card with a human action-label heading (`audit:actions.*`), related-event count and an expandable technical list; readable before/after diff (`ChangeDiff` component: humanized field names, `set to`/`was`/`X → Y` phrasing) instead of raw JSON by default; short reference (`AUD-XXXXXXXX`) with full UUID and correlation ID behind "Technical details".
|
||||
|
||||
## Attention Queue / clickable rows
|
||||
|
||||
- Full "stretched link" pattern applied to: Attention Queue, Today's movements, Vehicles table, Bookings table, Data Quality table. Entire row is one activation target (pointer cursor, hover state, keyboard-focusable, Enter/Space activates), secondary in-row links (e.g. the vehicle reference inside a booking row) remain independently clickable via `.cell-link { z-index: 2 }` layered above the row overlay.
|
||||
- Dedicated tests in `frontend/e2e/clickable-rows.spec.ts` (8 tests): click on empty row space, keyboard focus + Enter, mobile-viewport click, secondary-link independence, correct routing for each of the 5 surfaces, pointer-cursor/focus-ring check.
|
||||
|
||||
## Test results (all commands re-run against this exact final state)
|
||||
|
||||
### Backend (local dev stack, clean-checkout instance, and live Unraid deployment — all three, all green)
|
||||
|
||||
```
|
||||
docker compose exec api pytest -q → 131 passed
|
||||
docker compose exec api ruff check . → All checks passed!
|
||||
docker compose exec api mypy app → Success: no issues found in 48 source files
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```
|
||||
cd frontend && npm run build → tsc -b && vite build: success
|
||||
```
|
||||
|
||||
### Playwright (92 tests; run against local dev stack, the isolated clean-checkout stack, and the live Unraid deployment — 92/92 passed in all three runs)
|
||||
|
||||
```
|
||||
npx playwright test --project=chromium
|
||||
```
|
||||
|
||||
Suites: `demo-accessibility`, `demo-entry`, `demo-guide` (including the 3 new adaptive-breakpoint tests, chip close-control test, Escape test, scroll/focus/highlight test), `demo-legibility`, `demo`, `guided-demo-full`, `i18n-coverage`, `interactive-elements`, `responsive-i18n` (7 breakpoints × 3 languages = 21 tests), `ui-redesign`, `clickable-rows` (new, 8 tests).
|
||||
|
||||
## Clean-checkout drill (evidence)
|
||||
|
||||
Performed in an isolated environment (separate Compose project `mobilityops-clean`, separate host ports 8129/1229, no shared volumes or n8n) so the user's existing long-running dev/n8n environment was never touched:
|
||||
|
||||
1. `git clone` of the local repository at commit `845db14` (feature branch, pre-merge) into a scratch directory.
|
||||
2. `cp .env.example .env` (project name and ports overridden for isolation only).
|
||||
3. `docker compose up --build -d db api web` — migrations ran automatically on API startup.
|
||||
4. `docker compose exec api python -m app.cli seed --reset` — deterministic seed loaded (users:2, customers:180, vehicles:50, bookings:246, inspections:75, maintenance:40, data_quality_issues:26, workflow_runs:20).
|
||||
5. `docker compose exec api pytest -q` → 131 passed. `ruff check .` → clean. `mypy app` → clean.
|
||||
6. `npm ci && npm run build` → clean build.
|
||||
7. `npx playwright test --project=chromium` (pointed at the isolated stack via `MOBILITYOPS_PUBLIC_URL`) → 92 passed.
|
||||
8. Live browser verification in English and French (Dutch already covered as the automated-suite default): guided-demo dashboard, knowledge-assistant grounded answers in both languages with correct same-language citations.
|
||||
9. `POST /api/v1/demo/reset` → `scenario_integrity: {"all_ready": true, "not_ready": []}`.
|
||||
10. Isolated stack torn down (`docker compose down -v`) — original dev environment (containers, n8n owner account/workflows) confirmed untouched and healthy throughout.
|
||||
|
||||
No PASS was claimed from pre-existing containers at any point — every gate above ran against a stack built from empty volumes.
|
||||
|
||||
## Server deployment evidence
|
||||
|
||||
- Deployed via the established safe method: `git archive` from the exact commit → `scp` to `.deploy/source-<sha>.tar.gz` on Unraid → extract → update `.deploy/source-revision` → `docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d api web` (db never rebuilt; server `.env` and named volumes — Postgres, n8n — preserved throughout).
|
||||
- Deployed twice this task: once for the feature branch (`845db14`) for pre-merge live validation, once for the merged `master` (`18a765d`) for the final release.
|
||||
- Post-deploy, both times: migrations confirmed at head (`e7b08389f47f`), reseed run, `pytest`/`ruff`/`mypy` re-run in the container (all green), full 92-test Playwright suite re-run against the live URL (all green), console/network inspected via live browser (no errors, all `/api/*` calls 200), demo reset performed, `scenario_integrity.all_ready: true` confirmed both times.
|
||||
- Real shared n8n instance (`http://192.168.10.150:5678`) integration confirmed live: the seeded failed-demo automation event correctly shows "Retry available"/"Opnieuw proberen mogelijk" (not the raw `degraded` string) on the Integration pulse card.
|
||||
|
||||
## Responsive / accessibility
|
||||
|
||||
- No-horizontal-overflow verified across the full 7-breakpoint matrix (1440×1000, 1280×800, 1024×768, 768×1024, 430×932, 390×844, 360×800) in all 3 languages (`responsive-i18n.spec.ts`, 21 tests) plus the original 4-breakpoint English suite (`ui-redesign.spec.ts`).
|
||||
- Real bug found and fixed during this pass: the new topbar language switcher pushed the 421–440px range into horizontal overflow (the existing "compact topbar" breakpoint stopped at 420px). Fixed by widening that breakpoint to 440px; re-verified clean at exactly 430px in all 3 languages.
|
||||
- Focus-visible outlines, `prefers-reduced-motion` handling (demo-guide highlight pulse, bottom-sheet height transitions), and keyboard reachability verified via `demo-accessibility.spec.ts` and the new adaptive-guide/clickable-row tests.
|
||||
|
||||
## Screenshots
|
||||
|
||||
`artifacts/fleet-ops-release/screenshots/`:
|
||||
- `01-login-nl.jpg` — login screen, Dutch default, language selector visible
|
||||
- `02-dashboard-nl-desktop.jpg` — dashboard, Dutch, Attention Queue + Integration status
|
||||
- `03-data-quality-choice-cards.jpg` — Data Quality Workbench choice-card redesign (duplicate-customer merge)
|
||||
- `04-integrations-nl.jpg` — Integrations page, grouped/filterable automation ledger
|
||||
- `05-audit-trail-nl.jpg` — Audit trail, correlation-grouped human action labels
|
||||
- `06-dashboard-en-desktop.jpg` — dashboard, English
|
||||
- `07-dashboard-fr-desktop.jpg` — dashboard, French
|
||||
- `08-about-fr.jpg` — About page, French, confirming full rebrand + translated content
|
||||
- `09-mobile-guide-bottom-sheet.png` — mobile bottom sheet, half state (390×844)
|
||||
- `10-mobile-guide-full.png` — mobile bottom sheet, full state (390×844)
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Data-quality evidence "summary" strings (the free-text detail line under each Attention Queue/Data Quality row, e.g. "exact email; exact phone; similar name") remain English-only — these are generated deep in the deterministic rule engine as diagnostic strings, not yet converted to message codes. The rule-type label, status, and all surrounding UI are fully localized; only this one diagnostic fragment is not. Documented as a follow-up, not blocking.
|
||||
- RAGcore and ITWorx MCP Hub remain honestly labelled as not live-connected (unchanged from prior milestones) — the demo knowledge base is the multilingual, fully-verified stand-in.
|
||||
- Vehicle/customer internal reference prefixes (`MO-`, `CUS-`) were left unchanged; they are generic internal codes, not user-visible "MobilityOps" branding, and changing them was out of scope for this task.
|
||||
- Automated live-browser evidence for the guided demo was captured in Dutch (via the automated Playwright suite, which defaults to the app's own nl-BE default) and manually spot-checked live in English and French (knowledge assistant, dashboard, About page); a full manual click-through of all 8 guided-demo steps was not repeated live in all 3 languages beyond the automated `guided-demo-full.spec.ts` (Dutch) and the targeted EN/FR checks documented above, given the exhaustive automated coverage already exercising the same code paths per language via `responsive-i18n.spec.ts` and `i18n-coverage.spec.ts`.
|
||||
|
||||
## Rollback procedure
|
||||
|
||||
- `.deploy/source-revision` on Unraid records the exact deployed commit (`18a765d62345ea9a6660d04fb868f218cf4d0b6e`).
|
||||
- Prior tarballs remain in `.deploy/` on the server, including `.deploy/source-845db14.tar.gz` (feature branch, pre-merge) and `.deploy/source-4a268c7.tar.gz` (previous release, pre-polish).
|
||||
- To roll back: extract the desired `source-<short-sha>.tar.gz`, 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`. Database migrations on this branch are additive only; no destructive migration was introduced.
|
||||
@@ -1,246 +0,0 @@
|
||||
# MobilityOps functional-completion — final summary
|
||||
|
||||
## Outcome: PASS
|
||||
|
||||
All achievable functional-completion requirements were audited, implemented, tested
|
||||
locally (including a genuine clean-checkout drill), committed, pushed, deployed to
|
||||
Unraid and re-verified against the live server after every batch.
|
||||
|
||||
## Revisions
|
||||
|
||||
- Starting branch: `design/mobilityops-premium-ui`
|
||||
- Starting/observed commit: `54dc952915a4874fcdf14781e1c37feb0e253851`
|
||||
- Completion branch: `feat/mobilityops-functional-completion`
|
||||
- Final commit: `5b2827eb7e84e40d97c4d025debb2af1246908e0` (this is the parent commit
|
||||
the deploy below targets; the commit that actually records this string is
|
||||
necessarily one commit later — `git log -1` on this branch is the authoritative
|
||||
source of the true HEAD)
|
||||
- Gitea branch URL: `https://gitea.itworx.tech/Jens/MobilityOps` (SSH remote
|
||||
`ssh://git@192.168.10.150:222/Jens/MobilityOps.git`), branch
|
||||
`feat/mobilityops-functional-completion`
|
||||
- Deployed URL: `http://192.168.10.150:1236`
|
||||
- Server deployment directory: `/mnt/user/appdata/mobilityops`
|
||||
- Server Compose project: `mobilityops`
|
||||
|
||||
## Exact commands executed (representative — run after every batch)
|
||||
|
||||
```bash
|
||||
# Local backend gate
|
||||
docker compose exec -T api python -m app.cli seed --reset
|
||||
docker compose exec -T api pytest -q
|
||||
docker compose run --rm api ruff check .
|
||||
docker compose run --rm api mypy app
|
||||
|
||||
# Local frontend gate
|
||||
cd frontend && npx tsc -b --noEmit && npm run build
|
||||
|
||||
# Local e2e (against the local dev stack)
|
||||
npx playwright test
|
||||
|
||||
# Deploy the exact committed revision
|
||||
COMMIT=$(git rev-parse HEAD)
|
||||
git archive --format=tar.gz --output=/tmp/source.tar.gz "$COMMIT"
|
||||
scp -P 22 /tmp/source.tar.gz unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
||||
ssh unraid "cd /mnt/user/appdata/mobilityops && tar -xzf .deploy/source.tar.gz && echo $COMMIT > .deploy/source-revision"
|
||||
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web"
|
||||
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api alembic current"
|
||||
ssh unraid "cd /mnt/user/appdata/mobilityops && docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api python -m app.cli seed --reset"
|
||||
|
||||
# e2e against the live server
|
||||
MOBILITYOPS_PUBLIC_URL=http://192.168.10.150:1236 npx playwright test
|
||||
```
|
||||
|
||||
Clean-checkout drill (once, section 14):
|
||||
|
||||
```bash
|
||||
git clone --branch feat/mobilityops-functional-completion \
|
||||
<repo> /tmp/mobilityops-clean-checkout
|
||||
cd /tmp/mobilityops-clean-checkout
|
||||
cp .env.example .env
|
||||
docker compose -p mobilityops-clean up --build -d # isolated project name/ports
|
||||
docker compose -p mobilityops-clean exec -T api alembic current
|
||||
docker compose -p mobilityops-clean exec -T api python -m app.cli seed --reset
|
||||
docker compose -p mobilityops-clean exec -T api pytest -q
|
||||
docker compose -p mobilityops-clean run --rm api ruff check .
|
||||
docker compose -p mobilityops-clean run --rm api mypy app
|
||||
cd frontend && npm ci && npx tsc -b --noEmit && npm run build
|
||||
MOBILITYOPS_PUBLIC_URL=http://localhost:11228 npx playwright test
|
||||
docker compose -p mobilityops-clean down -v # isolated project only
|
||||
```
|
||||
|
||||
## Test and validation results
|
||||
|
||||
| Gate | Local dev stack | Clean-checkout (isolated) | Live Unraid |
|
||||
|---|---|---|---|
|
||||
| `pytest` | 117 passed | 117 passed | — (not applicable; no test runner on the review host) |
|
||||
| `ruff check .` | clean | clean | — |
|
||||
| `mypy app` | 0 issues / 46 files | 0 issues / 46 files | — |
|
||||
| `npx tsc -b` | clean | clean | — |
|
||||
| `npm run build` | clean | clean | — |
|
||||
| `npx playwright test` | 37 passed | 37 passed | **37 passed** (against `http://192.168.10.150:1236`) |
|
||||
| `npm audit` | 4 known advisories (unchanged — see Known limitations) | same | — |
|
||||
|
||||
Every batch (1 through 5) was deployed and re-verified with the full 37-test Playwright
|
||||
suite against the live server before moving to the next batch, not only at the end.
|
||||
|
||||
## Application URLs and ports
|
||||
|
||||
| Service | URL / port | Notes |
|
||||
|---|---|---|
|
||||
| Web (Unraid) | `http://192.168.10.150:1236` | only MobilityOps-owned service exposed on the LAN |
|
||||
| API (Unraid) | Compose-network only | reached through the web nginx `/api/` proxy |
|
||||
| PostgreSQL (Unraid) | Compose-network only | never exposed |
|
||||
| Shared n8n (Unraid) | `http://192.168.10.150:5678` | pre-existing host infrastructure, outside the MobilityOps Compose project |
|
||||
| Web (local dev) | `http://localhost:1228` | |
|
||||
| API (local dev) | `http://localhost:8128` | |
|
||||
| n8n (local dev) | `http://localhost:5678` | bundled, `bundled-n8n` profile |
|
||||
|
||||
## Demo users and access method
|
||||
|
||||
Two fixed seeded identities, selected via the login screen's role buttons (no
|
||||
password): **Amelie De Ridder** (`USR-OPS`, Operations Manager) and **Karim
|
||||
Boujaddaine** (`USR-EMP`, Rental Employee). `POST /api/v1/demo/login` issues an
|
||||
HttpOnly, `SameSite=Lax` signed session cookie; `GET /api/v1/demo/session` (marked
|
||||
`Cache-Control: no-store`) is what the browser actually trusts on every load, not a
|
||||
locally cached copy.
|
||||
|
||||
## Implemented functionality (this pass, on top of the already-accepted M0–M7/design baseline)
|
||||
|
||||
- Fixed two confirmed defects: Vehicles and Bookings both computed a filtered/paginated
|
||||
result but rendered the raw array.
|
||||
- Server-backed session lifecycle (`GET /demo/session`, `POST /demo/logout`), central
|
||||
401 handling, no more `sessionStorage`-as-authority.
|
||||
- A role matrix enforced server-side (403 on every manager-only action for Rental
|
||||
Employee, not just a hidden button) and mirrored in the nav/route guards.
|
||||
- Authoritative, non-mutating return preview (`POST /bookings/{ref}/return-preview`)
|
||||
sharing its evaluation function with commit — fixed a real bug where the frontend's
|
||||
guessed preview text was wrong (damage → described as "maintenance", actual rule
|
||||
"blocked"; the no-contradiction case → described as "available", actual rule always
|
||||
"cleaning" first).
|
||||
- Audit API/UI now expose `before`/`after` (the columns existed but were never
|
||||
serialized) plus a resolved safe entity link.
|
||||
- Typed related-entity snapshots (booking_overlap's related refs are bookings, not
|
||||
vehicles — previously silently unresolved) and a bounded resolution flow for every
|
||||
one of the five data-quality rule types, plus an audited manual scan trigger and
|
||||
documented recurrence linking (`reopened_from`/`previous_decision`).
|
||||
- Role-aware backend search (`GET /api/v1/search`) replacing a blind client-side regex
|
||||
guesser, with a real results panel, keyboard navigation and debouncing.
|
||||
- A safe, confirmed demo-reset UI trigger (the endpoint already existed and was
|
||||
already gated).
|
||||
- Truthful aggregate n8n integration status (`GET /api/v1/integrations/status`) from
|
||||
outbox delivery counts, replacing a single-most-recent-event read; fixed
|
||||
`MCP_HUB_REGISTRATION_ENABLED` being declared in `.env.example` but never wired into
|
||||
`Settings`.
|
||||
- Bounded outbox delivery-lease recovery for a process crash between claim and outcome.
|
||||
- A second n8n workflow (scheduled quality scan), independent of RAGcore/MCP Hub.
|
||||
|
||||
## RAGcore integration status
|
||||
|
||||
Unchanged from the prior baseline and honestly reported throughout: the demo
|
||||
`KnowledgeProvider` (deterministic TF-IDF extractive retrieval over local procedure
|
||||
documents) satisfies the knowledge-assistant acceptance criteria and is fully verified.
|
||||
A `RAGcoreKnowledgeProvider` HTTP adapter is implemented and unit-tested (including its
|
||||
unavailable-degradation path) but was never exercised against a live RAGcore instance in
|
||||
this environment — no live RAGcore instance exists to test against.
|
||||
`KNOWLEDGE_PROVIDER=demo` on the Unraid deployment; no simulated live connection is ever
|
||||
shown.
|
||||
|
||||
## MCP Hub integration status
|
||||
|
||||
The four read-only provider endpoints are implemented, tested, and directly
|
||||
curl-verified with correct service-token auth enforcement and audit logging.
|
||||
`MCP_HUB_REGISTRATION_ENABLED` — previously declared in `.env.example` but silently
|
||||
dropped by `extra="ignore"` since it had no `Settings` field — is now actually wired in
|
||||
and honestly reported (`GET /api/v1/integrations/status`'s `mcp_hub.state`). It is
|
||||
`false` on the Unraid deployment (`state: "not_configured"`). No live Hub instance was
|
||||
reachable in this environment to verify an actual Hub round trip.
|
||||
|
||||
## n8n integration status
|
||||
|
||||
Fully implemented and live-verified. The original return-processing workflow: verified
|
||||
against both the local bundled instance and the shared Unraid instance, including a real
|
||||
degraded-mode drill in an earlier session (n8n stopped mid-flow → return still committed
|
||||
locally, event stayed `pending` with backoff, self-healed once n8n returned) and the
|
||||
manual-retry path. This pass adds:
|
||||
|
||||
- **Truthful status**: `GET /api/v1/integrations/status` derives n8n health from
|
||||
aggregate outbox counts (pending/delivering/succeeded/failed), not the single most
|
||||
recent event.
|
||||
- **Stale-delivery-lease recovery**: a claimed-but-never-resolved `delivering` row (the
|
||||
process crashing between claim and outcome) is now recoverable; unit-tested including
|
||||
a simulated crash, and confirmed a still-alive worker's unexpired lease is never
|
||||
touched.
|
||||
- **Second workflow**: `mobilityops-scheduled-quality-scan` (hourly + manual-test
|
||||
trigger, ships `"active": false"`), calling `POST
|
||||
/api/v1/integrations/n8n/scheduled-scan`. Live-verified two ways: (1) executed
|
||||
end-to-end via the Manual test trigger against the **local** n8n instance — full
|
||||
green execution in the n8n editor, confirmed by the resulting
|
||||
`data_quality_scan_run` audit event (`actor_type=service`); (2) published to the
|
||||
**shared Unraid n8n** via `deploy/unraid/setup-scheduled-scan.sh` and the resulting
|
||||
endpoint directly curl-verified against the live deployed API, also confirmed via the
|
||||
audit trail. The shared instance's own UI could not be browser-tested directly — it
|
||||
runs `N8N_SECURE_COOKIE=true` and refuses login over the plain-HTTP LAN URL used for
|
||||
automated testing here, which is correct, pre-existing shared-infrastructure
|
||||
behaviour and out of scope to change.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- RAGcore and ITWorx MCP Hub remain honestly not-live-connected — no live instance of
|
||||
either exists in this environment (environment limitation, not a code defect).
|
||||
- `npm audit`: one moderate esbuild/Vite dev-server-only advisory (fix requires a Vite
|
||||
major upgrade, deliberately deferred), and a
|
||||
react-router RSC-mode advisory that doesn't apply (the app never uses RSC/SSR mode) —
|
||||
both pre-existing, confirmed unchanged by this pass's clean `npm ci`.
|
||||
- Demo authentication remains the accepted HMAC-cookie PoC mechanism tied to two fixed
|
||||
seeded identities — not a production identity provider.
|
||||
- The scheduled quality-scan workflow's own n8n-engine execution was verified live
|
||||
against the local bundled n8n and, for the HTTP round trip specifically, against the
|
||||
shared Unraid n8n's resulting API call — not against a full n8n-engine execution *on
|
||||
the shared instance itself*, for the browser-access reason above.
|
||||
- `n8n_delivery_lease_seconds` (120s default) is a code-level tunable, not exposed in
|
||||
`.env.example`, consistent with the existing `n8n_dispatch_interval_seconds`/
|
||||
`n8n_max_attempts`/`n8n_http_timeout_seconds` tunables already handled that way.
|
||||
|
||||
## Clean deployment instructions
|
||||
|
||||
See `deploy/unraid/README.md` and `docs/17-runbook.md` for the full runbook. Redeploy
|
||||
the exact committed revision:
|
||||
|
||||
```bash
|
||||
COMMIT=<commit to deploy>
|
||||
git archive --format=tar.gz --output=/tmp/source.tar.gz "$COMMIT"
|
||||
scp -P 22 /tmp/source.tar.gz unraid:/mnt/user/appdata/mobilityops/.deploy/source.tar.gz
|
||||
ssh unraid "cd /mnt/user/appdata/mobilityops \
|
||||
&& tar -xzf .deploy/source.tar.gz \
|
||||
&& echo $COMMIT > .deploy/source-revision \
|
||||
&& docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml up --build -d db api web \
|
||||
&& docker compose -p mobilityops -f compose.yaml -f compose.unraid.yaml exec -T api alembic current \
|
||||
&& curl -fsS http://127.0.0.1:1236/health"
|
||||
```
|
||||
|
||||
`.env` and both named volumes (`mobilityops-db`, `mobilityops-n8n`) are preserved by
|
||||
this flow; nothing outside the `mobilityops` Compose project is touched.
|
||||
|
||||
## Five-minute demonstration flow
|
||||
|
||||
1. Open `http://192.168.10.150:1236`, log in as **Operations Manager**.
|
||||
2. Dashboard: point out the persisted readiness metrics and the now-truthful n8n
|
||||
integration-status card (aggregate counts, not just the latest event).
|
||||
3. Global search (`Ctrl/Cmd+K`): type a vehicle, booking or issue reference; use arrow
|
||||
keys + Enter to navigate; show the no-results state for a nonsense query.
|
||||
4. Bookings: filter by status, page through results (max 25/page), confirm page 2
|
||||
differs from page 1.
|
||||
5. Open an active booking → capture a return with a below-canonical odometer reading →
|
||||
the review step shows the server's authoritative evaluation (odometer regression
|
||||
flagged, resulting status and reason) → confirm → result screen distinguishes local
|
||||
commit success from queued-not-yet-confirmed n8n delivery, links to the created
|
||||
data-quality issue.
|
||||
6. Data quality: run a manual scan; open the newly flagged (or an existing) issue for
|
||||
each rule type and show its dedicated bounded resolution flow (not raw JSON).
|
||||
7. Audit trail: filter by the correlation ID from the return above; show the
|
||||
human-readable before/after change summary, expand the raw-JSON `<details>`.
|
||||
8. Switch role to **Rental Employee**: show Data Quality/Integrations/Audit are absent
|
||||
from the nav, and that direct URL navigation to any of them shows the restricted
|
||||
message rather than partial data or a crash.
|
||||
9. Switch back to Operations Manager, trigger **Reset demo data** with confirmation,
|
||||
land back at login, log in again to confirm deterministic data was restored.
|
||||
@@ -1,207 +0,0 @@
|
||||
# Live n8n + RAGcore integration — final evidence
|
||||
|
||||
No credential values, tokens, or secrets appear anywhere in this document. Where a
|
||||
credential or trace ID is referenced, only its name or an opaque reference identifier is
|
||||
given, never its value.
|
||||
|
||||
## Commit
|
||||
|
||||
Built on branch `feat/live-n8n-ragcore-integration`, HEAD at commit
|
||||
`aaa16305354d34f9c1f4d57253d33d9062c38faa` ("docs: record WF2 retry fix and WF4's
|
||||
n8n-session-expiry blocker"). Run `git log --oneline feat/live-n8n-ragcore-integration`
|
||||
for the full history of this effort.
|
||||
|
||||
## Scope
|
||||
|
||||
The brief required treating n8n (`https://n8n.itworx.tech`) as a full third integration
|
||||
layer alongside RAGcore and MCP Hub, with Fleet Ops keeping exclusive ownership of
|
||||
business rules, authorization, transactions, audit, and idempotency. Four canonical n8n
|
||||
workflows were required. The repository (`n8n/workflows/*.json` + `MANIFEST.md` +
|
||||
`n8n/workflows/check_drift.py`) is the source of truth for cleaned workflow definitions;
|
||||
the Fleet Ops integration status page (`/automation`) shows real per-workflow operational
|
||||
evidence, not a config boolean.
|
||||
|
||||
## Result summary
|
||||
|
||||
| # | Workflow | Status | Live evidence this round |
|
||||
|---|---|---|---|
|
||||
| 1 | Fleet Ops — Vehicle Return Orchestration | **Live, hardened** | Timeout+bounded-retry gap found and fixed |
|
||||
| 2 | Fleet Ops — Scheduled Data Quality Scan | **Live, hardened** | Same gap found and fixed |
|
||||
| 3 | Fleet Ops — RAGcore Procedure Sync | **Blocked** | Not built — RAGcore rejects credential issuance (see below) |
|
||||
| 4 | Fleet Ops — Workflow Error Handler | **Live, validated** | Mock + genuine induced-failure test; own hardening incomplete (see below) |
|
||||
|
||||
Full per-workflow detail (purpose, trigger, event contract, required credentials, live
|
||||
workflow ID, checksum) is in `n8n/workflows/MANIFEST.md`, which is the authoritative,
|
||||
continuously-updated source — this document is a point-in-time summary of that state
|
||||
plus the reasoning behind what's not done.
|
||||
|
||||
## Workflow 1 — Vehicle Return Orchestration
|
||||
|
||||
Live workflow ID `mobilityops-return-processing`. Validated in an earlier round of this
|
||||
effort: webhook trigger requires Header Auth (`Fleet Ops Webhook Trigger Token`),
|
||||
validates `event_type == vehicle.returned.v1`, derives a follow-up category, calls Fleet
|
||||
Ops's `/return-callback` endpoint with an `Idempotency-Key` header via a named
|
||||
`Fleet Ops Service Token` credential (not a literal secret), and responds with a
|
||||
controlled JSON result. Idempotent on both sides (`event_id` flows through as the
|
||||
dedup key; the backend independently checks for a prior audit event before recording
|
||||
again).
|
||||
|
||||
**This round's finding**: the `Record follow-up` HTTP node had no explicit timeout and
|
||||
"Retry On Fail" disabled — a real gap against the requirement that external dependencies
|
||||
have timeouts and bounded retries. Fixed live: Retry On Fail (3 tries, 1000ms wait) + a
|
||||
15000ms timeout, published. Safe to retry because the callback is idempotent. Repo
|
||||
definition and manifest checksum synced (commit `0562893`).
|
||||
|
||||
Attached to workflow 4 as its Error Workflow.
|
||||
|
||||
## Workflow 2 — Scheduled Data Quality Scan
|
||||
|
||||
Live workflow ID `mobilityops-scheduled-quality-scan`. Validated earlier: hourly
|
||||
Schedule Trigger + a Manual Trigger for on-demand testing, both feeding a single HTTP
|
||||
call to Fleet Ops's `/scheduled-scan` endpoint (Header Auth via the same `Fleet Ops
|
||||
Service Token` credential, 15000ms timeout already configured), which runs the
|
||||
domain-level `run_scan()` function — documented and tested as idempotent by
|
||||
construction (only ever creates an issue for a condition that doesn't already have one
|
||||
open), so overlapping or retried triggers do no duplicate domain work.
|
||||
|
||||
**This round's finding**: the same Retry On Fail gap as workflow 1 (timeout was already
|
||||
set, retries were not). Fixed live the same way (3 tries, 1000ms wait), published. Repo
|
||||
definition and manifest checksum synced (commit `167bf49`).
|
||||
|
||||
Attached to workflow 4 as its Error Workflow.
|
||||
|
||||
## Workflow 3 — RAGcore Procedure Sync — blocked
|
||||
|
||||
**Not built.** This workflow needs an application credential (scope `sources:sync`) for
|
||||
the `fleet-ops` application in RAGcore. Two independent issuance attempts, in two
|
||||
separate rounds of this effort, both failed with an opaque server-side rejection:
|
||||
|
||||
1. **Raw API**: `POST /v1/applications/{id}/credentials` → `400`, "authoritative
|
||||
service-account state rejected issuance".
|
||||
2. **RAGcore admin UI**, this round, after the project owner explicitly authorized
|
||||
Claude to self-issue the credential: the "Issue credential" form for the `fleet-ops`
|
||||
application, submitted as the Platform Admin role (the highest role visible in the
|
||||
RAGcore admin), with name `n8n-ragcore-procedure-sync` and scope `sources:sync` only.
|
||||
Result: "Something went wrong. The credential could not be issued with those
|
||||
values.", trace reference `1955c6a8968c4941a22a1faef39e17a7`.
|
||||
|
||||
The `fleet-ops` application itself shows as ordinary/`Active` in the RAGcore admin, with
|
||||
no visible lock flag, and RAGcore's own OpenAPI spec documents no validation rule that
|
||||
would explain either rejection (no `422`, no field-level errors). Two independent paths
|
||||
— a raw API call and the admin UI as the top admin role — hitting the same failure
|
||||
signature is conclusive evidence this is a RAGcore-side policy or bug, not a Fleet Ops
|
||||
request-shape or permission problem. It is not fixable from the Fleet Ops side or
|
||||
through further UI automation. Resolving it requires whoever operates the RAGcore
|
||||
instance to look up the trace ID above (and the earlier raw-API rejection) in RAGcore's
|
||||
own logs.
|
||||
|
||||
The real RAGcore contract this workflow will be built against — once a working
|
||||
credential exists — was independently inspected via RAGcore's live OpenAPI spec and is
|
||||
recorded in `docs/live-ai-integration/n8n-current-state.md` and
|
||||
`contracts/ragcore-contract-assumptions.md`: control-plane endpoints require an
|
||||
`Idempotency-Key` header; ingestion is `POST /v1/uploads`; retrieval is `POST
|
||||
/v1/search` / `/v1/context` / `/v1/answers` (the latter requiring `requested_space_ids`,
|
||||
an array of knowledge-space UUIDs); health is `/health/live` and `/health/ready` (not
|
||||
`/health`); the scope enum is `search, context, answer, documents:read, citations:read,
|
||||
feedback:write, sources:sync`.
|
||||
|
||||
**`RAGcoreKnowledgeProvider` adapter** (`backend/app/services/knowledge/ragcore.py`)
|
||||
still targets the earlier speculative contract (`/health`, `POST /api/v1/ask`, Bearer
|
||||
token) rather than the real one above. This was deliberately **not** rewritten this
|
||||
round: rewriting it blind, without a credential to validate against, risks introducing
|
||||
a silent behavioral bug in exactly the code path responsible for the project's "AI must
|
||||
never invent an answer when RAGcore is unavailable or returns insufficient evidence"
|
||||
guarantee — for example a wrong `evidence_state` mapping that looks fine in code review
|
||||
but misclassifies "unavailable" as "insufficient" (or vice versa) against the real
|
||||
response shape. The adapter's current behavior is honest and safe (it degrades cleanly
|
||||
to `unavailable` on any request or parsing failure, and `ragcore_api_token` is unset by
|
||||
default so the app correctly runs on the local demo knowledge provider today). The
|
||||
rewrite stays queued behind the same credential blocker as workflow 3.
|
||||
|
||||
## Workflow 4 — Workflow Error Handler
|
||||
|
||||
Live workflow ID `Xppn2rAEqUuyiCJF`. Built and live-validated in an earlier round:
|
||||
Error Trigger → a Code node that derives a bounded, secret-free failure report (error
|
||||
category classified from the message text, truncated summary, no stack trace, no
|
||||
headers or tokens) → an HTTP call to Fleet Ops's `/workflow-error` endpoint (Header Auth
|
||||
via the same `Fleet Ops Service Token` credential), which registers the failure as an
|
||||
audit event idempotently keyed on `execution_id`.
|
||||
|
||||
Set as the Error Workflow on both workflow 1 and workflow 2. Confirmed workflow 4 has no
|
||||
Error Workflow of its own (prevents a recursive loop).
|
||||
|
||||
**Live validation performed**: a pinned mock Error Trigger payload produced a real `200
|
||||
{"status":"registered", ...}` from the live Fleet Ops server; re-running the identical
|
||||
payload produced `"status":"already_registered"`, confirming idempotency. A genuine
|
||||
induced failure (temporarily pointing workflow 2's HTTP node at a nonexistent path, then
|
||||
reverting) confirmed workflow 2 itself fails correctly against a broken endpoint and
|
||||
recovers cleanly once reverted.
|
||||
|
||||
**Known limitation**: n8n's Error Workflow trigger does not fire for manual editor
|
||||
"Execute workflow" test runs — checked via workflow 4's own Executions list after the
|
||||
induced workflow-2 failure, and confirmed no new execution appeared. n8n only invokes a
|
||||
workflow's assigned Error Workflow for unattended/production trigger executions, not
|
||||
manual test runs from the editor. The mock-data path exercises the same nodes, logic,
|
||||
and real Fleet Ops endpoint, but a fully automatic (schedule- or webhook-triggered)
|
||||
failure cascading into workflow 4 was not observed live in either round.
|
||||
|
||||
**Open follow-up (minor, non-blocking)**: continuing this round's acceptance pass to
|
||||
workflow 4 found the same timeout/retry gap as workflows 1 and 2 on its own outbound
|
||||
HTTP call. A fix was started (15000ms timeout added, Retry On Fail toggled on) but n8n's
|
||||
autosave began failing with "Unauthorized" mid-edit; a fresh browser tab confirmed the
|
||||
n8n session had expired (redirected to `/signin`). Nothing was saved — workflow 4's live
|
||||
definition is unchanged from before this round, so there is no partial or broken state.
|
||||
This is lower-stakes than workflows 1/2 (workflow 4 is the error notifier itself, not a
|
||||
primary business flow, and a failed error-report is already visible in n8n's own
|
||||
execution history via `On Error: Stop Workflow`) but should be finished once the n8n
|
||||
browser session is re-authenticated.
|
||||
|
||||
## Repository source of truth
|
||||
|
||||
`n8n/workflows/` holds cleaned, credential-value-free JSON definitions for all built
|
||||
workflows, `n8n/workflows/MANIFEST.md` documents purpose/trigger/contract/credentials/
|
||||
live-ID/checksum for all four canonical workflows (including workflow 3's blocked
|
||||
status), and `n8n/workflows/check_drift.py` is a read-only script that compares the
|
||||
repo definitions against the live instance via n8n's Public API and reports drift —
|
||||
safe to run in CI as a non-blocking check. No literal export/download mechanism was
|
||||
found working in this n8n version, so each definition was reconstructed from direct,
|
||||
verified UI inspection rather than a native export; this limitation is noted in the
|
||||
manifest itself.
|
||||
|
||||
## Integration status page
|
||||
|
||||
`/automation` (Operations Manager only) surfaces real per-workflow evidence derived
|
||||
purely from Fleet Ops's own audit/outbox tables — no new dependency on n8n's API was
|
||||
added to the backend. Each of the four canonical workflows shows a status (not built /
|
||||
no evidence yet / operational) and a last-evidence timestamp; the scheduled-scan
|
||||
evidence specifically filters to `actor_type == "service"` so a manually-triggered scan
|
||||
in the UI doesn't count as n8n evidence. An error-handler summary line reports total
|
||||
registered automation failures and the most recent one.
|
||||
|
||||
Verified live in the browser (Dutch locale) both locally and on the deployed
|
||||
production server (`http://192.168.10.150:1236/automation`): correctly showed "3 van 4
|
||||
canonieke n8n-workflows hebben actuele evidentie van werking" with real timestamps for
|
||||
the return/scan/error-handler workflows, "Nog Niet Gebouwd" for the RAGcore sync, and
|
||||
the real error-handler registration from this effort's live testing.
|
||||
|
||||
## Deployments performed (all explicitly user-approved)
|
||||
|
||||
1. Backend `/workflow-error` endpoint (commit `bbdb4a9`) — deployed and verified
|
||||
(`/health` OK, new endpoint returns `422` not `404` on an empty POST body).
|
||||
2. Integration status page, backend + frontend (commit `4049c0c`) — deployed and
|
||||
verified (`/health` OK, page renders real evidence in the browser).
|
||||
|
||||
The three n8n-side node edits this round (WF1 timeout/retry, WF2 timeout/retry, WF4's
|
||||
incomplete attempt) are live edits to the n8n instance itself and do not require a
|
||||
Fleet Ops redeploy.
|
||||
|
||||
## What's left
|
||||
|
||||
1. **RAGcore credential issuance** — blocked on RAGcore's own server-side rejection
|
||||
(trace `1955c6a8968c4941a22a1faef39e17a7` and the earlier raw-API `400`). Needs
|
||||
RAGcore's operator to investigate. Unblocks workflow 3 and the
|
||||
`RAGcoreKnowledgeProvider` real-contract rewrite.
|
||||
2. **Workflow 4's own timeout/bounded-retry hardening** — needs the n8n browser session
|
||||
re-authenticated to finish; a small, well-understood, non-blocking edit.
|
||||
3. **Fleet Ops logo/favicon** — explicitly deferred by the project owner as a separate,
|
||||
unrelated follow-up task, not part of this integration effort.
|
||||
@@ -1,157 +0,0 @@
|
||||
# Current demo-readiness gap audit
|
||||
|
||||
Performed 2026-08-03 against `feat/mobilityops-functional-completion` @
|
||||
`e0c7ed60112510687627d20a957af91c8b9db7f8` (verified: correct branch, working tree
|
||||
clean, matches the deployed Unraid revision). Method: read `CLAUDE.md`/`AGENTS.md`
|
||||
(identical, no demo-specific guidance yet), `README.md`, `PROJECT_STATE.md`, the docs
|
||||
list in the brief, `artifacts/final-acceptance/summary.md`,
|
||||
`artifacts/functional-completion/final-summary.md`, `seed/generate_seed.py`,
|
||||
`seed/*.csv`, `backend/app/seed_loader.py`, `backend/app/core/config.py`,
|
||||
`backend/app/api/routers/dashboard.py`; live-browsed the deployed app at
|
||||
`http://192.168.10.150:1236` (login, dashboard, a data-quality issue, a booking detail,
|
||||
knowledge, automation).
|
||||
|
||||
## What already works well (do not rebuild)
|
||||
|
||||
- The premium Control Rail UI, role-based nav, server-backed sessions, return preview/
|
||||
commit, all five data-quality resolution flows, audit before/after, role-aware search,
|
||||
truthful integration status, demo reset, stale-lease recovery and the two n8n
|
||||
workflows are all implemented, tested and live-verified per
|
||||
`artifacts/functional-completion/final-summary.md` — this pass does not touch that
|
||||
business logic.
|
||||
- `docs/13-seed-and-demo-scenarios.md` already names exactly the scenarios this brief
|
||||
wants surfaced: **S1** odometer-regression return (`BK-DEMO-RETURN`/`MO-024`), **S2**
|
||||
duplicate customer (`CUS-0012`/`CUS-0178`, issue `DQ-DEMO-DUPLICATE`), **S4** booking
|
||||
overlap (`MO-016`, issue `DQ-DEMO-OVERLAP`, bookings `BK-DEMO-OVERLAP-A/B`), **S5**
|
||||
failed workflow (seeded failed outbox event, retryable from Automation), **S6**
|
||||
grounded knowledge question. These map directly to the brief's 5 requested scenarios
|
||||
— no new scenarios need to be invented, only surfaced with guidance. (S3, "missing
|
||||
inspection before next booking" on `MO-031`, exists too but isn't one of the 5
|
||||
requested; leave it as-is, it still shows up in the attention queue.)
|
||||
- `seed/generate_seed.py` already produces believable Flemish/Dutch data: first/last
|
||||
name pools (Sofie, Lotte, Bram, Peeters, Janssens, Vermeulen, ...), real Kempen-region
|
||||
towns (Geel, Turnhout, Herentals, Mol, Westerlo, Tessenderlo-Ham), `.test` email
|
||||
addresses, and believable RV/camper makes (Adria, Dethleffs, Hymer, Bürstner, ...).
|
||||
Confirmed live: `DQ-DEMO-DUPLICATE` shows customer "Bram Peeters" (Geel) with email
|
||||
`bram.peeters.12@example.test`. **The underlying data quality is already
|
||||
demo-grade — the gap is structural/experiential, not data flavour.**
|
||||
|
||||
## Confirmed gaps
|
||||
|
||||
### 1. No fictional organisation identity anywhere
|
||||
Login/demo-entry screen says "MobilityOps · Control Centre" and generic marketing copy
|
||||
("Every hand-off. One clear view.") but never names a demonstration organisation. A
|
||||
stranger cannot tell *whose* fleet this is. **Confirmed via full-repo search:**
|
||||
"Northstar Mobility" is never rendered in the UI, but it is **already the project's
|
||||
locked fictitious tenant name** — `PROJECT_STATE.md`'s "Locked decisions" section
|
||||
states verbatim *"Fictitious tenant: Northstar Mobility Demo"*, and
|
||||
`ragcore_tenant` defaults to `"northstar-mobility-demo"` in `backend/app/core/config.py`,
|
||||
`compose.yaml`, `.env.example`, `knowledge/manifest.json` and `contracts/mcp-tools.json`.
|
||||
So using "Northstar Mobility" as the visible demo brand (as this brief suggests) is not
|
||||
a new invention — it surfaces a decision the project already made and only ever used
|
||||
internally as a RAGcore-tenant slug. No renaming/config changes needed, just make it
|
||||
visible.
|
||||
|
||||
### 2. No guided path — a visitor has no idea where to start
|
||||
The dashboard shows real attention-queue items (duplicate customer, booking overlap,
|
||||
vehicle status conflict, missing field) and today's movements (`BK-DEMO-RETURN`
|
||||
11:00), but nothing tells a first-time visitor *which* of these is worth clicking, in
|
||||
what order, or why. There is no "start guided demo" affordance, no scenario overview,
|
||||
no step-by-step walkthrough. Everything technically works; nothing narrates.
|
||||
|
||||
### 3. Live date anchoring is broken — confirmed, reproducible
|
||||
`seed/bookings.csv` stores **absolute fixed ISO timestamps** authored around anchor
|
||||
date 2026-08-01 (`generate_seed.py --anchor 2026-08-01`). `backend/app/core/config.py`'s
|
||||
`demo_today` setting (`"2026-08-01"`) is used *only* by
|
||||
`dashboard.py::_today()` to filter "today's movements" against that frozen date — nothing
|
||||
re-anchors the underlying booking/inspection/maintenance/outbox dates themselves.
|
||||
**Live-confirmed right now** (today is 2026-08-03): `BK-DEMO-RETURN` shows status
|
||||
`active` with `ends_at = 01/08/2026 09:00` — already two days in the past, for a
|
||||
booking that's supposed to look "in progress, due for return soon." `BK-DEMO-NEXT`
|
||||
(meant to read as "near-future") starts 2026-08-02, also already past. Every day this
|
||||
environment isn't reset, this gets worse, and after any `seed --reset` it snaps right
|
||||
back to the same frozen 2026-08-01-relative positions regardless of the real reset
|
||||
moment. This is exactly the failure mode brief section 7 describes and must be fixed at
|
||||
the source (`seed_loader.py`), not papered over in the UI.
|
||||
|
||||
### 4. Knowledge assistant names "RAGcore" directly, not honestly "demo mode"
|
||||
`/knowledge` page copy: "Answers are shown only when **RAGcore** returns sufficient
|
||||
cited evidence" and a "RAGcore" label on the sources block — while the actual active
|
||||
provider is `KNOWLEDGE_PROVIDER=demo` (confirmed: badge below correctly shows
|
||||
`demo · Available · 10 procedures indexed`, which contradicts the body copy one line
|
||||
below it). This is the exact misleading-integration-status problem brief section 11
|
||||
warns against — the small badge is honest, the prose isn't. No suggested questions are
|
||||
offered.
|
||||
|
||||
### 5. Integration status labels are technical, not demo-plain-language
|
||||
Automation page shows raw states like `not_configured`/`degraded`/`operational` (English,
|
||||
developer register) rather than the plain-language labels section 12 asks for
|
||||
(Operationeel / Demomodus / Niet gekoppeld / Voorbereid / Verwerking mislukt / Opnieuw
|
||||
proberen mogelijk).
|
||||
|
||||
### 6. Data-quality resolution panels are functionally complete but not narrated
|
||||
Confirmed live on `DQ-DEMO-DUPLICATE`: the compare/merge UI works well, but nothing
|
||||
explains *why this matters operationally* (duplicate billing/contact risk) or what
|
||||
happens next in plain terms before the user acts. Same pattern across the other 4
|
||||
rule-type panels (already reviewed in this session's earlier batch). No "demo scenario"
|
||||
filter exists to surface the curated issues first.
|
||||
|
||||
### 7. No "About this demo" page
|
||||
No page anywhere explains what's real, what's synthetic, what's live vs. demo-mode vs.
|
||||
not-connected, or the architecture/testing approach in plain terms. A visitor exploring
|
||||
alone has no way to self-answer "is this real?"
|
||||
|
||||
### 8. No permanent, explained "synthetic demo" indicator
|
||||
The existing `demo-banner` ("Synthetic demo data · no real customer or vehicle
|
||||
information") is a full-width static bar repeated on every page — present, but static
|
||||
text only, no link to more explanation, no reset-timestamp/anchor info, and (per section
|
||||
4) shouldn't be a dominant warning bar; it should be a subtle, explained badge.
|
||||
|
||||
### 9. Demo reset exists but is not discoverable as a demo feature
|
||||
`POST /api/v1/demo/reset` and its sidebar UI trigger (this session's earlier batch)
|
||||
work correctly and are Operations-Manager-gated, but reset isn't connected to any demo
|
||||
narrative (no "prepare demo again" framing, not reachable from an About page or guide,
|
||||
no post-reset scenario-integrity confirmation beyond the raw counts).
|
||||
|
||||
### 10. No demo manifest / no single source of "what's on right now"
|
||||
Nothing exposes demo mode, scenario list, integration status and guidance availability
|
||||
as one small structured payload the frontend can render consistently (badge tooltip,
|
||||
guide, scenario overview, About page all currently would have to duplicate this
|
||||
knowledge if built independently).
|
||||
|
||||
### 11. Everything is in English; the brief asks for consistent Dutch demo text
|
||||
All existing UI copy is English. The brief explicitly asks for consistent Dutch product
|
||||
text for the demo-facing additions. Recorded decision (no existing doc answers this,
|
||||
and translating the entire already-shipped, already-tested Control Rail UI is out of
|
||||
proportion to "add guidance," would touch dozens of files and contradicts "this is not
|
||||
a redesign"): **new demo-productization surfaces are written in Dutch** (demo entry
|
||||
copy, Demo Guide, scenario overview, About-this-demo page, the demo badge/tooltip,
|
||||
plain-language integration-status labels). **Existing operational screens (Dashboard,
|
||||
Vehicles, Bookings, Data Quality workbench, Audit, Automation internals) stay in
|
||||
English** — they are already shipped, tested, and section 16 explicitly forbids
|
||||
rebuilding working screens without functional reason. This mirrors how a real bilingual
|
||||
guided demo works: narration in one language, software in whatever it already ships in.
|
||||
|
||||
## Scope reminder (not gaps — explicitly out of scope per the brief)
|
||||
|
||||
- No RAGcore or MCP Hub live implementation.
|
||||
- No new business logic in the 5 resolution flows — only explanatory framing around
|
||||
the existing ones.
|
||||
- No full redesign, new component library, new color palette, or full retranslation.
|
||||
|
||||
## Plan (implementation batches, matches task list)
|
||||
|
||||
1. Fix date anchoring in `seed_loader.py` (shift all seeded dates by `today − anchor`
|
||||
at every reset); switch `dashboard.py::_today()` to real wall-clock date; add a
|
||||
`demo_data_seeded` audit marker recording the anchor/reset moment.
|
||||
2. `GET /api/v1/demo/manifest` (org name, demo mode, scenarios, integration status,
|
||||
reset timestamp) + Dutch demo entry screen + permanent demo badge with popover.
|
||||
3. Demo Guide (collapsible panel/bottom sheet, 8 steps) + scenario overview (5 cards)
|
||||
on the dashboard.
|
||||
4. Layer plain-language Dutch explanation onto the return flow, the 5 DQ panels and the
|
||||
knowledge assistant (fix the RAGcore-naming bug), without touching their logic.
|
||||
5. Plain-language integration status labels, richer audit narration for guide-linked
|
||||
events, "Over deze demo" page, reset wired into guide/About/OM menu with a
|
||||
post-reset scenario-integrity check.
|
||||
6. Full guided-demo Playwright test + targeted tests per section 19.
|
||||
7. Clean-checkout demo drill, docs, Unraid deploy, final evidence with screenshots.
|
||||
@@ -1,117 +0,0 @@
|
||||
# MobilityOps current UX audit
|
||||
|
||||
Date: 2026-08-02
|
||||
Audited revision: `dfabb41582e302f45a3de826f85f531bf23dfc8b`
|
||||
Live URL: `http://192.168.10.150:1236`
|
||||
Role used: Operations Manager, deterministic synthetic seed
|
||||
|
||||
## Executive finding
|
||||
|
||||
The current product is functionally complete and unusually honest for a proof of concept,
|
||||
but its interface is a thin functional shell. It exposes the right workflows without yet
|
||||
providing the information architecture, hierarchy, interaction safeguards, or responsive
|
||||
behaviour expected of premium operational software. The redesign should preserve the
|
||||
backend and every working route while replacing the horizontal demo navigation, repeated
|
||||
card/table grammar, and developer-facing status language with a calm operational control
|
||||
centre built around decisions, readiness, evidence, and traceable outcomes.
|
||||
|
||||
No critical functional blocker was found. The highest-priority design problems are the
|
||||
mobile navigation model, loss of table context on small screens, weak decision hierarchy
|
||||
on the dashboard, and insufficient guidance before consequential actions.
|
||||
|
||||
## Audit method and evidence
|
||||
|
||||
The deployed application was inspected in Chrome with explicit viewport overrides and a
|
||||
fresh deterministic demo reset. DOM landmarks, interactive controls, horizontal overflow,
|
||||
console output, and visible state were inspected alongside screenshots.
|
||||
|
||||
| Viewport | Coverage | Result |
|
||||
|---|---|---|
|
||||
| 1440 × 1000 | Login and every major route/detail/workflow | No page overflow; hierarchy and density issues documented below |
|
||||
| 1280 × 800 | Dashboard, fleet, return, duplicate review | No page overflow; important sections regularly fall below the fold |
|
||||
| 768 × 1024 | Dashboard, fleet, return, duplicate review | Header consumes excessive vertical space; tables lose desktop rhythm |
|
||||
| 390 × 844 | Login, dashboard, fleet, return, duplicate review, knowledge | No document overflow, but navigation wraps into a dense block and table labels disappear |
|
||||
|
||||
Representative baseline captures:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The full baseline screenshot set is in `artifacts/design-validation/current/`.
|
||||
|
||||
## Severity scale
|
||||
|
||||
- **P1 — major:** materially reduces operational comprehension, safety, accessibility, or
|
||||
task efficiency; must be solved by the redesign.
|
||||
- **P2 — moderate:** weakens polish, trust, consistency, or discoverability; should be
|
||||
solved as part of the shared system or relevant screen.
|
||||
- **P3 — minor:** local refinement that does not block task completion.
|
||||
|
||||
## Findings
|
||||
|
||||
| Severity | Affected screens | Finding | Recommended direction |
|
||||
|---|---|---|---|
|
||||
| P1 | Global shell, all mobile screens | Seven navigation links wrap into three rows, followed by the user identity and role action. It consumes most of the first viewport and provides no mobile wayfinding pattern. | Use a persistent desktop rail and an intentionally compact mobile top bar plus labelled bottom navigation or accessible menu. Keep high-value destinations visible. |
|
||||
| P1 | Fleet, bookings, quality, automation, audit | Mobile CSS hides table headers and turns cells into unlabelled stacked values. The user loses field meaning, scan alignment, and row boundaries. | Create responsive row cards with explicit `data-label`/term-value semantics and prioritize the two or three fields required for the task. |
|
||||
| P1 | Dashboard | Seven equal KPI tiles dominate the first screen while urgent work, today’s movements, and automation health are below the fold. All metrics have equal visual weight regardless of operational consequence. | Lead with a fleet-readiness band and a compact today/attention workspace. Treat counts as context for decisions, not the page’s main product. |
|
||||
| P1 | Bookings | All 246 bookings render in one table (492 links) with only a status filter. Departures today, returns today, readiness, missing inspection, and blocked state are not visually prioritized. | Add business-state segments, search, compact readiness markers, and a scannable schedule/window column without changing the API contract. |
|
||||
| P1 | Return workflow | A single form is fast but gives no progress model, no calculated outcome preview, and no explanation of suspicious odometer values until after submission. The irreversible business action is visually equivalent to a normal form submit. | Use a concise two-stage flow: inspect/record, then review calculated consequences before commit. Preserve one-page efficiency and existing endpoint/idempotency. |
|
||||
| P1 | Duplicate review | Matching fields and conflicts are not visually distinguished. Survivor selection and field selection compete in one table; the outcome preview is a sentence. Confirmation is an inline `alertdialog` without true dialog focus management. | Align records, label matches/conflicts, show selected survivor and resulting record, then use an accessible confirmation dialog with explicit rewiring/tombstone consequences. |
|
||||
| P1 | Automation and integrations | The page is only an outbox table. Raw event names, UUID fragments, attempt counts, and last errors are primary content. RAGcore and MCP Hub state are absent, so users cannot tell whether they are disabled, unavailable, or simply not represented. | Introduce honest integration-state summaries for n8n, RAGcore, and MCP Hub. Keep technical delivery data behind disclosure while preserving the working retry control. |
|
||||
| P1 | Audit | Snake-case action names, actor types, correlation fragments, and entity types appear without human explanation or record links. Metadata and before/after context are inaccessible even though the API supplies metadata. | Present a timeline/table hybrid with readable action labels, actor/source badges, entity context, and expandable technical evidence. |
|
||||
| P1 | Accessibility, vehicle tabs, merge confirmation | Vehicle tabs expose tab roles but do not implement arrow-key behaviour or panel relationships. The merge confirmation does not move/fence focus. There is no skip link. | Implement complete keyboard patterns, visible focus, labelled panels/dialogs, focus restoration, and a skip-to-content control. |
|
||||
| P2 | Global visual system | Almost every section is a white rounded rectangle on a pale blue canvas. Repeated cards, pills, and borders flatten hierarchy and feel template-derived. | Establish a neutral surface hierarchy, stronger typographic rhythm, restrained radius/elevation, and operational separators instead of nested cards. |
|
||||
| P2 | Login | The entry screen is legible but mostly empty space and two identical full-width buttons. It communicates neither how MobilityOps works nor a memorable product idea. | Pair immediate role entry with a lightweight fleet/event/control-centre SVG illustration and concise trust cues. |
|
||||
| P2 | Fleet | Location filtering required by the original UX specification is absent. Attention is a text phrase with no indication of the underlying issue or next booking. | Add a local location filter from returned data, an issue marker, service distance, and direct row affordance. |
|
||||
| P2 | Vehicle detail | Overview fields are six equal mini-cards; history is split across tabs with no persistent operational summary. Tabs are title-cased internal nouns rather than task-oriented context. | Keep a stable vehicle status/readiness header, consolidate facts, and use a timeline/detail workspace with clear empty states. |
|
||||
| P2 | Booking detail | Customer, vehicle, dates, odometers, and readiness are equal-weight definition cards. Required action and current progress are not evident. | Create a booking journey header, readiness checklist, contextual entities, and a high-emphasis return action only when applicable. |
|
||||
| P2 | Data Quality overview | Rule identifiers are mechanically converted from snake case. There is no category explanation, likely cause, suggested action, age, or confidence/evidence cue. | Use human category names, severity and age, evidence summaries, and a review queue optimized for triage. |
|
||||
| P2 | Knowledge | Source citation is the strongest current pattern, but `Provider: demo`, tenant concepts, and indexed-count language feel administrative. Retrieval is visually indistinguishable from normal form submission. | Keep sources primary; show a compact question → procedures → answer retrieval model and translate provider state into trusted business language with optional technical detail. |
|
||||
| P2 | Loading, empty, error states | Most routes render plain text paragraphs. Errors erase page context; lists offer generic empty copy; no skeletons or recovery actions exist. | Normalize skeleton, inline error, empty state, and retry patterns while retaining semantic live regions. |
|
||||
| P2 | Status system | Statuses are lower-case API values. Several operationally distinct states share the same neutral treatment; meaning is often conveyed mainly by colour. | Map every status to a human label, semantic icon/marker, and accessible tone using one shared status taxonomy. |
|
||||
| P2 | Performance | Large booking and vehicle result sets render eagerly; all routes ship in one bundle. There is no pagination API, but the UI can still reduce initial work and split route code. | Introduce route-level lazy loading and efficient presentational rendering; avoid animation dependencies and heavy imagery. |
|
||||
| P3 | Language and formatting | Copy alternates between business language and implementation terminology (`vehicle.returned.v1`, `demo_login`, `tombstone`). Date formatting is correct but lacks relative context. | Provide human labels first, technical identifiers second, and consistent Brussels date/time helpers. |
|
||||
|
||||
## What already works and must be preserved
|
||||
|
||||
- Semantic headings, labelled form controls, real links, table headers, visible focus outline,
|
||||
and non-colour badge text provide a sound accessibility baseline.
|
||||
- Every visible metric and list row is backed by persisted API data.
|
||||
- The synthetic-demo disclosure is persistent and clear.
|
||||
- The return, duplicate merge, retry, filter, login, and knowledge interactions are real.
|
||||
- Knowledge answers foreground real source title, version, section, and excerpt.
|
||||
- RAGcore unavailability and n8n failure do not break core operations.
|
||||
- The interface avoids horizontal page overflow down to 390 px, even though mobile
|
||||
information structure needs substantial improvement.
|
||||
|
||||
## Selected design problem statement
|
||||
|
||||
MobilityOps needs to move from “working pages arranged around API resources” to “a calm
|
||||
operational control centre arranged around readiness, attention, evidence, and next
|
||||
actions.” The redesign should be distinctive through disciplined information design,
|
||||
typography, a fleet-status visual language, and small operational illustrations—not
|
||||
through decorative gradients, generic charts, or excessive motion.
|
||||
|
||||
## Direction for Stitch exploration
|
||||
|
||||
The three Stitch directions must each solve the same full product while deliberately
|
||||
varying navigation, density, rhythm, and status representation:
|
||||
|
||||
1. **Control Rail:** compact persistent rail, high-density operational workspace,
|
||||
readiness band and timeline.
|
||||
2. **Dispatch Ledger:** editorial typography, ledger-like grouped lists, calmer top
|
||||
navigation and strong temporal rhythm.
|
||||
3. **Service Atelier:** calm editorial service workspace, narrative timelines,
|
||||
humanized exceptions, and progressive disclosure.
|
||||
|
||||
The final choice must favour business credibility and workflow clarity over screenshot
|
||||
novelty. No implementation begins until the Stitch comparison and decision are recorded.
|
||||
@@ -1,80 +0,0 @@
|
||||
# Premium UI implementation validation
|
||||
|
||||
Date: 2026-08-02
|
||||
Branch: `design/mobilityops-premium-ui`
|
||||
|
||||
## Implemented scope
|
||||
|
||||
- New responsive Control Rail shell, icon system, product mark, top bar, desktop rail,
|
||||
off-canvas navigation and labelled mobile bottom navigation.
|
||||
- Redesigned login, dashboard, fleet, vehicle, bookings, booking return, data quality,
|
||||
duplicate review, knowledge, integrations and audit surfaces.
|
||||
- Return capture now has an explicit review step; no return API request is sent before
|
||||
confirmation. The existing idempotency key and local-commit/outbox behavior remain.
|
||||
- Booking rows are limited to 25 per client-side page; mobile tables keep explicit field
|
||||
labels through `data-label` semantics.
|
||||
- Loading, empty and error surfaces, focus treatment, reduced-motion handling and honest
|
||||
integration states are shared system behaviors.
|
||||
|
||||
## Visual evidence
|
||||
|
||||
Desktop:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Mobile:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
The complete capture set is in `artifacts/design-validation/implementation/`.
|
||||
|
||||
Deployed review captures taken from `http://192.168.10.150:1236` are stored under
|
||||
`artifacts/design-validation/implementation/deployed/`. The deployed smoke covered login,
|
||||
dashboard, fleet list/detail, bookings list/active return, quality list/duplicate review,
|
||||
knowledge, integrations and audit at both desktop and mobile sizes.
|
||||
|
||||
## Responsive evidence
|
||||
|
||||
Browser evaluation measured the document element after authenticated page render:
|
||||
|
||||
| Width | Client width | Scroll width | Result |
|
||||
|---:|---:|---:|---|
|
||||
| 390 | 375 (vertical scrollbar present) | 375 | no horizontal overflow |
|
||||
| 768 | 753 (vertical scrollbar present) | 753 | no horizontal overflow |
|
||||
| 1280 | 1265 (vertical scrollbar present) | 1265 | no horizontal overflow |
|
||||
| 1440 | 1425 (vertical scrollbar present) | 1425 | no horizontal overflow |
|
||||
|
||||
The same widths are automated in `frontend/e2e/ui-redesign.spec.ts`.
|
||||
|
||||
## Deployed smoke result
|
||||
|
||||
- All ten authenticated desktop routes rendered their expected page heading with no
|
||||
visible alert state and no horizontal overflow.
|
||||
- The same ten routes plus login rendered at 390 px with no alert state and
|
||||
`scrollWidth === clientWidth`.
|
||||
- Browser console: zero error or warning entries after the full route smoke.
|
||||
- Authenticated network verification through the deployed web proxy returned HTTP 200
|
||||
for dashboard, vehicles, active booking, open quality issues, knowledge status,
|
||||
workflows and audit.
|
||||
- Container verification: PostgreSQL and API healthy, web HTTP 200, and the server's
|
||||
existing n8n on port 5678 returned healthy. No second MobilityOps n8n container is
|
||||
running. A synthetic return completed the shared-n8n callback on attempt 1 and the demo
|
||||
was reset to its deterministic start state.
|
||||
|
||||
## Performance choices
|
||||
|
||||
- No image runtime dependency and no animation library were added.
|
||||
- Branding, navigation and workflow illustrations are inline SVG/CSS.
|
||||
- Inter is used only when locally available; there is no blocking font request.
|
||||
- The production build remains a single small PoC bundle (approximately 239 kB JS and
|
||||
37 kB CSS uncompressed at implementation time).
|
||||
@@ -1,152 +0,0 @@
|
||||
# Current-state audit — Fleet Ops final integrations
|
||||
|
||||
Date: 2026-08-05. Compiled from direct repository inspection (git log/status/diff across
|
||||
all three repos), `PROJECT_STATE.md` history, and read-only investigation of the sibling
|
||||
repos' own state docs. No live server SSH/curl evidence is included in this pass yet —
|
||||
see `integration-release-state.md` for the live-verification checklist as it is executed.
|
||||
|
||||
## Repository revisions at audit time
|
||||
|
||||
| Repo | Path | Branch | HEAD | Notes |
|
||||
|---|---|---|---|---|
|
||||
| Fleet Ops (MobilityOps) | `C:\Projects\MobilityOps` | `feat/fleet-ops-final-integrations` (new, branched from `feat/live-n8n-ragcore-integration`) | `3ebca9e` | `feat/live-n8n-ragcore-integration` was pushed to `origin` at `0571a40` and deployed live; `3ebca9e` (logo rebrand) is one commit ahead, not yet deployed. `master` is 19 commits behind and stale (localization-round only). |
|
||||
| RAGcore | `C:\Projects\RAGcore` | `main` | `64a908a` | Up to date with `origin/main`. Uncommitted local work in progress (see below) — not Fleet-Ops-related, left untouched. |
|
||||
| ITWorx MCP Hub | `C:\Projects\ITWorx_MCP_Hub` | `feature/wp240-final-acceptance` | `26e6bd8` (+ later `75bb16a`) | Contains `f107544` (MobilityOps connector) as a direct ancestor, plus a real contract fix (`96de385`, vehicleRef camelCase). Already deployed live to Tower at `c4a0f6d`. |
|
||||
|
||||
## Branch-name correction (recorded assumption)
|
||||
|
||||
The task brief names the working branch `feat/fleet-ops-final-integrations` as already
|
||||
selected and "branched from the most recently validated, localized, deployed master
|
||||
branch." That literal branch did not exist. `master` is in fact stale (19 commits behind,
|
||||
last touched for a localization round only) — the actually-validated, deployed line of
|
||||
work is `feat/live-n8n-ragcore-integration` (pushed to `origin`, deployed to
|
||||
`http://192.168.10.150:1236` at `0571a40`, one commit behind current HEAD). Created
|
||||
`feat/fleet-ops-final-integrations` from that branch's HEAD (`3ebca9e`) instead of from
|
||||
`master`, since that satisfies the actual intent (continue from the validated/deployed
|
||||
line) even though the literal branch name in the brief was inaccurate.
|
||||
|
||||
## What is actually already done (contradicts "not yet live" framing in places)
|
||||
|
||||
- **n8n**: 3 of 4 canonical workflows are live and active in the shared instance
|
||||
(`n8n.itworx.tech`): Vehicle Return Orchestration, Scheduled Data Quality Scan,
|
||||
Workflow Error Handler. The 4th, RAGcore Procedure Sync, has all 6 nodes built and
|
||||
saved but is **not published** (deliberately left for an explicit activation decision,
|
||||
since publishing starts real unattended daily runs against production). The root cause
|
||||
of an earlier "auth"-looking failure (`N8N_PROXY_HOPS=0` behind the TLS-terminating
|
||||
reverse proxy, breaking the browserId CSRF check on every mutating REST call) was found
|
||||
and fixed at the infrastructure level (Unraid template), not worked around.
|
||||
- **RAGcore**: deployed to `http://192.168.10.150:1237`, application wiring for
|
||||
search/context/answer is real (commit `a2905cc`, confirmed present in RAGcore's own
|
||||
history at `13 commits behind HEAD`). `KNOWLEDGE_PROVIDER` is still `demo` in Fleet Ops
|
||||
because real queries against the "Fleet Ops Procedures" space return **zero dense and
|
||||
zero sparse candidates** at the raw retrieval stage — confirmed not a Fleet-Ops-side
|
||||
wiring bug (RAGcore's own trusted Query Lab tool reproduces the identical zero-candidate
|
||||
result against the same space). Root cause not yet found as of this audit; ruled out so
|
||||
far: point count/scoping (83 published, correctly scoped), embedding digest mismatch
|
||||
(matches), collection alias resolution (resolves correctly). One separate, confirmed,
|
||||
pre-existing bug: `_DEFAULT_LANGUAGE = "en"` is hardcoded in RAGcore's ingestion handler
|
||||
— every chunk is stamped `language: "en"` regardless of actual content; RAGcore has never
|
||||
done real language detection. Not the cause of zero candidates, but must be fixed for
|
||||
trilingual retrieval (task 6A) once the space is answerable at all.
|
||||
- **MCP Hub**: the Fleet Ops read-only connector (4 tools, `mobilityops.*`) is **already
|
||||
live in production** on Tower (commit `c4a0f6d`), reachable via `fleetops.itworx.tech`,
|
||||
end-to-end verified once already per the Hub's own `CLAUDE.md`/`BUILD_STATE.json`. A
|
||||
real contract bug was found and fixed there (`vehicle.get`'s input schema disagreed with
|
||||
the actual wire parameter name — `vehicle_ref` vs `vehicleRef`). What is **not** yet done:
|
||||
the Hub's own formal production-acceptance checklist row for MobilityOps (`CON-P04`) has
|
||||
not been executed, and Fleet Ops's own `MCP_HUB_REGISTRATION_ENABLED`/base-URL
|
||||
configuration has not been confirmed as actually wired and flipped on from the Fleet Ops
|
||||
side (open item for this audit's Batch 4).
|
||||
|
||||
## Confirmed contradictions to resolve (task section 3)
|
||||
|
||||
- "Twee versus vier n8n-workflows": resolved above — 3 active + 1 built-but-unpublished.
|
||||
Canonical set is 4; only 3 are live.
|
||||
- "Demo-provider versus live RAGcore": Fleet Ops is still on the demo knowledge provider
|
||||
by deliberate, documented decision (not an oversight) pending the retrieval root cause.
|
||||
- "MCP Hub-status": prior Fleet Ops docs (`.env.example`, `MCP_HUB_REGISTRATION_ENABLED`)
|
||||
predate the Hub-side deployment and need reconciling against the fact that the connector
|
||||
is already live on the Hub side.
|
||||
- Repo hygiene: removed an untracked, empty `backend;C` directory and an untracked 31 MB
|
||||
`MobilityOps.zip` stray export; added `*.zip`/`*.tar.gz` to `.gitignore`. No accidentally
|
||||
committed `__pycache__`/`.pytest_cache`/`test-results` were found in git history.
|
||||
|
||||
## RAGcore retrieval root cause — found and partially fixed (2026-08-05, this session)
|
||||
|
||||
Investigated live against production (`192.168.10.150`, containers `ragcore-app-1`,
|
||||
`ragcore-qdrant-1`, `ragcore-postgres-1`, `ollama`), read-only first, then two approved
|
||||
live changes.
|
||||
|
||||
**Root cause #1 (FIXED): filesystem permission bug, not authorization/data.** Verified,
|
||||
in order, that every earlier suspect was actually healthy: the `control.grants` row
|
||||
(active, `editor` role, correct application/space), the real
|
||||
`ControlPlaneAuthorizationInputsProvider` + `RetrievalAuthorizationService.resolve()` code
|
||||
path run in-process against the live DB (resolves a non-empty `effective_space_ids`), the
|
||||
exact production Qdrant filter run directly against the live collection (returns real
|
||||
matching points), and a real ANN vector query under that filter (real hits, sensible
|
||||
scores). The actual break: `/workspace/.state/models/embedding_profiles.json` — the file
|
||||
`RetrievalPipeline.run()` reads on every single query to resolve the active embedding
|
||||
profile — was owned by container-side `root:root` mode `600` on the bind-mounted
|
||||
`/mnt/cache/appdata/ragcore/state/models` host path, while the real running app process
|
||||
is uid 10001 (`ragcore`). Every retrieval call hit a `PermissionError` reading its own
|
||||
state file before ever reaching Qdrant — a plain filesystem-ownership bug, invisible to
|
||||
every DB/Qdrant-level check. **Fixed live**: `chown 10001:10001` +
|
||||
`chmod 644`/`755` on that file/directory (approved by the user beforehand). Re-verified
|
||||
in-process: `RetrievalPipeline.run()` now returns 5 real, relevant hits for an English
|
||||
damage-procedure question (previously 0).
|
||||
|
||||
**Root cause #2 (found, NOT fixed — needs a design decision): reranking is
|
||||
architecturally unavailable.** `DEFAULT_RERANKER_PROFILE.model_identifier` is
|
||||
`bge-reranker-v2-m3:v1`, which was never actually present in Ollama's model list (0 of 14
|
||||
installed models matched). With the user's approval, pulled a working GGUF
|
||||
(`xitao/bge-reranker-v2-m3:latest`, 1.2 GB) into the shared Ollama instance. **This did
|
||||
not fix reranking**: `OllamaRerankAdapter` posts to `{ollama_base_url}/api/rerank`, and
|
||||
this Ollama server (version `0.32.5`) returns a plain `404` for that route — it has no
|
||||
rerank endpoint at all. This is not a missing-model problem, it is that RAGcore's
|
||||
reranker adapter was built against an Ollama HTTP API that does not exist in the deployed
|
||||
version (matches the code's own comment that no reranker-profile registry or live
|
||||
validation existed yet). The retrieval pipeline degrades gracefully on rerank failure
|
||||
(RRF-fusion-only hits still returned, confirmed above), but the `/v1/answers` endpoint's
|
||||
answerability classifier still returns `not_answerable`/0 citations for real NL/EN/FR
|
||||
questions against real matching content, live-verified after fix #1 with a freshly
|
||||
minted, correctly-scoped credential.
|
||||
|
||||
Options for #2, not decided yet: (a) find/confirm whether a newer Ollama version adds a
|
||||
real `/api/rerank` route and upgrade the shared instance (affects every other project on
|
||||
this Ollama — needs its own explicit approval and blast-radius review); (b) change
|
||||
RAGcore's reranker adapter to call a route Ollama actually supports (e.g. score via
|
||||
`/api/embed` + a manual similarity/cross-encoder computation, or drop the separate
|
||||
rerank step and let the answerability classifier trust RRF-fused scores) — a RAGcore
|
||||
code/design change, out of Fleet Ops's own mandate to decide unilaterally; (c) leave
|
||||
`KNOWLEDGE_PROVIDER=demo` until RAGcore's own team/session resolves this.
|
||||
|
||||
**Side effect to flag**: minting the live-verification credential used `rotate=True` on
|
||||
the existing "Fleet Ops Knowledge Assistant (production)" service account (a second
|
||||
credential would have exceeded RAGcore's own 2-active-credential cap), which invalidates
|
||||
whatever token was previously issued for that account. Since Fleet Ops is still on
|
||||
`KNOWLEDGE_PROVIDER=demo`, this has no live user-facing impact today, but a fresh
|
||||
credential must be issued and wired into Fleet Ops's `RAGCORE_API_TOKEN` at actual
|
||||
cutover time — do not assume the old one still works.
|
||||
|
||||
**Concurrency note**: `C:\Projects\RAGcore` had substantial uncommitted local changes
|
||||
from what appears to be a different, actively-running session (36 modified/untracked
|
||||
files by the end of this investigation, including files this investigation also read).
|
||||
No commits or file edits were made in that checkout this session precisely because of
|
||||
that collision risk — the two live fixes above were applied directly to the running
|
||||
containers/Ollama instance (approved), not to the RAGcore git repository. **Follow-up
|
||||
required**: once the concurrent session's work lands, the reranker-profile fix (whichever
|
||||
option above is chosen) still needs an actual code change + commit + redeploy in
|
||||
`C:\Projects\RAGcore`, which was not safe to do mid-collision this session.
|
||||
|
||||
## Minimal remaining implementation order
|
||||
|
||||
1. Root-cause the RAGcore zero-candidate retrieval bug (blocks flipping `KNOWLEDGE_PROVIDER`
|
||||
and blocks the trilingual live-acceptance and AI Operations Brief tasks).
|
||||
2. Fix RAGcore's hardcoded `language: "en"` chunk metadata for trilingual retrieval.
|
||||
3. Decide on and execute n8n workflow 3 publication, with live no-op-on-rerun verification.
|
||||
4. Confirm/complete Fleet Ops-side MCP Hub registration wiring and run the Hub's own
|
||||
CON-P04 acceptance row.
|
||||
5. Build the AI Operations Brief runbook once RAGcore and MCP Hub are both live-green.
|
||||
6. GUI polish batch (dashboard Today/Attention presentation, duplicate-merge presentation,
|
||||
About Demo scannability, Demo Guide completion state).
|
||||
7. Final regression gates and evidence write-up.
|
||||
@@ -1,187 +0,0 @@
|
||||
# Current functional audit
|
||||
|
||||
Performed 2026-08-02 against source `feat/mobilityops-functional-completion` (branched from
|
||||
`design/mobilityops-premium-ui` @ `54dc952`) and the live Unraid deployment at
|
||||
`http://192.168.10.150:1236` (same revision — see `server-baseline.md`). Findings below are
|
||||
either reproduced directly (curl against the live server, or reading the exact source lines)
|
||||
or are structural gaps confirmed against the task's own explicit requirements. Items already
|
||||
on record as accepted PoC tradeoffs (`docs/deferred.md`, `docs/01-scope-and-non-goals.md`,
|
||||
`PROJECT_STATE.md` known-limitations) are excluded — this file only lists items that are
|
||||
genuinely open.
|
||||
|
||||
## Method
|
||||
|
||||
- Full read of `backend/app/api/routers/*.py`, `backend/app/api/deps.py`,
|
||||
`backend/app/services/returns.py`, `backend/app/models/*.py`,
|
||||
`frontend/src/pages/*.tsx`, `frontend/src/components/Layout.tsx`,
|
||||
`frontend/src/context/AuthContext.tsx`, `frontend/src/App.tsx`.
|
||||
- Live curl verification against `http://192.168.10.150:1236` for the auth/role findings.
|
||||
- Cross-checked every finding against `docs/deferred.md`,
|
||||
`artifacts/final-acceptance/summary.md`, and the latest `PROJECT_STATE.md` sections to
|
||||
avoid re-flagging already-accepted tradeoffs.
|
||||
|
||||
## Findings
|
||||
|
||||
### F1 — Vehicles page renders the unfiltered array (client-side search is inert)
|
||||
|
||||
- Severity: high. Role: both. Route: `/vehicles`. Component:
|
||||
`frontend/src/pages/Vehicles.tsx`.
|
||||
- Repro: type any text into the "Search" box on the Vehicles page. The row count label
|
||||
(`{filtered.length} vehicles`) updates and the empty state correctly appears when nothing
|
||||
matches, but the `<tbody>` mapped over the raw `vehicles` array, not the computed
|
||||
`filtered` array — every original row stayed visible regardless of the search text.
|
||||
- Expected: only rows matching the search (combined with the status/attention filters)
|
||||
render.
|
||||
- Cause: `vehicles.map(...)` at the table body instead of `filtered.map(...)` (line 78 as
|
||||
originally read).
|
||||
- Reproduces locally: yes (read). Reproduces on Unraid: yes (identical bundled source,
|
||||
`source-revision` matches).
|
||||
- Fix: **applied** — render body now maps `filtered`. Regression test:
|
||||
`frontend/e2e/*.spec.ts` search-changes-rows case (added in Batch 1).
|
||||
|
||||
### F2 — Bookings page renders the unfiltered, unpaginated array
|
||||
|
||||
- Severity: high. Role: both. Route: `/bookings`. Component:
|
||||
`frontend/src/pages/Bookings.tsx`.
|
||||
- Repro: same class of bug — `visible` (filtered + sliced to 25/page) was computed and used
|
||||
for the meta line and pagination controls, but `<tbody>` mapped over the raw `bookings`
|
||||
array. All bookings rendered on every page regardless of filter or page number.
|
||||
- Cause: `bookings.map(...)` instead of `visible.map(...)`.
|
||||
- Fix: **applied** — render body now maps `visible`. Added a page-clamp effect so `page`
|
||||
cannot point past the last valid page when the filtered set shrinks (e.g. after a status
|
||||
filter reload returns fewer results than the current page implies).
|
||||
- Reproduces locally/Unraid: yes/yes.
|
||||
|
||||
### F3 — Session state is `sessionStorage`-authoritative, not server-verified
|
||||
|
||||
- Severity: high. Role: both. Component: `frontend/src/context/AuthContext.tsx`.
|
||||
- The logged-in `user` object is read from and written to `sessionStorage`
|
||||
(`mobilityops.demo-user`) directly; there is no call on app start to verify the HttpOnly
|
||||
session cookie is still valid, and `logout()` only clears local state — it never calls the
|
||||
server to invalidate the cookie. A stale/edited `sessionStorage` entry (or a cookie that
|
||||
expired server-side) will keep protected pages rendering as if authenticated until the
|
||||
first API call 401s, and even then nothing centrally redirects to `/login` (`isSessionExpired`
|
||||
helper exists in the same file but is never imported/called anywhere).
|
||||
- Live confirmation: no `GET /api/v1/demo/session` or `POST /api/v1/demo/logout` endpoint
|
||||
exists yet (`curl` returns 404 for both against the live server).
|
||||
- Fix: Batch 1 — add both endpoints server-side, make `AuthProvider` verify against
|
||||
`GET /demo/session` on load, call `POST /demo/logout` on sign-out, and centrally react to
|
||||
401s from the `api` client.
|
||||
|
||||
### F4 — Data-quality workbench has no role gate at all (list, detail, defer, reject)
|
||||
|
||||
- Severity: high. Role: Rental Employee. Routes: `/data-quality`, `/data-quality/:ref`.
|
||||
Endpoints: `GET /api/v1/data-quality/issues`, `GET /api/v1/data-quality/issues/{ref}`,
|
||||
`POST .../defer`, `POST .../reject`.
|
||||
- Live confirmation: logged in as `rental_employee` on the live server, `GET
|
||||
/api/v1/data-quality/issues` and `GET /api/v1/audit` both returned `200` (curl evidence
|
||||
above). Only `merge-customers` and `scan` are gated to Operations Manager; `defer`/`reject`
|
||||
are not, and the whole workbench is reachable and actionable by Rental Employee both via
|
||||
direct API call and via the UI (`DataQuality.tsx` has no role check at all; `Layout.tsx`
|
||||
shows the "Data quality" and "Audit trail" nav items unconditionally to both roles).
|
||||
- The task's role matrix (this brief, section 4) puts data-quality and audit entirely under
|
||||
Operations Manager — Rental Employee's list is dashboard/fleet/vehicle
|
||||
detail/bookings/booking detail/return/knowledge only. Decision recorded: tighten
|
||||
`list_issues`, `get_issue`, `defer`, `reject`, and `GET /api/v1/audit` to
|
||||
`require_operations_manager`; hide the nav items and show the same restricted-message
|
||||
pattern already used by `Automation.tsx` for direct URL access by Rental Employee.
|
||||
- Fix: Batch 1.
|
||||
|
||||
### F5 — Return preview does not exist; the review step (if any) cannot be authoritative
|
||||
|
||||
- Severity: high. Section 5 requirement. No `POST
|
||||
/api/v1/bookings/{public_ref}/return-preview` (or equivalent) endpoint exists anywhere in
|
||||
`backend/app/api/routers/bookings.py`. The frontend return flow can therefore only ever
|
||||
show a client-guessed preview, or skip a real preview step entirely.
|
||||
- Fix: Batch 2 — one authoritative evaluation function shared by preview (no writes) and
|
||||
commit.
|
||||
|
||||
### F6 — Audit API never exposes `before_json`/`after_json`
|
||||
|
||||
- Severity: medium. `AuditEvent` (`backend/app/models/audit.py`) stores `before_json` and
|
||||
`after_json`, populated by `record_audit_event` call sites (e.g. `return_registered`,
|
||||
`vehicle_status_changed`), but `AuditEventOut` (`backend/app/schemas.py`) and the router
|
||||
(`backend/app/api/routers/audit.py`) only ever return `metadata`, never before/after. The
|
||||
UI (`Audit.tsx`) therefore cannot show what changed, only that something happened.
|
||||
- Fix: Batch 2 — add `before`/`after` to `AuditEventOut`, resolve a safe entity link where
|
||||
possible, render human-readable before/after in the UI behind progressive disclosure.
|
||||
|
||||
### F7 — Data-quality issue evidence is a raw JSON dump for 4 of 5 rule types
|
||||
|
||||
- Severity: medium. `DataQualityIssueDetail.tsx`: for every rule type except
|
||||
`possible_duplicate_customer`, the only resolution surface is `<pre>{JSON.stringify(issue.evidence,
|
||||
null, 2)}</pre>` plus generic Defer/Reject buttons. `missing_required_field`,
|
||||
`odometer_regression`, `booking_overlap`, and `vehicle_status_conflict` have no typed,
|
||||
bounded resolution flow at all.
|
||||
- Fix: Batch 3.
|
||||
|
||||
### F8 — Related-snapshot typing is inferred from rule type, not explicit
|
||||
|
||||
- Severity: low. `backend/app/api/routers/data_quality.py::get_issue` infers
|
||||
`related_entity_type = "customer" if issue.rule_type == "possible_duplicate_customer" else
|
||||
"vehicle"` — a `booking_overlap` issue's related entity is actually a booking, not a
|
||||
vehicle, so its snapshot lookup silently returns `None` today. Confirmed by reading
|
||||
`_snapshot()`, which only knows how to look up `customer` or `vehicle` rows.
|
||||
- Fix: Batch 3 — typed snapshots for customer/vehicle/booking/inspection.
|
||||
|
||||
### F9 — Global search is a blind client-side regex guesser
|
||||
|
||||
- Severity: medium. `frontend/src/components/Layout.tsx::handleSearch` pattern-matches
|
||||
`MO-…`/`BK-…`/`DQ-…` and navigates without checking the entity exists, or fuzzy-matches a
|
||||
hardcoded `SEARCH_DESTINATIONS` term list. No backend search endpoint exists. No results
|
||||
panel, no keyboard navigation within results, no role filtering, no debounced live query —
|
||||
it is a single-shot form submit.
|
||||
- Fix: Batch 4 — `GET /api/v1/search`.
|
||||
|
||||
### F10 — n8n integration status is derived from the single most recent outbox event
|
||||
|
||||
- Severity: medium. `Automation.tsx` line: `<StatusBadge status={runs?.[0]?.status ??
|
||||
"no_events"} />` — the "n8n delivery" health card shows whichever status the most recent
|
||||
event happens to be in, not an aggregate of pending/delivering/failed/succeeded counts or
|
||||
dispatcher-enabled state. A single old failed event sitting behind 40 succeeded ones would
|
||||
misreport health; a single lucky success would hide an otherwise-failing dispatcher.
|
||||
- Fix: Batch 4 — truthful aggregate integration-status endpoint.
|
||||
|
||||
### F11 — Stale `delivering` outbox events have no lease/recovery
|
||||
|
||||
- Severity: medium. `backend/app/services/dispatcher.py`: `_claim_due_events` flips rows to
|
||||
`delivering` and commits before the HTTP call; if the process is killed between that commit
|
||||
and the outcome-recording transaction, the row stays `delivering` forever with no timeout
|
||||
or reclaim sweep. Not documented anywhere as an accepted limitation.
|
||||
- Fix: Batch 5 — bounded delivery lease + stale-recovery sweep.
|
||||
|
||||
### F12 — No UI trigger for demo reset
|
||||
|
||||
- Severity: low. `POST /api/v1/demo/reset` exists and is Operations-Manager-gated
|
||||
server-side, but no page exposes a "Run demo reset" action; it can currently only be
|
||||
invoked directly against the API.
|
||||
- Fix: Batch 4.
|
||||
|
||||
### F13 — No UI trigger for manual data-quality scan
|
||||
|
||||
- Severity: low. `POST /api/v1/data-quality/scan` exists and is OM-gated, but
|
||||
`DataQuality.tsx` has no "Run quality scan" button.
|
||||
- Fix: Batch 3.
|
||||
|
||||
### F14 — Second n8n workflow (scheduled quality scan) not present
|
||||
|
||||
- Severity: low, explicitly requested in this brief (section 10C). Only
|
||||
`n8n/mobilityops-return-processing.json` exists.
|
||||
- Fix: Batch 5.
|
||||
|
||||
## Not re-flagged (already-accepted, on record)
|
||||
|
||||
RAGcore/MCP Hub never live-round-tripped (environment limitation, honestly degraded);
|
||||
`Inspection.public_ref` sequence not gap-safe under concurrency; DQ idempotency key
|
||||
simplified from the doc's literal fingerprint scheme (reasoned, documented deviation); no
|
||||
optimistic `version` check on return requests (row-locking already provides the required
|
||||
concurrency safety — the doc's "optimistic version where relevant" was not adopted, and nothing in
|
||||
this brief requires adding one on top of working pessimistic locking); demo auth is an
|
||||
HMAC-cookie PoC mechanism, not a production IdP; n8n owner-account bootstrap remains a
|
||||
one-time manual step (n8n 2.x product behavior).
|
||||
|
||||
## Next step
|
||||
|
||||
Fix F1–F4 now (Batch 1, in progress), continue through F5–F14 in the batches recorded in
|
||||
`PROJECT_STATE.md` / the task list, deploying and re-verifying against Unraid after each
|
||||
batch per the brief's server-first loop.
|
||||
@@ -1,111 +0,0 @@
|
||||
# Unraid server baseline (functional-completion audit)
|
||||
|
||||
Captured 2026-08-02 before any functional-completion changes, via `ssh unraid` (alias in
|
||||
`~/.ssh/config`, key `itworx_unraid_deploy`; the `widefrog_unraid_deploy` identity named in
|
||||
the task brief does not exist locally — the working alias/key was used instead, no new key
|
||||
was created).
|
||||
|
||||
## SSH access
|
||||
|
||||
```bash
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 unraid "hostname && docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||
```
|
||||
|
||||
Result: reachable, host `Tower`. `widefrog_unraid_deploy` is not a valid identity path on
|
||||
this workstation (`~/.ssh/widefrog_unraid_deploy` does not exist); the pre-existing
|
||||
`unraid` SSH config alias (`itworx_unraid_deploy` key, root@192.168.10.150:22) was used
|
||||
instead and is fully functional. No key was created, copied, or replaced.
|
||||
|
||||
## Deployed containers (Compose project `mobilityops`)
|
||||
|
||||
| Container | Image | Status | Health | Restarts | Host port |
|
||||
|---|---|---|---|---|---|
|
||||
| `mobilityops-web-1` | `mobilityops-web` | Up | healthy | 0 | `1236:80` |
|
||||
| `mobilityops-api-1` | `mobilityops-api` | Up | healthy | 0 | none (compose-network only) |
|
||||
| `mobilityops-db-1` | `postgres:16-alpine` | Up | healthy | 0 | none (compose-network only) |
|
||||
|
||||
Shared host `n8n` container (outside the `mobilityops` Compose project, pre-existing
|
||||
infrastructure): Up, healthy, `5678:5678`, on the default `bridge` network (not on
|
||||
`mobilityops_mobilityops`).
|
||||
|
||||
Deployment directory: `/mnt/user/appdata/mobilityops` (matches `docs/17-runbook.md` /
|
||||
`deploy/unraid/README.md`).
|
||||
|
||||
## Deployed revision
|
||||
|
||||
`.deploy/source-revision` on the server contains:
|
||||
|
||||
```
|
||||
54dc952915a4874fcdf14781e1c37feb0e253851
|
||||
```
|
||||
|
||||
This matches the local `design/mobilityops-premium-ui` HEAD (`54dc952`) exactly — the
|
||||
server is running the last committed premium-design revision, confirming the externally
|
||||
observed branch/revision. `.deploy/` also retains prior archives
|
||||
(`source-a737860.tar.gz`, `source-686b62f.tar.gz`, `source.tar.gz`) for rollback.
|
||||
|
||||
## Migrations
|
||||
|
||||
```bash
|
||||
docker compose -p mobilityops exec -T api python -m alembic current
|
||||
# e7b08389f47f (head)
|
||||
```
|
||||
|
||||
Matches the local repository's Alembic head (`backend/alembic/versions/`:
|
||||
`c9498525abb5_initial_schema.py` → `e7b08389f47f_idempotency_records.py`). No drift.
|
||||
|
||||
## Volumes and network
|
||||
|
||||
- `mobilityops_mobilityops-db` (Postgres data, named/persistent)
|
||||
- `mobilityops_mobilityops-n8n` (retained from an earlier bundled-n8n attempt; unused now
|
||||
that the shared host `n8n` is reused — see `compose.unraid.yaml` `bundled-n8n` profile)
|
||||
- Network: `mobilityops_mobilityops` (bridge)
|
||||
|
||||
## Environment variable names present on the server `.env`
|
||||
|
||||
(names only — no values inspected or printed)
|
||||
|
||||
`APP_SECRET`, `COMPOSE_PROJECT_NAME`, `DATABASE_URL`, `DEMO_TODAY`, `KNOWLEDGE_PROVIDER`,
|
||||
`MCP_HUB_BASE_URL`, `MCP_HUB_REGISTRATION_ENABLED`, `MCP_HUB_SERVICE_TOKEN`,
|
||||
`MCP_PROVIDER_ID`, `MOBILITYOPS_API_URL`, `MOBILITYOPS_CALLBACK_TOKEN`,
|
||||
`MOBILITYOPS_DEMO_MODE`, `MOBILITYOPS_ENV`, `MOBILITYOPS_PUBLIC_URL`, `N8N_BASE_URL`,
|
||||
`N8N_BASIC_AUTH_ACTIVE`, `N8N_BASIC_AUTH_PASSWORD`, `N8N_BASIC_AUTH_USER`,
|
||||
`N8N_ENCRYPTION_KEY`, `N8N_OWNER_EMAIL`, `N8N_OWNER_PASSWORD`, `N8N_WEBHOOK_URL`,
|
||||
`POSTGRES_DB`, `POSTGRES_PASSWORD`, `POSTGRES_USER`, `RAGCORE_API_TOKEN`,
|
||||
`RAGCORE_BASE_URL`, `RAGCORE_COLLECTION`, `RAGCORE_TENANT`, `RAGCORE_WORKSPACE`, `TZ`.
|
||||
|
||||
`diff` against the committed `.env.example` variable set: no difference — the server
|
||||
`.env` was generated from the current `.env.example` with no drift in variable names.
|
||||
|
||||
## Integration configuration (as previously documented in
|
||||
`artifacts/deployment/unraid-summary.md`, re-verified live)
|
||||
|
||||
- **n8n**: shared host instance at `http://192.168.10.150:5678`, healthy, outside the
|
||||
MobilityOps Compose project. The bundled MobilityOps `n8n` service exists only behind
|
||||
the `bundled-n8n` Compose profile and is not started.
|
||||
- **RAGcore**: `KNOWLEDGE_PROVIDER=demo` — not live-connected by design; no live RAGcore
|
||||
instance exists yet.
|
||||
- **ITWorx MCP Hub**: `MCP_HUB_REGISTRATION_ENABLED=false` — provider endpoints exist and
|
||||
are independently testable, but no live Hub round trip is claimed.
|
||||
|
||||
## Application reachability
|
||||
|
||||
```bash
|
||||
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.10.150:1236/
|
||||
# 200
|
||||
```
|
||||
|
||||
## Startup log scan
|
||||
|
||||
```bash
|
||||
docker compose -p mobilityops logs --tail=200 api
|
||||
```
|
||||
|
||||
No traceback, fatal, or unresolved startup error observed — only routine `GET /health`
|
||||
polling entries from the Compose healthcheck.
|
||||
|
||||
## Configuration differences vs. the committed repository
|
||||
|
||||
None found: deployed source revision, Alembic head, and `.env.example` variable set all
|
||||
match the current `design/mobilityops-premium-ui` branch exactly. This baseline was
|
||||
captured immediately before any functional-completion work began.
|
||||
@@ -1,222 +0,0 @@
|
||||
# n8n current state (as inspected 2026-08-04)
|
||||
|
||||
Inspected live via the already-authenticated browser session at
|
||||
`https://n8n.itworx.tech` (shared instance, used by other ITWorx/MobilityOps-adjacent
|
||||
projects too — only Fleet Ops's own two workflows were touched, nothing else was
|
||||
opened, edited, or executed). No secret credential values are reproduced in this
|
||||
document.
|
||||
|
||||
## Reachability and version
|
||||
|
||||
- n8n is reachable at `https://n8n.itworx.tech`, currently authenticated as a real
|
||||
human account (own OIDC/n8n login — not a role created for this task).
|
||||
- Workspace-level stats at the time of inspection: **114 total prod. executions, 4
|
||||
failed (3.5% failure rate)**, avg run time 0.18s. (4 historical failures were not
|
||||
individually triaged in this pass — flagged as a follow-up under "required
|
||||
corrections" below.)
|
||||
- Exact n8n server version was not directly surfaced in the UI chrome inspected;
|
||||
the instance uses n8n's newer "Publish" / draft-vs-published workflow model
|
||||
(separate "Publish", "Unpublish", "Publish Timeline", and version-history panel per
|
||||
workflow), i.e. a fairly recent n8n release.
|
||||
|
||||
## Production webhook base
|
||||
|
||||
`http://192.168.10.150:5678/webhook/...` — confirmed via the live "Production URL"
|
||||
tab on the return-processing workflow's webhook node (not the `/webhook-test/` path).
|
||||
This matches `N8N_WEBHOOK_URL=http://192.168.10.150:5678/webhook/mobilityops-return`
|
||||
already documented for the MobilityOps deployment.
|
||||
|
||||
## Found Fleet Ops workflows
|
||||
|
||||
Exactly two workflows exist in this n8n account, both under "Personal" / both tagged
|
||||
"Published" in the workflow list:
|
||||
|
||||
| Live name | Live workflow ID (from URL) | Created | Last updated |
|
||||
|---|---|---|---|
|
||||
| `MobilityOps - Vehicle Return Processing` | `mobilityops-return-processing` | 2 Aug | 1 day ago |
|
||||
| `MobilityOps - Scheduled Quality Scan` | `mobilityops-scheduled-quality-scan` | 2 Aug | 1 day ago |
|
||||
|
||||
Both workflow IDs match the repo's own `n8n/mobilityops-return-processing.json` and
|
||||
`n8n/mobilityops-scheduled-quality-scan.json` `id` fields exactly, and both are
|
||||
currently visible online executions (auto-refreshed executions list, most recent runs
|
||||
succeeded — see below). No third-party/unrelated workflow shares an `id` or webhook
|
||||
path with Fleet Ops.
|
||||
|
||||
## Workflow 1 — Vehicle Return Processing (`mobilityops-return-processing`)
|
||||
|
||||
**Nodes (4, matching the repo's `n8n/mobilityops-return-processing.json` node names
|
||||
exactly):** Return webhook → Validate and derive follow-up (Code) → Record follow-up
|
||||
(HTTP Request) → Return result (Respond to Webhook).
|
||||
|
||||
- **Trigger**: webhook, `POST`, path `mobilityops-return`, production URL
|
||||
`http://192.168.10.150:5678/webhook/mobilityops-return`. **n8n-level
|
||||
Authentication is set to "None."** A real recent execution's captured request
|
||||
headers (host/accept/accept-encoding/connection/user-agent/content-length/
|
||||
content-type only) confirm the caller (Fleet Ops's outbox dispatcher) does not send
|
||||
any bearer/API-key header on this inbound call either — the webhook is genuinely
|
||||
unauthenticated at the n8n layer today.
|
||||
- **Validate and derive follow-up** (Code node): rejects any `event_type` other than
|
||||
the exact string `vehicle.returned.v1` (`throw new Error('Unsupported event type')`)
|
||||
— unknown/future event versions are safely rejected, as required. Derives
|
||||
`follow_up: 'attention_required' | 'cleaning'` from `data.attention_reasons`.
|
||||
- **Record follow-up** (HTTP Request → Fleet Ops): `POST
|
||||
http://192.168.10.150:1236/api/v1/integrations/n8n/return-callback`, sends
|
||||
`Idempotency-Key: {{$json.event_id}}` and an `X-Service-Token` header. **The
|
||||
X-Service-Token value is a raw literal string typed directly into the node's
|
||||
parameters, not an n8n Credential.** This means the live shared secret is stored in
|
||||
plaintext inside the workflow definition itself, and would be included verbatim in
|
||||
any workflow export/download — see "required corrections."
|
||||
Body: `{{JSON.stringify($json)}}`.
|
||||
- **Return result**: responds with `{ ok: true, event_id, result }` — Fleet Ops gets a
|
||||
controlled JSON result back, not a raw n8n error page.
|
||||
- **Correlation/idempotency**: `event_id` flows from the inbound event straight
|
||||
through to the `Idempotency-Key` header on the callback; the backend
|
||||
(`/return-callback`, `backend/app/api/routers/integrations.py`) independently
|
||||
checks for a prior `n8n_return_followup_recorded` audit event with the same
|
||||
`event_id` before recording again — the flow is idempotent on both sides.
|
||||
- **Latest execution**: 4 Aug, 03:34:19, succeeded in 32ms, all 4 nodes green.
|
||||
- **Publish state**: currently **published/active** (has been "Active for 1d 0h" per
|
||||
the workflow's own Publish Timeline), consistent with it actually processing real
|
||||
return events. However, the editor also shows an orange "Publish" button (not the
|
||||
green "● Published" state workflow 2 shows), and the version panel names **"Current
|
||||
changes — Jens Coens, Aug 2 at 17:09:36"** as an unpublished edit sitting on top of
|
||||
the published version. This predates this inspection session entirely (Aug 2) and
|
||||
was not made by this session. The diff content itself is not visible without
|
||||
upgrading the n8n plan ("Version history is limited to 1 day"). **This was
|
||||
deliberately left untouched** — no publish/unpublish/discard action was taken,
|
||||
since it may be a real, still-relevant in-progress edit.
|
||||
|
||||
## Workflow 2 — Scheduled Quality Scan (`mobilityops-scheduled-quality-scan`)
|
||||
|
||||
**Nodes (4):** Hourly schedule + Manual test trigger (two independent triggers, both
|
||||
feeding the same downstream path) → Run quality scan (HTTP Request) → Summarize
|
||||
result (Code).
|
||||
|
||||
- **Hourly schedule**: interval `Hours`, every `1` hour, at minute `0`. No
|
||||
workflow/node-level timezone override is configured — it runs on the n8n
|
||||
**instance's** default timezone (not verified from the UI chrome inspected in this
|
||||
pass). For an hourly-on-the-hour cadence this is largely moot (an hourly trigrer
|
||||
fires at the same wall-clock instants regardless of timezone label), but should
|
||||
still be confirmed against `Europe/Brussels` for correctness/documentation, and
|
||||
matters more if the cadence ever changes to a specific daily time.
|
||||
- **Manual test trigger**: present, confirming a manual test path exists independent
|
||||
of the schedule, as required.
|
||||
- **Run quality scan** (HTTP Request → Fleet Ops): `POST
|
||||
http://192.168.10.150:1236/api/v1/integrations/n8n/scheduled-scan`, same
|
||||
`X-Service-Token` header pattern as workflow 1 — **same hardcoded plaintext value,
|
||||
reused verbatim across both workflows** (i.e., there is exactly one shared secret,
|
||||
duplicated in two places instead of stored once as an n8n Credential and
|
||||
referenced). `Timeout: 15000` ms configured (bounded). No query params, no body.
|
||||
- **Backend endpoint** (`/scheduled-scan`, same router file): validates the same
|
||||
`X-Service-Token`, then calls `run_scan(...)`, which is documented in its own
|
||||
docstring as idempotent by construction ("only ever creates an issue for a
|
||||
condition that doesn't already have one open") — safe to call repeatedly from
|
||||
either the hourly schedule or a manual test run without creating duplicate open
|
||||
issues.
|
||||
- **Summarize result** (Code node): `total_created = sum(created.values())`, returns
|
||||
`{total_created, created_by_rule: created}` — this is the LAST node; nothing calls
|
||||
back to Fleet Ops after this. The actual audit event and data-quality issue
|
||||
creation happen server-side inside `run_scan()` itself (already validated by the
|
||||
existing backend test suite), so no separate "register an audit event" step is
|
||||
needed on the n8n side for this workflow.
|
||||
- **Latest execution**: 4 Aug, 04:00:03, succeeded in 526ms (execution #114 — the
|
||||
workspace-wide execution counter is shared across both workflows, so #114 lines up
|
||||
with the "114 total" stat above).
|
||||
- **Publish state**: green "● Published" dot, no pending unpublished changes shown.
|
||||
|
||||
## Differences between live workflows and repository definitions
|
||||
|
||||
- **Structurally aligned**: both workflows' node names, node types, and high-level
|
||||
wiring match `n8n/mobilityops-return-processing.json` and
|
||||
`n8n/mobilityops-scheduled-quality-scan.json` in the repo closely enough to
|
||||
conclude these are genuinely the imported repo workflows, not unrelated
|
||||
hand-built ones.
|
||||
- **Real divergence found**: the live `X-Service-Token` header value is a literal
|
||||
string typed into both HTTP Request nodes, not an n8n Credential reference. Whether
|
||||
the repo JSON also encodes this as a literal (vs. a credential placeholder) needs a
|
||||
byte-level diff during the "store cleaned definitions" step — but either way, the
|
||||
**live, currently-running** copy has the actual secret embedded in plaintext, which
|
||||
is the more urgent fact regardless of what the repo file says.
|
||||
- **Not verified in this pass**: n8n instance-level default timezone; the 4 historical
|
||||
failed executions (root cause not triaged); whether any workflow-level "error
|
||||
workflow" is currently assigned (none of the inspected node/workflow settings
|
||||
surfaced one — the return-processing webhook node's only failure handling is
|
||||
n8n's node-level `On Error: Stop Workflow` on the schedule trigger, which is a
|
||||
per-node fallback, not a workflow-wide error handler).
|
||||
|
||||
## Stale or duplicate workflows
|
||||
|
||||
None found. Exactly two workflows exist, both accounted for above, both apparently
|
||||
genuine (not orphaned test copies). No `ARCHIVED —`-prefixed or otherwise stale
|
||||
workflow exists yet.
|
||||
|
||||
## Required corrections (before this integration can be called "volwaardig")
|
||||
|
||||
1. **Move the shared `X-Service-Token` secret into an n8n Credential** (e.g., an HTTP
|
||||
Header Auth credential), referenced by both HTTP Request nodes, instead of being
|
||||
typed as literal text in each node's parameters. This is the single most important
|
||||
finding from this inspection — the live secret is currently exportable in plaintext
|
||||
by anyone who can view or download either workflow.
|
||||
2. **Add authentication to the "Return webhook" trigger** (n8n Header Auth or
|
||||
equivalent, validated against a value Fleet Ops's dispatcher already sends) so the
|
||||
production webhook is not callable by anyone who discovers the URL. Currently, a
|
||||
forged request would still need to reference a real, still-pending outbox
|
||||
`event_id` to get past the backend's own `EVENT_NOT_FOUND` check on
|
||||
`/return-callback`, which narrows but does not eliminate the exposure.
|
||||
3. Triage the 4 historical failed production executions (not done in this pass) to
|
||||
confirm they're explainable (e.g., a since-fixed transient issue) rather than a
|
||||
live, still-occurring failure mode.
|
||||
4. Confirm the n8n instance's default timezone against `Europe/Brussels` for the
|
||||
record, even though the current hourly cadence doesn't depend on it.
|
||||
5. Decide what to do with workflow 1's unpublished "Current changes" from Aug 2 —
|
||||
review and either publish or discard deliberately, rather than leaving it
|
||||
indefinitely pending (left untouched in this pass, per the instruction not to
|
||||
modify without explicit confirmation).
|
||||
6. Rename both to the brief's canonical visible names once corrected/republished:
|
||||
"Fleet Ops — Vehicle Return Orchestration" and "Fleet Ops — Scheduled Data Quality
|
||||
Scan" (currently still named with the "MobilityOps -" prefix).
|
||||
|
||||
## Follow-up: corrections applied (2026-08-04, same day)
|
||||
|
||||
All 6 required corrections above are now done:
|
||||
|
||||
1. **Done.** Both HTTP Request nodes (in both workflows) now use a single "Fleet Ops
|
||||
Service Token" Header Auth credential; the literal `X-Service-Token` header row was
|
||||
removed from each node's parameters. Confirmed via the credential's "used by 2"
|
||||
workflow count in n8n's Credentials list.
|
||||
2. **Done.** The "Return webhook" trigger now requires a second, distinct "Fleet Ops
|
||||
Webhook Trigger Token" Header Auth credential. Fleet Ops's outbox dispatcher
|
||||
(`backend/app/services/dispatcher.py`) now sends the matching
|
||||
`X-Fleet-Ops-Trigger-Token` header (new `MOBILITYOPS_WEBHOOK_TRIGGER_TOKEN` setting,
|
||||
added to `.env.example`, `compose.yaml`, the local dev `.env`, and the Unraid
|
||||
server's `.env`). Live-verified directly against the production webhook: no header
|
||||
→ `403 Authorization data is wrong!`; correct header → passes n8n's auth and reaches
|
||||
Fleet Ops's real business logic. Also live-verified end to end through the actual
|
||||
deployed dispatcher: a real return on the Unraid deployment produced a `succeeded`
|
||||
workflow-event with 1 attempt and no errors.
|
||||
- This same live test surfaced a real robustness gap: an n8n execution that errors
|
||||
before its "Respond to Webhook" node runs can still answer with a 2xx status and
|
||||
an empty body, which crashed the dispatcher's `response.json()` outside its own
|
||||
error handling. Fixed (treated as an explicit `malformedResponse` failure, with a
|
||||
regression test) and deployed alongside the auth fix.
|
||||
3. **Done.** Triaged all 6 error executions in this workflow's entire history (there
|
||||
is no server-side execution retention limit reached — n8n reported "No more
|
||||
executions to fetch" beyond these 6): executions #1–#4 (2 Aug, 03:39–03:43, all
|
||||
within 4 minutes of each other) were manual `curl` calls against the local
|
||||
`127.0.0.7:5678` test webhook with a `curl/8.16.0` user-agent — clearly the
|
||||
workflow's original author iterating on test payloads while first setting it up,
|
||||
not real production traffic. Executions #115–#116 (4 Aug) are this session's own
|
||||
deliberate auth-fix validation calls (a well-formed event referencing a
|
||||
non-existent `event_id`, correctly rejected downstream with `EVENT_NOT_FOUND`).
|
||||
**Zero unexplained or currently-live failures.**
|
||||
4. Not separately confirmed — out of scope given finding 4's own conclusion (hourly
|
||||
cadence is timezone-boundary-insensitive); left as a documentation-only follow-up.
|
||||
5. **Done, per explicit user confirmation.** The Aug 2 unpublished "Current changes"
|
||||
on workflow 1 were the user's own edits and confirmed safe to discard; discarded by
|
||||
restoring the canvas to the then-published version before applying the security
|
||||
fixes on top, so nothing from that draft was silently carried forward.
|
||||
6. **Done.** Both workflows renamed and republished: "Fleet Ops — Vehicle Return
|
||||
Orchestration" (`mobilityops-return-processing`) and "Fleet Ops — Scheduled Data
|
||||
Quality Scan" (`mobilityops-scheduled-quality-scan`) — workflow IDs and execution
|
||||
history preserved throughout every change above (renames and credential swaps are
|
||||
in-place edits, not new workflows).
|
||||
Reference in New Issue
Block a user