diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md
index d09c139..de4e22a 100644
--- a/PROJECT_STATE.md
+++ b/PROJECT_STATE.md
@@ -688,3 +688,51 @@ scenarios, demo manifest, About page). Gap audit: `docs/demo-release/current-dem
- Exact next action: plain-language integration-status labels, richer audit narration,
the full "Over deze demo" page content (currently a first pass from Batch 2), and
wiring reset into the guide/About/OM menu narrative — task #34.
+
+### Batch 5 — integration-status UX, audit UX, About page, reset integrity (complete)
+
+- Plain-language integration status: extracted `frontend/src/data/integrationLabels.ts`
+ (`N8N_STATE_META`/`MCP_STATE_META`) mapping raw backend states to honest labels
+ ("Operational"/"Not connected"/"Prepared"/"Delivery failed"/"Retry available") while
+ keeping each mapped onto an existing `.status-*` CSS colour class (a few raw values like
+ `degraded`/`disabled`/`configured` had no matching CSS rule at all before this — a real,
+ pre-existing colour-coding gap). `StatusBadge` gained an optional `label` override prop
+ (backward compatible) so the badge's colour class and its displayed text can differ.
+ Wired into both `Automation.tsx` and `Dashboard.tsx`'s integration cards; also renamed
+ the "RAGcore" card heading to "Knowledge assistant" and made its text honestly name the
+ actual active provider (same bug class fixed in Knowledge.tsx in Batch 4).
+- Audit trail: added a "Follow-up" column with a "View related events" action per row that
+ filters the same list by `correlation_id` (reuses the backend's existing, already-tested
+ `correlation_id` query param — no new business logic), with a "Clear this filter"
+ affordance. This is how a visitor sees "what else happened as a result of this action"
+ (e.g. a return's linked vehicle-status-changed / workflow-queued events) without a
+ bigger grouped-timeline rebuild.
+- About page: added target-audience/scope, a short architecture summary, security
+ principles, and a testing-approach section (previously only covered the fictional
+ problem/real/synthetic/integrations/reset); added a "Start begeleide demo" CTA for
+ Operations Managers that opens the Demo Guide directly from this page.
+- Reset integrity: added `scenario_integrity_report()` (`backend/app/services/
+ demo_manifest.py`), reusing the exact same scenario-readiness derivation the manifest
+ and scenario overview already use (so it can't drift), and wired it into `POST
+ /api/v1/demo/reset` — both the response body and the `demo_reset` audit event's
+ metadata now carry `scenario_integrity: {all_ready, not_ready}`. This is the
+ server-side post-reset integrity check the brief asks for; visible today via the audit
+ event's raw-detail view, satisfying the requirement without adding a UI banner to a
+ flow that immediately logs the user out and redirects to `/login`.
+- **Fixed a second real regression this batch, caught by the existing return-review
+ e2e test**: restructured the odometer pre-fill so `BookingDetail.tsx` withholds
+ rendering `ReturnForm` until the scenario's canonical odometer has resolved (with a
+ brief "Scenario voorbereiden…" loading state), instead of mounting the form immediately
+ and patching its value in asynchronously. The previous approach raced visibly with
+ Playwright's `fill()` (and would have raced with a real visitor typing quickly),
+ producing a corrupted concatenated value in one observed failure. This also let the
+ now-unnecessary `odometerEditedByUser` ref guard be removed — simpler and more robust
+ than the effect-based patch it replaced.
+- Evidence: `pytest` **127 passed**, `ruff check .` clean, `mypy app` clean (48 files);
+ frontend `tsc -b` clean, `npm run build` clean; full Playwright suite **51 passed**,
+ confirmed stable across three consecutive full runs (given how many timing races this
+ batch and the previous one surfaced, stability was verified deliberately rather than
+ assumed from a single green run).
+- Exact next action: full guided-demo Playwright test + remaining targeted demo tests per
+ section 19 (mobile guide, keyboard nav, all scenario flows, About page, accessibility/
+ reduced-motion/console/network checks) — task #35.
diff --git a/backend/app/api/routers/demo.py b/backend/app/api/routers/demo.py
index 97c5a78..fd06a68 100644
--- a/backend/app/api/routers/demo.py
+++ b/backend/app/api/routers/demo.py
@@ -14,7 +14,7 @@ from app.models.user import User
from app.schemas import CurrentUser, DemoLoginRequest, DemoManifestOut
from app.seed_loader import reset_and_seed
from app.services.audit import record_audit_event
-from app.services.demo_manifest import build_demo_manifest
+from app.services.demo_manifest import build_demo_manifest, scenario_integrity_report
router = APIRouter(prefix="/api/v1/demo", tags=["demo"])
settings = get_settings()
@@ -107,6 +107,7 @@ def demo_reset(
detail="Demo reset is disabled on this deployment.",
)
result = reset_and_seed(db)
+ integrity = scenario_integrity_report(db)
record_audit_event(
db,
actor_type="user",
@@ -116,6 +117,7 @@ def demo_reset(
metadata={
"counts": result.counts,
"anchor_date": result.anchor_date.isoformat(),
+ "scenario_integrity": integrity,
},
)
db.commit()
@@ -125,4 +127,5 @@ def demo_reset(
"counts": result.counts,
"anchor_date": result.anchor_date.isoformat(),
"seeded_at": result.seeded_at.isoformat(),
+ "scenario_integrity": integrity,
}
diff --git a/backend/app/services/demo_manifest.py b/backend/app/services/demo_manifest.py
index f9b38b2..d3e2106 100644
--- a/backend/app/services/demo_manifest.py
+++ b/backend/app/services/demo_manifest.py
@@ -223,6 +223,20 @@ def _integrations(db: Session) -> list[DemoIntegrationSummaryOut]:
]
+def scenario_integrity_report(db: Session) -> dict:
+ """Server-side scenario-integrity check run after every reset (section 15): confirms
+ each of the 5 named scenarios is actually present and ready, rather than trusting the
+ seed loader silently. Reuses the same readiness derivation the manifest/scenario
+ overview already use, so this can never drift from what a visitor actually sees."""
+ scenarios = _scenarios(db)
+ not_ready = [
+ {"id": s.id, "title": s.title, "reason": s.blocked_reason}
+ for s in scenarios
+ if not s.ready
+ ]
+ return {"all_ready": len(not_ready) == 0, "not_ready": not_ready}
+
+
def build_demo_manifest(db: Session) -> DemoManifestOut:
last_reset_at, anchor_date = _last_reset(db)
return DemoManifestOut(
diff --git a/backend/tests/test_auth.py b/backend/tests/test_auth.py
index 143757f..4cdd169 100644
--- a/backend/tests/test_auth.py
+++ b/backend/tests/test_auth.py
@@ -17,9 +17,12 @@ def test_rental_employee_cannot_reset_demo(employee_client):
def test_operations_manager_can_reset_demo(ops_client):
response = ops_client.post("/api/v1/demo/reset")
assert response.status_code == 200
- assert response.json()["counts"]["vehicles"] == 50
- assert response.json()["anchor_date"]
- assert response.json()["seeded_at"]
+ body = response.json()
+ assert body["counts"]["vehicles"] == 50
+ assert body["anchor_date"]
+ assert body["seeded_at"]
+ assert body["scenario_integrity"]["all_ready"] is True
+ assert body["scenario_integrity"]["not_ready"] == []
def test_reset_is_rejected_when_demo_allow_reset_is_disabled(ops_client, monkeypatch):
diff --git a/frontend/src/components/Badge.tsx b/frontend/src/components/Badge.tsx
index 7cbfc09..b0e1eea 100644
--- a/frontend/src/components/Badge.tsx
+++ b/frontend/src/components/Badge.tsx
@@ -3,6 +3,6 @@ export function SeverityBadge({ severity }: { severity: "low" | "medium" | "high
return {label} severity;
}
-export function StatusBadge({ status }: { status: string }) {
- return {status.replace(/_/g, " ")};
+export function StatusBadge({ status, label }: { status: string; label?: string }) {
+ return {label ?? status.replace(/_/g, " ")};
}
diff --git a/frontend/src/components/ReturnForm.tsx b/frontend/src/components/ReturnForm.tsx
index 3694691..4198061 100644
--- a/frontend/src/components/ReturnForm.tsx
+++ b/frontend/src/components/ReturnForm.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState, type FormEvent } from "react";
+import { useState, type FormEvent } from "react";
import { Link, useNavigate } from "react-router-dom";
import { api, ApiError } from "../api/client";
import type { RegisterReturnRequest, RegisterReturnResult, ReturnPreviewResult } from "../api/types";
@@ -95,7 +95,6 @@ export function ReturnForm({
suggestedOdometerKm?: number;
}) {
const [odometer, setOdometer] = useState(suggestedOdometerKm !== undefined ? String(suggestedOdometerKm) : "");
- const odometerEditedByUser = useRef(false);
const [fuel, setFuel] = useState("50");
const [cleanlinessOk, setCleanlinessOk] = useState(true);
const [damageReported, setDamageReported] = useState(false);
@@ -108,16 +107,6 @@ export function ReturnForm({
const [step, setStep] = useState<"capture" | "review">("capture");
const [preview, setPreview] = useState(null);
- useEffect(() => {
- // suggestedOdometerKm arrives asynchronously (BookingDetail fetches the vehicle's
- // canonical odometer after this form has already mounted), so the initial useState
- // value alone won't catch it -- sync it once it resolves. Never overwrite a value the
- // visitor has already started typing.
- if (suggestedOdometerKm !== undefined && !odometerEditedByUser.current) {
- setOdometer(String(suggestedOdometerKm));
- }
- }, [suggestedOdometerKm]);
-
function currentBody(): RegisterReturnRequest {
return {
end_odometer_km: Number(odometer),
@@ -184,10 +173,7 @@ export function ReturnForm({
required
min={0}
value={odometer}
- onChange={(e) => {
- odometerEditedByUser.current = true;
- setOdometer(e.target.value);
- }}
+ onChange={(e) => setOdometer(e.target.value)}
/>
diff --git a/frontend/src/data/integrationLabels.ts b/frontend/src/data/integrationLabels.ts
new file mode 100644
index 0000000..3125236
--- /dev/null
+++ b/frontend/src/data/integrationLabels.ts
@@ -0,0 +1,18 @@
+import type { IntegrationStatus } from "../api/types";
+
+// Plain-language status per section 12 of the demo brief: a visitor shouldn't have to
+// decode raw backend state strings to tell whether an integration is actually working.
+// `statusClass` maps onto an existing `.status-*` CSS modifier so the badge keeps
+// correct colour-coding; `label` is the human-readable text shown instead of the raw value.
+export const N8N_STATE_META: Record = {
+ disabled: { statusClass: "not_configured", label: "Not connected" },
+ unavailable: { statusClass: "unavailable", label: "Delivery failed" },
+ degraded: { statusClass: "needs_attention", label: "Retry available" },
+ operational: { statusClass: "available", label: "Operational" },
+ no_evidence: { statusClass: "no_events", label: "Prepared" },
+};
+
+export const MCP_STATE_META: Record = {
+ not_configured: { statusClass: "not_configured", label: "Not connected" },
+ configured: { statusClass: "no_events", label: "Prepared" },
+};
diff --git a/frontend/src/pages/AboutDemo.tsx b/frontend/src/pages/AboutDemo.tsx
index 5c2733e..e38d8f6 100644
--- a/frontend/src/pages/AboutDemo.tsx
+++ b/frontend/src/pages/AboutDemo.tsx
@@ -1,4 +1,6 @@
+import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
+import { useDemoGuide } from "../context/DemoGuideContext";
import { useDemoManifest } from "../context/DemoManifestContext";
import { Icon } from "../components/Icons";
import { IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
@@ -21,6 +23,7 @@ const INTEGRATION_ICON: Record = {
export function AboutDemo() {
const { manifest, loading } = useDemoManifest();
const { user } = useAuth();
+ const { openGuide } = useDemoGuide();
return (
De gegidste demo doorloopt alle acht stappen hierboven in de praktijk.
+
+
+
+ )}
+
Het fictieve probleem
@@ -50,6 +65,17 @@ export function AboutDemo() {
+
+
Voor wie en met welke scope
+
+ Deze demo is bedoeld voor wie wil zien hoe MobilityOps operationele problemen bij
+ een kleine verhuurder aanpakt: Operations Managers en Rental Employees, en
+ iedereen die de aanpak evalueert. De scope is bewust afgebakend tot één
+ samenhangende proof of concept — geen boekhouding, geen betalingen, geen publieke
+ reservaties, geen volledig CRM of ERP.
+
+
+
Wat écht werkt
@@ -72,6 +98,37 @@ export function AboutDemo() {
+
+
Architectuur in het kort
+
+ Een React/TypeScript-frontend praat met een FastAPI-backend (PostgreSQL via
+ SQLAlchemy/Alembic-migraties); belangrijke bedrijfsregels leven in de backend, niet
+ in n8n of in prompts. Retours en andere gebeurtenissen worden eerst lokaal
+ gecommit en pas daarna asynchroon via een outbox-patroon aan n8n afgeleverd, zodat
+ een tijdelijke storing in de automatisering nooit een operationele actie blokkeert.
+
+
+
+
+
Beveiliging en toegang
+
+ Toegang verloopt via ondertekende, HTTP-only sessiecookies per rol; elke rol
+ gebonden aan een set toegestane routes, zowel serverzijdig afgedwongen als in de
+ navigatie weerspiegeld. Belangrijke statuswijzigingen worden altijd gecontroleerd
+ en gelogd — nooit stilzwijgend automatisch gecorrigeerd.
+
+
+
+
+
Hoe dit getest is
+
+ Een geautomatiseerde backend-testsuite dekt bedrijfsregels en API-contracten;
+ een volledige Playwright-eindtot-eind-suite dekt de gebruikersstromen, inclusief
+ deze demo-ervaring zelf. Elke wijziging wordt bovendien tegen een schone checkout
+ (lege database, opnieuw opgebouwd vanaf de seed-data) gevalideerd voor deployment.
+
{e.correlation_id.slice(0, 8)}
diff --git a/frontend/src/pages/Automation.tsx b/frontend/src/pages/Automation.tsx
index d47727c..e48ffc7 100644
--- a/frontend/src/pages/Automation.tsx
+++ b/frontend/src/pages/Automation.tsx
@@ -4,14 +4,7 @@ import type { AutomationRun, IntegrationStatus, KnowledgeHealth } from "../api/t
import { StatusBadge } from "../components/Badge";
import { useAuth } from "../context/AuthContext";
import { ErrorState, IntegrationMark, LoadingState, PageHeader, SectionHeading } from "../components/PageChrome";
-
-const N8N_STATE_LABEL: Record = {
- disabled: "disabled",
- unavailable: "unavailable",
- degraded: "degraded",
- operational: "available",
- no_evidence: "no_events",
-};
+import { N8N_STATE_META, MCP_STATE_META } from "../data/integrationLabels";
export function Automation() {
const { user } = useAuth();
@@ -98,9 +91,38 @@ export function Automation() {
: "Return events are committed locally first and then delivered through the outbox."}
-
+ {(() => {
+ const meta = integrationStatus ? N8N_STATE_META[integrationStatus.n8n.state] : null;
+ return (
+
+ );
+ })()}
+
+
+
+
+ Knowledge
+
Knowledge assistant
+
+ {knowledge
+ ? `${knowledge.provider === "ragcore" ? "RAGcore" : "Demo knowledge base"} · ${knowledge.document_count} procedures indexed in ${knowledge.collection}.`
+ : "Health evidence is currently unavailable."}
+
+
+
-
Knowledge
RAGcore
{knowledge ? `${knowledge.document_count} procedures indexed in ${knowledge.collection}.` : "Health evidence is currently unavailable."}
@@ -109,10 +131,13 @@ export function Automation() {
{integrationStatus?.mcp_hub.registration_enabled
? "Registration is enabled for this deployment."
- : "No active MobilityOps adapter is configured in this proof of concept."}
+ : "Not yet connected — prepared for future controlled tool calls from the ITWorx MCP Hub."}
-
+ {(() => {
+ const meta = integrationStatus ? MCP_STATE_META[integrationStatus.mcp_hub.state] : null;
+ return ;
+ })()}
diff --git a/frontend/src/pages/BookingDetail.tsx b/frontend/src/pages/BookingDetail.tsx
index 866c8e2..1b7ec39 100644
--- a/frontend/src/pages/BookingDetail.tsx
+++ b/frontend/src/pages/BookingDetail.tsx
@@ -84,7 +84,14 @@ export function BookingDetail() {
)}
{returnResult && }
- {!returnResult && booking.status === "active" && (
+ {/* Wait for the scenario's canonical odometer before mounting the form at all when
+ this is the known demo scenario, so ReturnForm's suggestedOdometerKm is correct
+ from its very first render -- never updated asynchronously after mount, which
+ previously raced with anyone already typing into the field. */}
+ {!returnResult && booking.status === "active" && isReturnAnomalyScenario && canonicalOdometerKm === null && (
+
+ )}
+ {!returnResult && booking.status === "active" && (!isReturnAnomalyScenario || canonicalOdometerKm !== null) && (
= [
{ key: "available", label: "Available", tone: "ready" },
@@ -178,24 +179,37 @@ export function Dashboard() {
: "No workflow evidence recorded"}
+ {(() => {
+ const meta = integrationStatus ? N8N_STATE_META[integrationStatus.n8n.state] : null;
+ return (
+
+ );
+ })()}
+
+