chore: remove generated deployment and audit evidence
This commit is contained in:
@@ -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