From 1867828a9d7157c4dac6ad807c94ffb274a2a95f Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Sun, 2 Aug 2026 06:42:27 +0200 Subject: [PATCH] feat(demo): add reset UI and wire truthful integration status into pages Add a "Reset demo data" action to the sidebar (Operations Manager only, explicit confirmation, progress, error handling) -- POST /api/v1/demo/reset already existed and was already role-gated server-side, but had no UI trigger. Reset invalidates the acting session server-side, so the flow signs the user out and returns them to login afterward. Wire the new GET /api/v1/integrations/status into Automation.tsx and Dashboard.tsx so both show the aggregate n8n state instead of the most recent event's status, and the MCP Hub card reflects the actual registration_enabled setting instead of a hardcoded "not configured" label. --- frontend/e2e/interactive-elements.spec.ts | 24 +++++++++++ frontend/src/pages/Automation.tsx | 51 +++++++++++++++++++++-- frontend/src/pages/Dashboard.tsx | 43 +++++++++++++++++-- 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/frontend/e2e/interactive-elements.spec.ts b/frontend/e2e/interactive-elements.spec.ts index 4c24ab0..143e870 100644 --- a/frontend/e2e/interactive-elements.spec.ts +++ b/frontend/e2e/interactive-elements.spec.ts @@ -362,6 +362,30 @@ test("rental employee role has a restricted nav and cannot reach manager-only pa await page.goto("/audit"); await expect(page.getByText("Audit history is visible to Operations Managers only.")).toBeVisible(); + + await expect(page.getByRole("button", { name: "Reset demo data" })).toHaveCount(0); +}); + +test("operations manager can reset demo data and is returned to login", async ({ page }) => { + await expect(page.getByRole("button", { name: "Reset demo data" })).toBeVisible(); + await page.getByRole("button", { name: "Reset demo data" }).click(); + await page.getByRole("button", { name: "Yes, reset" }).click(); + + await expect(page).toHaveURL(/\/login$/); + + // The reset must not have affected the ability to log back in against fresh data. + await page.getByRole("button", { name: "Open as Operations Manager" }).click(); + await expect(page).toHaveURL(/\/dashboard$/); +}); + +test("automation page shows the aggregate n8n integration status, not just the latest event", async ({ + page, + request, +}) => { + await resetDemoData(request); + await page.goto("/automation"); + await expect(page.locator(".integration-cards")).toContainText("succeeded"); + await expect(page.locator(".integration-cards")).toContainText("failed"); }); test("rental employee direct API access to manager-only endpoints is rejected", async ({ diff --git a/frontend/src/pages/Automation.tsx b/frontend/src/pages/Automation.tsx index 8630be1..d47727c 100644 --- a/frontend/src/pages/Automation.tsx +++ b/frontend/src/pages/Automation.tsx @@ -1,10 +1,18 @@ import { useCallback, useEffect, useState } from "react"; import { api, ApiError } from "../api/client"; -import type { AutomationRun, KnowledgeHealth } from "../api/types"; +import type { AutomationRun, IntegrationStatus, KnowledgeHealth } from "../api/types"; 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", +}; + export function Automation() { const { user } = useAuth(); const [runs, setRuns] = useState(null); @@ -13,6 +21,7 @@ export function Automation() { const [retryError, setRetryError] = useState(null); const [retrying, setRetrying] = useState(null); const [knowledge, setKnowledge] = useState(null); + const [integrationStatus, setIntegrationStatus] = useState(null); const load = useCallback(() => { setRuns(null); @@ -39,12 +48,24 @@ export function Automation() { api.get("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null)); }, []); + const loadIntegrationStatus = useCallback(() => { + api + .get("/api/v1/integrations/status") + .then(setIntegrationStatus) + .catch(() => setIntegrationStatus(null)); + }, []); + + useEffect(() => { + loadIntegrationStatus(); + }, [loadIntegrationStatus]); + async function handleRetry(eventId: string) { setRetryError(null); setRetrying(eventId); try { await api.post(`/api/v1/workflows/${eventId}/retry`); load(); + loadIntegrationStatus(); } catch (err) { setRetryError(err instanceof ApiError ? err.message : "Could not retry this delivery."); } finally { @@ -66,9 +87,33 @@ export function Automation() {
-
Orchestration

n8n delivery

Return events are committed locally first and then delivered through the outbox.

+
+ +
+ Orchestration +

n8n delivery

+

+ {integrationStatus + ? `${integrationStatus.n8n.succeeded} succeeded · ${integrationStatus.n8n.failed} failed · ${integrationStatus.n8n.pending} pending · ${integrationStatus.n8n.delivering} delivering.` + : "Return events are committed locally first and then delivered through the outbox."} +

+
+ +
Knowledge

RAGcore

{knowledge ? `${knowledge.document_count} procedures indexed in ${knowledge.collection}.` : "Health evidence is currently unavailable."}

-
Tool gateway

MCP Hub

No active MobilityOps adapter is configured in this proof of concept.

+
+ +
+ Tool gateway +

MCP Hub

+

+ {integrationStatus?.mcp_hub.registration_enabled + ? "Registration is enabled for this deployment." + : "No active MobilityOps adapter is configured in this proof of concept."} +

+
+ +
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 483d695..54751d7 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api } from "../api/client"; -import type { Dashboard as DashboardData, KnowledgeHealth } from "../api/types"; +import type { Dashboard as DashboardData, IntegrationStatus, KnowledgeHealth } from "../api/types"; import { useAuth } from "../context/AuthContext"; import { SeverityBadge, StatusBadge } from "../components/Badge"; import { Icon } from "../components/Icons"; @@ -30,6 +30,7 @@ export function Dashboard() { const canSeeAutomation = user?.role === "operations_manager"; const [data, setData] = useState(null); const [knowledge, setKnowledge] = useState(null); + const [integrationStatus, setIntegrationStatus] = useState(null); const [error, setError] = useState(null); const [severity, setSeverity] = useState("all"); const [query, setQuery] = useState(""); @@ -39,6 +40,14 @@ export function Dashboard() { api.get("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null)); }, []); + useEffect(() => { + if (!canSeeAutomation) return; + api + .get("/api/v1/integrations/status") + .then(setIntegrationStatus) + .catch(() => setIntegrationStatus(null)); + }, [canSeeAutomation]); + const attention = useMemo(() => data?.attention_items.filter((item) => { const matchesSeverity = severity === "all" || item.severity === severity; const haystack = `${item.title} ${item.detail} ${item.link_ref}`.toLowerCase(); @@ -122,9 +131,37 @@ export function Dashboard() {
System detail : undefined} />
    -
  • n8n delivery{latestRun ? `Latest event ${latestRun.aggregate_ref}` : "No workflow evidence recorded"}
  • +
  • + +
    + n8n delivery + + {integrationStatus + ? `${integrationStatus.n8n.succeeded} succeeded · ${integrationStatus.n8n.failed} failed` + : latestRun + ? `Latest event ${latestRun.aggregate_ref}` + : "No workflow evidence recorded"} + +
    + +
  • RAGcore knowledge{knowledge ? `${knowledge.document_count} procedures indexed` : "Health check unavailable"}
  • -
  • MCP HubNo active adapter in this PoC
  • +
  • + +
    + MCP Hub + {integrationStatus?.mcp_hub.registration_enabled ? "Registration enabled" : "No active adapter in this PoC"} +
    + +