diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md
index cf71768..08ed384 100644
--- a/PROJECT_STATE.md
+++ b/PROJECT_STATE.md
@@ -2647,3 +2647,28 @@ evidence yet."
error or traceback appears in post-deployment logs.
- Exact next action: none for the selected scope. Optional secret rotation and new n8n
execution evidence remain intentionally deferred by the user.
+
+## M29 — recruiter-facing product polish and engineering evidence (2026-08-10)
+
+- Added a passwordless **90-second Highlights** entry that presents three directly
+ verifiable engineering stories: transactional vehicle returns, human-controlled data
+ correction and citation-bound knowledge. Rebuilt the Engineering page around domain
+ ownership, commit-before-orchestration, AI evidence, architecture and honest live/demo
+ integration states.
+- Return completion now carries its persisted correlation ID into a dedicated audit
+ processing trace, linking the return, vehicle state, transactional outbox and delivery
+ evidence. Data-quality detail pages lead with the human rule name instead of a raw
+ identifier; Knowledge Hub source titles, staged progress and provider diagnostics are
+ clearer without overstating RAGcore index evidence.
+- Improved recruiter entry metadata and social preview, route skeletons, 360 px touch
+ targets, reduced-motion behaviour, trilingual content and deterministic guide fallbacks.
+ Added automated recruiter and mobile-overflow coverage plus reproducible evidence
+ screenshot tooling. Refocused README, architecture and portfolio documentation on
+ inspectable product and reliability claims.
+- Validation: isolated backend **239 passed**; Ruff clean; mypy clean across 58 files;
+ frontend lint/build passed; full Playwright **150 passed in 5.2 minutes**; full and
+ production npm audits report zero vulnerabilities; screenshot capture **1 passed** and
+ desktop/mobile output was visually inspected.
+- Exact next action: commit and push M29, create and verify a live database backup,
+ deploy the committed archive to Unraid, then run live health, migration, browser and
+ acceptance checks before recording the final production evidence.
diff --git a/README.md b/README.md
index 3296269..425d830 100644
--- a/README.md
+++ b/README.md
@@ -1,136 +1,94 @@
# Fleet Ops
-**Connected operations for vehicle rental and service teams.**
+**A recruiter-ready operations platform for vehicle rental and service teams.**
-Fleet Ops is a working proof of concept for a fictitious mobility company. It combines vehicle and booking operations, a controlled vehicle-return workflow, data-quality review, RAGcore-backed internal knowledge, n8n orchestration and read-only tools published through ITWorx MCP Hub.
+[Open the live demo](http://192.168.10.150:1236) · no password required · choose **Highlights in 90 seconds** for the shortest tour.
-**Naming:** "Fleet Ops" is the product's visible name everywhere in the UI, the demo
-knowledge base, and this documentation. "MobilityOps" remains the technical
-identifier only — the repository name, local directory, package/module names, Docker
-Compose project, deployment directory, and database names. The UI is fully trilingual
-(nl-BE default, en-GB, fr-BE); see `docs/fleet-ops-correction/` for the localization
-architecture, the vehicle-status decision table, and the correction evidence, and
-`docs/fleet-ops-final-localization/` for the follow-up correction round (remaining
-NL/FR translation gaps, centralized API-error localization, the time-dependent
-Europe/Brussels dashboard greeting).
+Fleet Ops turns fragmented vehicle, booking and procedure data into one controlled operational workspace. It is a complete synthetic-data product demo: the company and records are fictional, while the workflows, persistence, validation, authorization, audit trail and integration boundaries are implemented.
-The web application uses the premium responsive **Control Rail** interface: a compact
-operations-first workspace with persisted readiness metrics, evidence-led exceptions,
-review-before-commit return handling and mobile navigation designed down to 390 px. See
-`docs/design/design-directions.md` and `docs/design/implementation-validation.md` for the
-design decision and visual evidence.
+
-All people, companies, vehicles, bookings and documents are synthetic. The workflows, validation, integrations, audit logging and access boundaries are intended to be real.
+## The 90-second tour
-## Demo
+1. Open **Highlights** from the login screen.
+2. Follow a vehicle return from review to atomic commit, quality issue, outbox and correlated audit trace.
+3. Compare and merge a duplicate customer with explicit human confirmation.
+4. Ask the Knowledge Hub a damage question and inspect its cited procedure evidence.
+5. Open **Engineering** for the architecture, reliability guarantees, test evidence and honest scope boundary.
-The demo presents itself as **Northstar Mobility**, a fictitious Belgian camper/van
-rental company — the login screen, a permanent "Synthetische demo" indicator, an in-app
-guided tour (Demo Guide), a curated `/scenarios` overview, and an "Over deze demo" page
-all make the fictional context, synthetic-data status, and real-vs-simulated boundaries
-explicit without any verbal explanation. See `docs/demo-release/` for the full demo
-concept, the five named scenarios, the seed/date-anchoring strategy, the guided-tour
-design, and the operational runbook (5-minute and 10-minute demo flows, reset, redeploy,
-rollback).
+## What makes it more than a mock-up
-## Scope
+- **Transactional operations:** a return writes the inspection, vehicle/booking state, audit events and outbox record atomically. n8n downtime never rolls back the local business transaction.
+- **Explainable data quality:** five persisted rule types, SLA deadlines, assignment, bulk queue controls and bounded resolution flows—not decorative warning cards.
+- **Grounded knowledge:** the live deployment uses RAGcore; insufficient or unavailable evidence produces no invented answer. Citations and provider provenance remain inspectable.
+- **Safe AI exposure:** four tenant-bound, service-authenticated, read-only Fleet Ops tools are published through ITWorx MCP Hub and audited with correlation IDs.
+- **Operational reliability:** bounded retries, delivery leases, health/readiness, Prometheus metrics, Grafana, scheduled verified backups and graceful external-dependency degradation.
+- **Real product ergonomics:** nl-BE, en-GB and fr-BE; responsive from 360 px; keyboard-accessible navigation; role-aware global search; route-level lazy loading; server-enforced permissions.
-The PoC implements:
+## Architecture
-- operations dashboard with a truthful aggregate n8n/MCP integration-status card;
-- vehicle and booking workspaces with planning windows, location filters, service/next-
- booking context, operational ordering and pagination;
-- server-backed session lifecycle (refresh-safe, central 401 handling);
-- a role matrix enforced server-side and mirrored in the UI (see
- `docs/12-security-and-audit.md`);
-- vehicle return capture → authoritative server-evaluated review → commit → result;
-- five deterministic data-quality checks with SLA deadlines, assignment, overdue/bulk
- queue controls, bounded resolution flows and a manual scan action;
-- human review and customer merge;
-- audit trail with human-readable before/after evidence and safe entity links;
-- role-aware global search across vehicles, bookings and (Operations Manager) issues;
-- safe, confirmed demo reset;
-- RAGcore-backed knowledge assistant with citations;
-- four canonical n8n workflows: return processing, scheduled data-quality scanning,
- RAGcore procedure sync and centralized workflow-error handling, with explicit
- heartbeat evidence and crash-recoverable outbox delivery leases;
-- four read-only MCP tools through ITWorx MCP Hub;
-- deterministic demo reset and five-minute showcase.
-- audited operational user creation, role/name editing, password reset and activation.
+```mermaid
+flowchart LR
+ UI["React + TypeScript\nresponsive operations UI"] -->|session cookie| API["FastAPI\nbusiness rules + RBAC"]
+ API --> DB[(PostgreSQL)]
+ API -->|grounded retrieval| RAG[RAGcore]
+ DB --> OUT["Transactional outbox"]
+ OUT -->|bounded retry| N8N["Existing central n8n"]
+ N8N -->|authenticated callback| API
+ HUB["ITWorx MCP Hub"] -->|4 read-only tools| API
+```
-It is not an ERP, CRM, accounting package, public booking site, payment system or autonomous agent.
+Fleet Ops owns operational truth. RAGcore owns retrieval, n8n performs post-commit orchestration, and MCP Hub owns tool transport/publication. Neither RAGcore nor MCP Hub accesses the Fleet Ops database directly. See [the as-built architecture](artifacts/evidence/architecture.md).
-## Integration status
+## Demonstrable scope
-- **n8n**: fully implemented and verified against a real n8n instance, including
- degraded mode (n8n stopped mid-flow → return still commits, event stays `pending`
- with backoff, self-heals once n8n returns), the failed-delivery manual-retry path,
- stale-delivery-lease recovery after a simulated crash, and a second (scheduled
- quality-scan) workflow live-verified end to end against a real n8n instance.
- `GET /api/v1/integrations/status` reports a truthful aggregate state from outbox
- delivery counts, not just the most recent event.
-- **RAGcore**: the live deployment uses `KNOWLEDGE_PROVIDER=ragcore`. Readiness and real
- grounded retrieval are verified end to end; when RAGcore's generated-answer endpoint
- is unavailable, Fleet Ops falls back only to cited extractive search results and never
- invents an answer. The deterministic demo provider remains available for clean-checkout
- acceptance and local development.
-- **ITWorx MCP Hub**: the four read-only provider endpoints are implemented, tested, and
- directly `curl`-verified with correct auth enforcement and audit logging.
- `MCP_HUB_REGISTRATION_ENABLED` is actually wired into `Settings` and reported honestly
- by the integration-status endpoint (evidence-based: real tool-call audit history, not
- just the flag). The Fleet Ops connector is confirmed live in the ITWorx MCP Hub's own
- production deployment (Tower), with a real contract fix already applied there
- (`vehicle.get`'s wire parameter normalized to `vehicleRef`).
+- dashboard, vehicle fleet, booking lifecycle and controlled returns;
+- data-quality queue, assignment, review, merge and resolution;
+- correlated human-readable audit history;
+- cited Knowledge Hub with honest provider state;
+- n8n delivery monitoring and manual retry;
+- user administration, privacy export/anonymisation and retention guards;
+- deterministic reset with 2 users, 180 customers, 50 vehicles, 254 bookings, 75 inspections, 40 maintenance records, 33 quality issues and 20 workflow runs.
-See `artifacts/functional-completion/final-summary.md` for the functional-completion
-audit evidence (supersedes the design-validation summary below for integration status),
-and `artifacts/final-acceptance/summary.md` for the original M0–M7 acceptance evidence.
+This is deliberately not accounting, payments, a public reservation site, generic CRM, inventory, HR or an autonomous write agent.
-## Repository map
+## Stack
-- `CLAUDE.md` — binding implementation rules.
-- `MASTER_BUILD_PROMPT.md` — prompt to start an autonomous Claude run.
-- `PROJECT_STATE.md` — short persistent project memory.
-- `docs/` — product, architecture, UX and acceptance specification.
-- `contracts/` — OpenAPI, event and MCP contracts.
-- `knowledge/` — fictitious source documents for the MobilityOps RAGcore workspace.
-- `seed/` — deterministic synthetic dataset and generator.
-- `n8n/` — importable workflow definitions.
-- `backend/` — FastAPI/SQLAlchemy/Alembic API.
-- `frontend/` — React/TypeScript/Vite web app, including the Playwright end-to-end suite (`frontend/e2e/`).
-- `artifacts/evidence/` — final acceptance evidence (screenshots, architecture, `final-summary.md`).
-- `artifacts/design-validation/` — baseline audit, Stitch direction references and implemented responsive captures.
-- `docs/functional-completion/` — the functional-completion audit and pre-work server baseline.
-- `artifacts/functional-completion/` — functional-completion acceptance evidence.
-- `docs/demo-release/` — demo concept, scenarios, seed/date-anchoring strategy, guided
- tour, and runbook.
-- `artifacts/demo-release/` — demo-productization acceptance evidence.
+React, TypeScript, Vite, FastAPI, SQLAlchemy 2, PostgreSQL, Alembic, n8n, RAGcore, ITWorx MCP Hub, Docker Compose, Prometheus, Grafana and Playwright.
-## Quickstart
+## Run locally
```bash
cp .env.example .env
make demo
```
-This builds and starts the full stack (migrations run automatically) and loads the
-deterministic demo dataset. See `docs/17-runbook.md` for the one-time n8n workflow setup
-required for the automation demo, and the full operational runbook.
-
-Endpoints:
-
- Web: `http://localhost:1228`
-- API readiness: `http://localhost:8128/health/ready` (liveness: `/health/live`)
-- n8n: `http://localhost:5678`
+- API readiness: `http://localhost:8128/health/ready`
+- Existing n8n server: configure `N8N_BASE_URL`; Fleet Ops does not create a second n8n instance.
-All defaults are configurable via `.env` (see `.env.example`).
+The deterministic local knowledge provider supports clean-checkout acceptance without pretending to be the live RAGcore integration. Configuration is documented in `.env.example`; operations and recovery are in [docs/17-runbook.md](docs/17-runbook.md).
## Quality gates
```bash
-make test # backend: isolated PostgreSQL Compose project; never the live database
-make lint # backend: ruff + mypy (strict, zero errors)
-make e2e # frontend: complete Playwright acceptance (live stack required)
+make test # isolated PostgreSQL backend suite
+make lint # Ruff + strict mypy
+make e2e # complete Playwright browser acceptance
+cd frontend && npm run build
```
-Frontend build/typecheck: `cd frontend && npm run build` (`tsc -b && vite build`).
+Current verified results and production evidence are recorded in [artifacts/evidence/final-summary.md](artifacts/evidence/final-summary.md) and [PROJECT_STATE.md](PROJECT_STATE.md).
+
+## Repository map
+
+- `backend/` — FastAPI domain, API, migrations and tests
+- `frontend/` — React app and Playwright acceptance suite
+- `contracts/` — OpenAPI, event and MCP contracts
+- `knowledge/` — versioned fictional procedures
+- `n8n/` — importable workflow definitions for the existing server
+- `seed/` — deterministic synthetic dataset
+- `docs/` — architecture, security, UX, testing and runbooks
+- `artifacts/evidence/` — current acceptance evidence and screenshots
+
+“MobilityOps” remains the repository/deployment identifier; **Fleet Ops** is the product name shown to users.
diff --git a/artifacts/evidence/architecture.md b/artifacts/evidence/architecture.md
index 398ae32..abea803 100644
--- a/artifacts/evidence/architecture.md
+++ b/artifacts/evidence/architecture.md
@@ -1,63 +1,68 @@
-# MobilityOps — as-built architecture
+# Fleet Ops — as-built architecture
```mermaid
flowchart TB
- subgraph Browser
- UI["MobilityOps Web React + TypeScript"]
- end
+ UI["Fleet Ops Web\nReact + TypeScript + Vite"]
- subgraph MobilityOps["MobilityOps (this repo)"]
- API["FastAPI backend /api/v1/*"]
- DISPATCH["Outbox dispatcher background thread"]
+ subgraph CORE["Fleet Ops — this repository"]
+ API["FastAPI /api/v1\nRBAC + domain rules"]
+ OUT["Outbox dispatcher\nleases + bounded retry"]
DB[(PostgreSQL)]
+ OBS["Prometheus metrics\nGrafana dashboards"]
API --> DB
- DISPATCH --> DB
+ OUT --> DB
+ API --> OBS
end
- subgraph External["External central services"]
- N8N["n8n return-processing workflow"]
- RAGDEMO["Demo KnowledgeProvider TF-IDF extractive, local files"]
- RAGCORE["RAGcore (adapter built, no live instance)"]
- HUB["ITWorx MCP Hub (endpoints built, no live instance)"]
+ subgraph EXT["Existing external platforms"]
+ N8N["Central n8n\nsecondary orchestration"]
+ RAG["RAGcore\ngrounded procedure retrieval"]
+ HUB["ITWorx MCP Hub\ntool transport + publication"]
end
- UI -->|session cookie| API
- API -->|GroundedAnswer| RAGDEMO
- API -.->|configurable, unavailable-safe| RAGCORE
- DISPATCH -->|POST vehicle.returned.v1| N8N
- N8N -->|callback, X-Service-Token| API
- HUB -.->|X-Service-Token, read-only| API
-
- classDef unverified stroke-dasharray: 5 5;
- class RAGCORE,HUB unverified;
+ UI -->|secure session cookie| API
+ API -->|tenant/workspace adapter| RAG
+ OUT -->|vehicle.returned.v1| N8N
+ N8N -->|service-authenticated callback| API
+ HUB -->|service-authenticated read-only tools| API
```
-Dashed boxes/arrows are implemented and unit/contract-tested but were never exercised
-against a live instance in this environment (no reachable RAGcore or ITWorx MCP Hub).
-Solid boxes were verified end-to-end, including a real n8n instance.
+## Ownership and trust boundaries
-## Component responsibility (unchanged from `docs/03-architecture.md`)
+| Component | Owns | Explicitly does not own |
+|---|---|---|
+| Fleet Ops | vehicles, customers, bookings, inspections, quality issues, audit, permissions, outbox state | external workflow execution or procedure retrieval |
+| RAGcore | indexing/retrieval and grounded procedure evidence | Fleet Ops database or business state |
+| ITWorx MCP Hub | MCP transport, connector publication and central tool-call audit | Fleet Ops database or write actions |
+| n8n | post-commit workflow orchestration | critical business rules or the source-of-truth transaction |
-| Component | Owns |
-|---|---|
-| MobilityOps | vehicles, customers, bookings, inspections, data-quality issues, audit, outbox/delivery state |
-| RAGcore | procedure retrieval and grounded answers (demo provider substitutes locally) |
-| ITWorx MCP Hub | MCP transport, tool publication, central tool-call audit |
-| n8n | post-commit secondary orchestration only — never the source of truth for vehicle state |
+## End-to-end return trace
-## Reliability boundaries verified in this build
+```mermaid
+sequenceDiagram
+ actor Operator
+ participant Web
+ participant API
+ participant DB
+ participant n8n
+ Operator->>Web: Review and confirm return
+ Web->>API: POST return with idempotency key
+ API->>DB: Lock booking and validate invariants
+ API->>DB: Commit inspection, state, audit and outbox atomically
+ API-->>Web: Result + correlation ID
+ Web-->>Operator: Human result and full processing trace
+ API->>n8n: Deliver persisted outbox event
+ n8n->>API: Authenticated status callback
+ API->>DB: Persist delivery/audit evidence
+```
-1. **Return commits atomically with its outbox event** — `app/services/returns.py`, one
- transaction; verified by `test_concurrent_returns_only_one_succeeds` (real Postgres row
- locking, not mocked).
-2. **Outbox delivery is at-least-once, idempotent by event ID** — verified live: the n8n
- callback checks for an existing `AuditEvent` by event ID before recording a second time.
-3. **RAGcore failure disables knowledge answers only** — `RAGcoreKnowledgeProvider` degrades
- to `unavailable`; the rest of the app is unaffected because the knowledge router is the
- only consumer.
-4. **MCP Hub failure does not affect the web application** — the four MCP provider
- endpoints are a separate authenticated surface (`X-Service-Token`), invisible to the
- browser-facing API/UI.
-5. **n8n failure leaves events pending with bounded retries** — verified live: a seeded
- `failed` event, retried through the UI, was picked up by the background dispatcher and
- delivered through the real n8n instance within one poll cycle.
+## Verified reliability properties
+
+1. Concurrent returns serialize through PostgreSQL row locking; only one can commit.
+2. Local return success is independent of n8n availability. Pending delivery remains persisted and retryable.
+3. Outbox delivery is at-least-once and idempotent by event ID, with crash-recoverable leases and bounded backoff.
+4. RAGcore failure affects knowledge answers only. The UI reports unavailable/insufficient evidence and does not invent an answer.
+5. MCP endpoints are a separate tenant-bound, client-identity-validated, read-only surface; every call is audited with a correlation ID.
+6. Browser authorization is enforced again on the API. Hiding a navigation item is never the security boundary.
+
+The live deployment has exercised all three external boundaries. Local clean-checkout acceptance can use the deterministic extractive knowledge provider while reporting that mode honestly.
diff --git a/artifacts/evidence/screenshots/1-login.png b/artifacts/evidence/screenshots/1-login.png
index c513487..56d0d33 100644
Binary files a/artifacts/evidence/screenshots/1-login.png and b/artifacts/evidence/screenshots/1-login.png differ
diff --git a/artifacts/evidence/screenshots/2-highlights.png b/artifacts/evidence/screenshots/2-highlights.png
new file mode 100644
index 0000000..6a21149
Binary files /dev/null and b/artifacts/evidence/screenshots/2-highlights.png differ
diff --git a/artifacts/evidence/screenshots/3-engineering-story.png b/artifacts/evidence/screenshots/3-engineering-story.png
new file mode 100644
index 0000000..a267269
Binary files /dev/null and b/artifacts/evidence/screenshots/3-engineering-story.png differ
diff --git a/artifacts/evidence/screenshots/4-dashboard.png b/artifacts/evidence/screenshots/4-dashboard.png
new file mode 100644
index 0000000..c038ca4
Binary files /dev/null and b/artifacts/evidence/screenshots/4-dashboard.png differ
diff --git a/artifacts/evidence/screenshots/5-knowledge-evidence.png b/artifacts/evidence/screenshots/5-knowledge-evidence.png
new file mode 100644
index 0000000..8898872
Binary files /dev/null and b/artifacts/evidence/screenshots/5-knowledge-evidence.png differ
diff --git a/artifacts/evidence/screenshots/6-highlights-mobile.png b/artifacts/evidence/screenshots/6-highlights-mobile.png
new file mode 100644
index 0000000..77a9b64
Binary files /dev/null and b/artifacts/evidence/screenshots/6-highlights-mobile.png differ
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 7d39a91..2ee13e5 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -164,6 +164,7 @@ class RegisterReturnResult(BaseModel):
odometer_regression: bool
quality_issue_ref: str | None
workflow_event_id: str
+ correlation_id: str
next_booking_risk: NextBookingRisk | None
diff --git a/backend/app/services/demo_manifest.py b/backend/app/services/demo_manifest.py
index a63771a..a42b870 100644
--- a/backend/app/services/demo_manifest.py
+++ b/backend/app/services/demo_manifest.py
@@ -143,14 +143,21 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
),
DemoIntegrationSummaryOut(
key="ragcore",
- status_code="operational" if knowledge_health.provider == "ragcore" else "demoMode",
- detail_code="ragcoreDetail",
+ status_code=(
+ "operational"
+ if knowledge_health.provider == "ragcore" and knowledge_health.available
+ else "demoMode"
+ if knowledge_health.provider == "demo" and knowledge_health.available
+ else "unavailable"
+ ),
+ detail_code=(
+ "ragcoreDetail"
+ if knowledge_health.document_count is not None
+ else "ragcoreSourceDetail"
+ ),
detail_params={
- "count": (
- knowledge_health.document_count
- if knowledge_health.document_count is not None
- else "unknown"
- ),
+ "count": knowledge_health.document_count or 0,
+ "source_count": knowledge_health.source_document_count,
"collection": knowledge_health.collection,
},
),
@@ -162,7 +169,9 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
# "operational" -- same evidence rule the integration status page uses.
status_code="operational" if mcp_hub.state == "operational" else "notConnected",
detail_code=(
- "mcpDetailEnabled" if mcp_hub.state == "operational" else "mcpDetailNotConnected"
+ "mcpDetailOperational"
+ if mcp_hub.state == "operational"
+ else "mcpDetailPrepared"
),
detail_params={},
),
diff --git a/backend/app/services/returns.py b/backend/app/services/returns.py
index 6095113..5196b69 100644
--- a/backend/app/services/returns.py
+++ b/backend/app/services/returns.py
@@ -328,6 +328,7 @@ def register_vehicle_return(
"odometer_regression": evaluation.odometer_regression,
"quality_issue_ref": quality_issue_ref,
"workflow_event_id": str(event.event_id),
+ "correlation_id": str(correlation_id),
"next_booking_risk": evaluation.next_booking_risk,
}
diff --git a/backend/tests/test_return.py b/backend/tests/test_return.py
index 53b103d..6d2f1c6 100644
--- a/backend/tests/test_return.py
+++ b/backend/tests/test_return.py
@@ -120,6 +120,8 @@ def test_preview_performs_no_writes_and_matches_commit(ops_client):
assert commit_body["resulting_vehicle_status"] == preview_body["resulting_vehicle_status"]
assert commit_body["odometer_regression"] == preview_body["odometer_regression"]
assert commit_body["next_booking_risk"] == preview_body["next_booking_risk"]
+ assert commit_body["correlation_id"]
+ assert len(commit_body["correlation_id"]) == 36
def test_preview_detects_odometer_regression(ops_client):
diff --git a/docs/16-portfolio-case-study.md b/docs/16-portfolio-case-study.md
index 18dee71..071fe3d 100644
--- a/docs/16-portfolio-case-study.md
+++ b/docs/16-portfolio-case-study.md
@@ -1,43 +1,47 @@
-# Portfolio case-study template
+# Fleet Ops — portfolio case study
-## Title
+## Product
-MobilityOps — connected operations for vehicle rental and service teams
-
-## One-sentence summary
-
-A working synthetic-data PoC that unifies vehicle and booking operations, automates vehicle returns, surfaces data-quality problems and exposes grounded internal knowledge through reusable RAG and MCP platforms.
+Fleet Ops is a working operations platform for a fictional Belgian mobility company. It unifies vehicle and booking work, makes data defects resolvable, automates post-return follow-up and turns internal procedures into cited answers.
## Problem
-Operational information was modelled as fragmented across booking, vehicle and procedure sources, causing manual checks, inconsistent states and slow access to instructions.
+Rental operations often span booking records, vehicle state, inspections and separate procedure documents. The difficult part is not another dashboard; it is preserving one trustworthy state while people, automation and AI consume the same workflow.
-## Approach
+## Engineering approach
-- MobilityOps owns canonical operational data and business rules.
-- A transactional return workflow persists inspections, derives status and queues post-commit orchestration.
-- Explainable rules detect duplicates, mileage regressions, overlaps, missing fields and status conflicts.
-- RAGcore provides source-grounded procedure answers.
-- ITWorx MCP Hub exposes narrowly scoped read-only AI tools.
-- n8n orchestrates secondary processing without becoming the business-rule engine.
+- Keep business invariants in a FastAPI/SQLAlchemy domain layer backed by PostgreSQL constraints and row locks.
+- Commit vehicle returns, audit evidence and an outbox message in one transaction.
+- Let the existing central n8n execute secondary work asynchronously, with leases, retries and authenticated callbacks.
+- Detect five classes of data-quality issue and require bounded, explicit human resolution.
+- Integrate RAGcore through a provider adapter that refuses unsupported answers and preserves citation provenance.
+- Publish only four narrow read-only tools through ITWorx MCP Hub; never give external AI systems database access.
+- Expose correlation-based evidence in the product so architecture claims can be inspected, not merely read in a diagram.
-## Demonstrable results
+## Demonstrable result
-Use only facts actually measured in the final build, such as:
+- 50 vehicles, 180 customers and 254 bookings in a deterministic synthetic dataset;
+- one complete controlled return workflow and five persisted quality-rule types;
+- 11 authoritative Dutch procedure sources with localized grounded retrieval;
+- four tenant-bound, service-authenticated read-only MCP tools;
+- trilingual responsive UI, server-backed roles, global search, privacy controls and audit;
+- degraded-mode coverage for n8n and RAGcore, production health/metrics and verified backups;
+- complete backend and browser acceptance recorded in `artifacts/evidence/final-summary.md`.
-- 50 deterministic synthetic vehicles and 245 bookings;
-- five automated data-quality rule types;
-- one end-to-end return workflow;
-- ten versioned source procedures;
-- four read-only MCP tools;
-- tested degraded modes for RAGcore and n8n.
+## Product decisions worth discussing
-Do not claim real customer adoption, production use or percentage time savings.
+**Why an outbox?** A return must remain committed when n8n is unavailable. Persisting the event in the same database transaction closes the gap between business success and later orchestration.
+
+**Why no generative fallback?** A fluent procedure answer without evidence is worse than a clear unavailable state. Knowledge answers require cited provider evidence.
+
+**Why read-only MCP tools?** Recruiter demos should show credible AI integration without granting autonomous mutation rights. Operational writes remain explicit product workflows with authorization, validation and audit.
+
+**Why synthetic but production-shaped data?** The public demo can remain safely open while still exercising realistic state transitions, failure recovery and resettable scenarios.
## Stack
-React, TypeScript, FastAPI, PostgreSQL, n8n, RAGcore, ITWorx MCP Hub, Docker Compose and Playwright.
+React, TypeScript, Vite, FastAPI, SQLAlchemy 2, PostgreSQL, Alembic, n8n, RAGcore, ITWorx MCP Hub, Docker Compose, Prometheus, Grafana and Playwright.
-## Demo disclosure
+## Honest boundary
-All data and the represented company are fictitious. The software behaviour, integration contracts, audit trail and validation are implemented as a real proof of concept.
+Fleet Ops is a completed locked PoC, not claimed customer adoption or a general ERP. Accounting, payments, public reservations, CRM, inventory, HR and autonomous write agents are intentionally out of scope. A real personal-data rollout would additionally require the adopting organisation's identity provider, retention approvals, secrets lifecycle, alert ownership and disaster-recovery process.
diff --git a/frontend/e2e/_capture-recruiter-screenshots.spec.ts b/frontend/e2e/_capture-recruiter-screenshots.spec.ts
new file mode 100644
index 0000000..ead1a70
--- /dev/null
+++ b/frontend/e2e/_capture-recruiter-screenshots.spec.ts
@@ -0,0 +1,40 @@
+import { expect, test } from "@playwright/test";
+
+const OUT = "../artifacts/evidence/screenshots";
+
+test("capture current recruiter-facing evidence", async ({ page, request }) => {
+ await request.post("/api/v1/demo/login", { data: { role: "operations_manager" } });
+ await request.post("/api/v1/demo/reset");
+
+ await page.goto("/login");
+ await expect(page.getByRole("heading", { name: "Elke overdracht. Eén helder overzicht." })).toBeVisible();
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/1-login.png`, fullPage: true });
+
+ await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
+ await expect(page).toHaveURL(/\/highlights$/);
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/2-highlights.png`, fullPage: true });
+
+ await page.goto("/about");
+ await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" }).first()).toBeVisible();
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/3-engineering-story.png`, fullPage: true });
+
+ await page.goto("/dashboard");
+ await expect(page.getByRole("heading", { name: "Wagenparkstatus" })).toBeVisible();
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/4-dashboard.png`, fullPage: true });
+
+ await page.goto("/knowledge");
+ await page.getByRole("button", { name: "Wat moet ik doen wanneer een voertuig terugkomt met schade?" }).click();
+ await expect(page.getByText("Onderbouwd met geciteerde procedures")).toBeVisible({ timeout: 15_000 });
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/5-knowledge-evidence.png`, fullPage: true });
+
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/highlights");
+ await expect(page.locator(".highlight-card").first()).toBeVisible();
+ await page.waitForTimeout(500);
+ await page.screenshot({ path: `${OUT}/6-highlights-mobile.png` });
+});
diff --git a/frontend/e2e/demo-entry.spec.ts b/frontend/e2e/demo-entry.spec.ts
index 4a1b51f..0af6b33 100644
--- a/frontend/e2e/demo-entry.spec.ts
+++ b/frontend/e2e/demo-entry.spec.ts
@@ -5,7 +5,7 @@ test.describe.configure({ mode: "serial" });
test("demo entry screen names the fictional org and never shows a password", async ({ page }) => {
await page.goto("/login");
await expect(page.getByText(/Northstar Mobility/)).toBeVisible();
- await expect(page.getByText(/Synthetische demo/)).toBeVisible();
+ await expect(page.getByText(/Synthetische demo · geen echte klant-/)).toBeVisible();
await expect(page.getByRole("button", { name: "Start begeleide demo" })).toBeVisible();
await expect(page.getByRole("button", { name: "Verken als Operationsmanager" })).toBeVisible();
await expect(page.getByRole("button", { name: "Verken als Verhuurmedewerker" })).toBeVisible();
diff --git a/frontend/e2e/demo-legibility.spec.ts b/frontend/e2e/demo-legibility.spec.ts
index 6011e6d..e08c9cf 100644
--- a/frontend/e2e/demo-legibility.spec.ts
+++ b/frontend/e2e/demo-legibility.spec.ts
@@ -27,7 +27,7 @@ test("return flow pre-fills the suspicious odometer reading and explains why", a
await page.getByRole("button", { name: "Retour bevestigen" }).click();
await expect(page.getByRole("heading", { name: "Retour geregistreerd" })).toBeVisible();
await expect(page.getByRole("link", { name: "Automatiseringsstatus bekijken" })).toBeVisible();
- await expect(page.getByRole("link", { name: "Auditgeschiedenis bekijken" })).toBeVisible();
+ await expect(page.getByRole("link", { name: "Volledige verwerking volgen" })).toBeVisible();
});
test("data quality issue detail explains what's wrong and why it matters", async ({ page }) => {
diff --git a/frontend/e2e/demo.spec.ts b/frontend/e2e/demo.spec.ts
index 7e5fd91..7bd1085 100644
--- a/frontend/e2e/demo.spec.ts
+++ b/frontend/e2e/demo.spec.ts
@@ -18,7 +18,7 @@ test("five-minute demo script end to end", async ({ page, request }) => {
await test.step("1. login as Operations Manager", async () => {
await page.goto("/login");
- await expect(page.getByText(/Synthetische demo/)).toBeVisible();
+ await expect(page.getByText(/Synthetische demo · geen echte klant-/)).toBeVisible();
await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
await expect(page).toHaveURL(/\/dashboard$/);
});
@@ -90,7 +90,7 @@ test("five-minute demo script end to end", async ({ page, request }) => {
await test.step("9. verify responsive navigation at mobile width", async () => {
await page.setViewportSize({ width: 360, height: 800 });
await page.goto("/dashboard");
- await expect(page.getByText(/Synthetische demo/).first()).toBeVisible();
+ await expect(page.getByRole("button", { name: /Synthetische demo/ })).toBeVisible();
await expect(page.getByRole("link", { name: "Overzicht" }).first()).toBeVisible();
const scrollWidth = await page.evaluate(() => document.documentElement.scrollWidth);
const clientWidth = await page.evaluate(() => document.documentElement.clientWidth);
diff --git a/frontend/e2e/fleet-ops-correction.spec.ts b/frontend/e2e/fleet-ops-correction.spec.ts
index 1396505..b1467ca 100644
--- a/frontend/e2e/fleet-ops-correction.spec.ts
+++ b/frontend/e2e/fleet-ops-correction.spec.ts
@@ -107,7 +107,7 @@ test.describe("status-recommendation flow", () => {
await resetDemoData(request);
await loginAsOpsManager(page, "nl-BE");
await page.goto("/data-quality/DQ-DEMO-STATUS");
- await expect(page.getByRole("heading", { name: "DQ-DEMO-STATUS" })).toBeVisible();
+ await expect(page.getByText("DQ-DEMO-STATUS", { exact: true })).toBeVisible();
await page.getByRole("button", { name: REVIEW_RECOMMENDATION["nl-BE"] }).click();
await expect(page.getByText("Aanbevolen status")).toBeVisible();
diff --git a/frontend/e2e/interactive-elements.spec.ts b/frontend/e2e/interactive-elements.spec.ts
index 4eb8b73..a32b54c 100644
--- a/frontend/e2e/interactive-elements.spec.ts
+++ b/frontend/e2e/interactive-elements.spec.ts
@@ -185,7 +185,7 @@ test("data quality issue detail: defer and reject buttons work", async ({ page,
expect(ref).not.toBe("");
await firstLink.click();
await expect(page).toHaveURL(new RegExp(`/data-quality/${ref}$`));
- await expect(page.getByRole("heading", { name: ref })).toBeVisible();
+ await expect(page.getByText(ref, { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Defer" }).click();
await expect(page.locator(".badge.status-deferred")).toBeVisible();
});
@@ -193,7 +193,7 @@ test("data quality issue detail: defer and reject buttons work", async ({ page,
test("data quality: providing missing fields resolves a vehicle issue", async ({ page, request }) => {
await resetDemoData(request);
await page.goto("/data-quality/DQ-DEMO-ATTENTION");
- await expect(page.getByRole("heading", { name: "DQ-DEMO-ATTENTION" })).toBeVisible();
+ await expect(page.getByText("DQ-DEMO-ATTENTION", { exact: true })).toBeVisible();
await page.getByLabel("Registration number").fill("TST-777");
await page.getByLabel("Make").fill("TestMake");
@@ -207,7 +207,7 @@ test("data quality: providing missing fields resolves a vehicle issue", async ({
test("data quality: resolving a booking overlap blocks one booking", async ({ page, request }) => {
await resetDemoData(request);
await page.goto("/data-quality/DQ-DEMO-OVERLAP");
- await expect(page.getByRole("heading", { name: "DQ-DEMO-OVERLAP" })).toBeVisible();
+ await expect(page.getByText("DQ-DEMO-OVERLAP", { exact: true })).toBeVisible();
await page.getByRole("radio", { name: /Block BK-DEMO-OVERLAP-A/ }).check();
await page.getByRole("button", { name: /^Block BK-DEMO-OVERLAP-A$/ }).click();
@@ -227,7 +227,7 @@ test("data quality: applying the recommended status resolves a vehicle conflict"
}) => {
await resetDemoData(request);
await page.goto("/data-quality/DQ-DEMO-STATUS");
- await expect(page.getByRole("heading", { name: "DQ-DEMO-STATUS" })).toBeVisible();
+ await expect(page.getByText("DQ-DEMO-STATUS", { exact: true })).toBeVisible();
await page.getByRole("button", { name: "Review recommendation" }).click();
await page.getByRole("button", { name: /^Change status to/ }).click();
@@ -248,7 +248,7 @@ test("data quality: retaining canonical resolves an odometer regression issue",
const target = issues[0];
await page.goto(`/data-quality/${target.public_ref}`);
- await expect(page.getByRole("heading", { name: target.public_ref })).toBeVisible();
+ await expect(page.getByText(target.public_ref, { exact: true })).toBeVisible();
await page.getByRole("radio", { name: /Retain canonical/ }).check();
await page.getByRole("button", { name: "Resolve issue" }).click();
diff --git a/frontend/e2e/recruiter-polish.spec.ts b/frontend/e2e/recruiter-polish.spec.ts
new file mode 100644
index 0000000..6e2502f
--- /dev/null
+++ b/frontend/e2e/recruiter-polish.spec.ts
@@ -0,0 +1,38 @@
+import { expect, test } from "@playwright/test";
+
+test.describe.configure({ mode: "serial" });
+
+test("90-second recruiter entry exposes three verifiable engineering highlights", async ({ page }) => {
+ await page.goto("/login");
+ await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
+
+ await expect(page).toHaveURL(/\/highlights$/);
+ await expect(page.getByRole("heading", { name: "De sterkste keuzes in 90 seconden" })).toBeVisible();
+ await expect(page.locator(".highlight-card")).toHaveCount(3);
+ await expect(page.getByText("Transactie · idempotency · outbox · degradatie")).toBeVisible();
+ await expect(page.getByText("RAGcore · citations · veilige degradatie")).toBeVisible();
+});
+
+test("Engineering Story exposes architecture, reliability and honest integration evidence", async ({ page }) => {
+ await page.goto("/login");
+ await page.getByRole("button", { name: "Verken als Operationsmanager" }).click();
+ await page.goto("/about");
+
+ await expect(page.getByRole("heading", { name: "Controle, betrouwbaarheid en uitlegbaarheid" }).first()).toBeVisible();
+ await expect(page.getByText("Commit eerst, orkestreer daarna")).toBeVisible();
+ await expect(page.getByText("AI moet bewijs tonen")).toBeVisible();
+ await expect(page.getByText("PostgreSQL + audittrail")).toBeVisible();
+ await expect(page.getByText(/nog niet live gekoppeld/)).toHaveCount(0);
+ await expect(page.getByText(/unknown procedures/)).toHaveCount(0);
+});
+
+test("mobile recruiter surfaces remain readable without horizontal overflow", async ({ page }) => {
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/login");
+ await page.getByRole("button", { name: "Bekijk de highlights in 90 seconden" }).click();
+ await expect(page).toHaveURL(/\/highlights$/);
+
+ const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
+ expect(overflow).toBeLessThanOrEqual(1);
+ await expect(page.locator(".highlight-card").first()).toBeVisible();
+});
diff --git a/frontend/index.html b/frontend/index.html
index 373547b..919576e 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,9 +3,18 @@
-
+
+
+
+
+
+
+
+
+
+
- Fleet Ops
+ Fleet Ops — connected vehicle operations
diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts
index 4b78c66..f8a79f2 100644
--- a/frontend/playwright.config.ts
+++ b/frontend/playwright.config.ts
@@ -4,7 +4,7 @@ export default defineConfig({
testDir: "./e2e",
// _capture-screenshots.spec.ts is evidence-generation tooling, not a regression test —
// it mutates demo data via a reset and is run explicitly, not as part of the suite.
- testIgnore: "**/_*.spec.ts",
+ testIgnore: process.env.CAPTURE_EVIDENCE === "1" ? undefined : "**/_*.spec.ts",
timeout: 30_000,
fullyParallel: false,
workers: 1,
diff --git a/frontend/public/og-fleet-ops.svg b/frontend/public/og-fleet-ops.svg
new file mode 100644
index 0000000..3b6d594
--- /dev/null
+++ b/frontend/public/og-fleet-ops.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index ffa2b4d..9884382 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -21,9 +21,10 @@ const AboutDemo = lazy(() => import("./pages/AboutDemo").then((module) => ({ def
const Scenarios = lazy(() => import("./pages/Scenarios").then((module) => ({ default: module.Scenarios })));
const Users = lazy(() => import("./pages/Users").then((module) => ({ default: module.Users })));
const Privacy = lazy(() => import("./pages/Privacy").then((module) => ({ default: module.Privacy })));
+const Highlights = lazy(() => import("./pages/Highlights").then((module) => ({ default: module.Highlights })));
function deferredPage(element: ReactNode) {
- return }>{element};
+ return