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.
This commit is contained in:
@@ -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 ({
|
||||
|
||||
@@ -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<IntegrationStatus["n8n"]["state"], string> = {
|
||||
disabled: "disabled",
|
||||
unavailable: "unavailable",
|
||||
degraded: "degraded",
|
||||
operational: "available",
|
||||
no_evidence: "no_events",
|
||||
};
|
||||
|
||||
export function Automation() {
|
||||
const { user } = useAuth();
|
||||
const [runs, setRuns] = useState<AutomationRun[] | null>(null);
|
||||
@@ -13,6 +21,7 @@ export function Automation() {
|
||||
const [retryError, setRetryError] = useState<string | null>(null);
|
||||
const [retrying, setRetrying] = useState<string | null>(null);
|
||||
const [knowledge, setKnowledge] = useState<KnowledgeHealth | null>(null);
|
||||
const [integrationStatus, setIntegrationStatus] = useState<IntegrationStatus | null>(null);
|
||||
|
||||
const load = useCallback(() => {
|
||||
setRuns(null);
|
||||
@@ -39,12 +48,24 @@ export function Automation() {
|
||||
api.get<KnowledgeHealth>("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null));
|
||||
}, []);
|
||||
|
||||
const loadIntegrationStatus = useCallback(() => {
|
||||
api
|
||||
.get<IntegrationStatus>("/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() {
|
||||
<PageHeader eyebrow="Assurance / Integrations" title="Integration control" description="Monitor delivery health, graceful degradation and retryable workflow events." />
|
||||
|
||||
<section className="integration-cards" aria-label="Integration health">
|
||||
<article><IntegrationMark kind="n8n" /><div><span className="integration-kicker">Orchestration</span><h2>n8n delivery</h2><p>Return events are committed locally first and then delivered through the outbox.</p></div><StatusBadge status={runs?.[0]?.status ?? "no_events"} /></article>
|
||||
<article>
|
||||
<IntegrationMark kind="n8n" />
|
||||
<div>
|
||||
<span className="integration-kicker">Orchestration</span>
|
||||
<h2>n8n delivery</h2>
|
||||
<p>
|
||||
{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."}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={integrationStatus ? N8N_STATE_LABEL[integrationStatus.n8n.state] : (runs?.[0]?.status ?? "no_events")} />
|
||||
</article>
|
||||
<article><IntegrationMark kind="rag" /><div><span className="integration-kicker">Knowledge</span><h2>RAGcore</h2><p>{knowledge ? `${knowledge.document_count} procedures indexed in ${knowledge.collection}.` : "Health evidence is currently unavailable."}</p></div><StatusBadge status={knowledge?.available ? "available" : "unavailable"} /></article>
|
||||
<article><IntegrationMark kind="mcp" /><div><span className="integration-kicker">Tool gateway</span><h2>MCP Hub</h2><p>No active MobilityOps adapter is configured in this proof of concept.</p></div><StatusBadge status="not_configured" /></article>
|
||||
<article>
|
||||
<IntegrationMark kind="mcp" />
|
||||
<div>
|
||||
<span className="integration-kicker">Tool gateway</span>
|
||||
<h2>MCP Hub</h2>
|
||||
<p>
|
||||
{integrationStatus?.mcp_hub.registration_enabled
|
||||
? "Registration is enabled for this deployment."
|
||||
: "No active MobilityOps adapter is configured in this proof of concept."}
|
||||
</p>
|
||||
</div>
|
||||
<StatusBadge status={integrationStatus?.mcp_hub.state ?? "not_configured"} />
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<SectionHeading title="Delivery ledger" description="Persisted outbox attempts with the latest failure evidence." />
|
||||
|
||||
@@ -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<DashboardData | null>(null);
|
||||
const [knowledge, setKnowledge] = useState<KnowledgeHealth | null>(null);
|
||||
const [integrationStatus, setIntegrationStatus] = useState<IntegrationStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [severity, setSeverity] = useState("all");
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -39,6 +40,14 @@ export function Dashboard() {
|
||||
api.get<KnowledgeHealth>("/api/v1/knowledge/status").then(setKnowledge).catch(() => setKnowledge(null));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!canSeeAutomation) return;
|
||||
api
|
||||
.get<IntegrationStatus>("/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() {
|
||||
<section className="work-panel integration-panel" aria-labelledby="integration-heading">
|
||||
<SectionHeading title="Integration pulse" description="Current evidence from connected services" action={canSeeAutomation ? <Link to="/automation">System detail <Icon name="chevron" /></Link> : undefined} />
|
||||
<ul className="integration-list">
|
||||
<li><IntegrationMark kind="n8n" /><div><strong>n8n delivery</strong><span>{latestRun ? `Latest event ${latestRun.aggregate_ref}` : "No workflow evidence recorded"}</span></div><StatusBadge status={n8nState.toLowerCase().replace(/ /g, "_")} /></li>
|
||||
<li>
|
||||
<IntegrationMark kind="n8n" />
|
||||
<div>
|
||||
<strong>n8n delivery</strong>
|
||||
<span>
|
||||
{integrationStatus
|
||||
? `${integrationStatus.n8n.succeeded} succeeded · ${integrationStatus.n8n.failed} failed`
|
||||
: latestRun
|
||||
? `Latest event ${latestRun.aggregate_ref}`
|
||||
: "No workflow evidence recorded"}
|
||||
</span>
|
||||
</div>
|
||||
<StatusBadge
|
||||
status={
|
||||
integrationStatus
|
||||
? integrationStatus.n8n.state === "operational"
|
||||
? "available"
|
||||
: integrationStatus.n8n.state
|
||||
: n8nState.toLowerCase().replace(/ /g, "_")
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
<li><IntegrationMark kind="rag" /><div><strong>RAGcore knowledge</strong><span>{knowledge ? `${knowledge.document_count} procedures indexed` : "Health check unavailable"}</span></div><StatusBadge status={knowledge?.available ? "available" : "unavailable"} /></li>
|
||||
<li><IntegrationMark kind="mcp" /><div><strong>MCP Hub</strong><span>No active adapter in this PoC</span></div><StatusBadge status="not_configured" /></li>
|
||||
<li>
|
||||
<IntegrationMark kind="mcp" />
|
||||
<div>
|
||||
<strong>MCP Hub</strong>
|
||||
<span>{integrationStatus?.mcp_hub.registration_enabled ? "Registration enabled" : "No active adapter in this PoC"}</span>
|
||||
</div>
|
||||
<StatusBadge status={integrationStatus?.mcp_hub.state ?? "not_configured"} />
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user