From 805501ae5d22b67eb81c5af258837d08ac249cc2 Mon Sep 17 00:00:00 2001 From: Aldon Smith Date: Fri, 22 May 2026 10:34:01 -0400 Subject: [PATCH] Add demo API connection banner --- apps/web/README.md | 14 +++- apps/web/app/components/demo-state.tsx | 80 +++++++++++++++++- apps/web/app/globals.css | 83 +++++++++++++++++++ apps/web/app/layout.tsx | 2 + apps/web/app/page.tsx | 13 ++- .../web/e2e/operations-workbench-demo.spec.ts | 5 ++ apps/web/lib/api-client.ts | 3 + apps/web/tests/app-shell.test.mjs | 14 ++++ docs/LEARNING_LOG.md | 47 +++++++++++ .../demo/OPERATIONS_WORKBENCH_DEMO_RUNBOOK.md | 39 ++++++--- 10 files changed, 281 insertions(+), 19 deletions(-) diff --git a/apps/web/README.md b/apps/web/README.md index f548ed5..e8fc77f 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -64,8 +64,15 @@ The API client currently covers: - `POST /recommendations/{recommendation_id}/defer` - `GET /reports/rca-capa-drafts/{detection_id}` +The Overview page shows the configured API target, `/health` status, and +simulator-backed demo source so a presenter can confirm the browser is reading +from the expected local backend. + When the backend is unavailable, the demo pages render a readable API connection -message instead of failing silently. +message instead of failing silently. The recovery panel shows the configured +target and prompts the presenter to run `make demo`, start the API with +`make api`, and restart the Workbench with `NEXT_PUBLIC_API_BASE_URL` if the API +is running on a non-default port. ## Routes @@ -85,8 +92,9 @@ message instead of failing silently. - `/rca-capa-draft?detection_id={detection_id}` - RCA/CAPA draft preview scoped to the selected Process Sentinel detection -The shell labels all demo content as simulator-backed data. The pages include -loading states, empty states, and simple user-readable API error states. +The shell labels all demo content as simulator-backed data and states that the +scenario is synthetic local data, not real plant data. The pages include loading +states, empty states, and simple user-readable API error states. ## Governance Decision Feedback diff --git a/apps/web/app/components/demo-state.tsx b/apps/web/app/components/demo-state.tsx index 2b8d9dd..598e657 100644 --- a/apps/web/app/components/demo-state.tsx +++ b/apps/web/app/components/demo-state.tsx @@ -1,7 +1,20 @@ +import type { HealthResponse } from "../../lib/api-client"; +import { getApiBaseUrl } from "../../lib/api-client"; + type ApiErrorPanelProps = { + apiBaseUrl?: string; message: string; }; +type ApiConnectionBannerProps = { + apiBaseUrl?: string; + health?: HealthResponse; +}; + +type DemoDataBadgeProps = { + label?: string; +}; + type EmptyStateProps = { title: string; text: string; @@ -23,15 +36,76 @@ type StatusBadgeProps = { value: string; }; -export function ApiErrorPanel({ message }: ApiErrorPanelProps) { +export function ApiConnectionBanner({ + apiBaseUrl = getApiBaseUrl(), + health, +}: ApiConnectionBannerProps) { + return ( +
+
+ Local API connected + + Workbench data is coming from the configured FastAPI target for this + simulator-backed demo. + +
+
+
+
API target
+
{apiBaseUrl}
+
+
+
Health
+
{health?.status ?? "Not checked"}
+
+
+
Source
+
+ {health?.simulator_backed + ? "Simulator-backed demo API" + : "Simulator-backed demo API expected"} +
+
+
+
+ ); +} + +export function ApiErrorPanel({ + apiBaseUrl = getApiBaseUrl(), + message, +}: ApiErrorPanelProps) { return (
API connection issue - {message} + + The Workbench could not reach the local simulator-backed API at{" "} + {apiBaseUrl}. + + + Start the demo state with make demo, start the API with{" "} + make api, then refresh this page. If the API is using a + different port, restart the Workbench with{" "} + NEXT_PUBLIC_API_BASE_URL set to that target. + + Details: {message}
); } +export function DemoDataBadge({ + label = "Simulator-backed demo data", +}: DemoDataBadgeProps) { + return ( + + {label} + + Synthetic local scenario; not real plant data. + + + ); +} + export function EmptyState({ title, text }: EmptyStateProps) { return (
@@ -44,7 +118,7 @@ export function EmptyState({ title, text }: EmptyStateProps) { export function LoadingState({ title = "Loading simulator-backed demo data" }: LoadingStateProps) { return (
- Simulator-backed demo data +
{title} Connecting to the local FastAPI demo backend. diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index 6663c98..b905754 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -143,6 +143,29 @@ a { padding: 7px 8px; } +.demo-notice { + display: inline-grid; + width: fit-content; + gap: 4px; + border: 1px solid #d9c18c; + border-radius: 7px; + background: #fff8e7; + color: var(--warning); + line-height: 1.2; + padding: 7px 9px; +} + +.demo-notice-label { + font-size: 0.78rem; + font-weight: 760; +} + +.demo-notice-copy { + color: #765011; + font-size: 0.72rem; + font-weight: 650; +} + .status-badge { display: inline-flex; width: fit-content; @@ -337,6 +360,58 @@ h3 { margin-top: 24px; } +.api-connection-banner { + display: grid; + grid-template-columns: minmax(0, 0.9fr) minmax(320px, 1.1fr); + gap: 18px; + align-items: start; + margin-top: 18px; + border: 1px solid #b7d4c8; + border-left: 4px solid var(--accent); + border-radius: 8px; + background: #f3faf6; + padding: 16px; +} + +.api-connection-banner strong { + display: block; + margin-bottom: 5px; +} + +.api-connection-banner span, +.api-connection-details dd { + color: #244337; + line-height: 1.45; +} + +.api-connection-details { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin: 0; +} + +.api-connection-details div { + display: grid; + gap: 4px; + min-width: 0; + border-left: 1px solid #b7d4c8; + padding-left: 10px; +} + +.api-connection-details dt { + color: #4d665b; + font-size: 0.74rem; + font-weight: 760; + text-transform: uppercase; +} + +.api-connection-details dd { + margin: 0; + overflow-wrap: anywhere; + font-weight: 720; +} + .overview-panel, .metric-card { border: 1px solid var(--border); @@ -923,6 +998,10 @@ code { justify-content: flex-start; } + .api-connection-banner { + grid-template-columns: 1fr; + } + .content-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -986,6 +1065,10 @@ code { grid-template-columns: 1fr; } + .api-connection-details { + grid-template-columns: 1fr; + } + .detection-fields { grid-template-columns: 1fr; } diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 700b3f4..4252727 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import type { ReactNode } from "react"; +import { DemoDataBadge } from "./components/demo-state"; import "./globals.css"; export const metadata: Metadata = { @@ -34,6 +35,7 @@ export default function RootLayout({ children }: { children: ReactNode }) { ))} +
{children}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index fa4ad64..5d6f785 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,6 +1,11 @@ import Link from "next/link"; -import { ApiErrorPanel, StatusBadge } from "./components/demo-state"; +import { + ApiConnectionBanner, + ApiErrorPanel, + DemoDataBadge, + StatusBadge, +} from "./components/demo-state"; import { type Area, type Batch, @@ -35,7 +40,7 @@ export default async function OverviewPage() { <>
- Simulator-backed demo data +

{overview.ok ? overview.context.siteName : "Operations Workbench"}

{overview.ok @@ -73,6 +78,10 @@ export default async function OverviewPage() {

+ {overview.ok ? ( + + ) : null} + {!overview.ok ? : null} {overview.ok ? ( diff --git a/apps/web/e2e/operations-workbench-demo.spec.ts b/apps/web/e2e/operations-workbench-demo.spec.ts index c122515..f0c65d1 100644 --- a/apps/web/e2e/operations-workbench-demo.spec.ts +++ b/apps/web/e2e/operations-workbench-demo.spec.ts @@ -11,6 +11,11 @@ test("walks the simulator-backed Operations Workbench demo path", async ({ page await page.goto("/"); await expect(page.getByText("Simulator-backed demo data").first()).toBeVisible(); + await expect(page.getByText("Synthetic local scenario; not real plant data.").first()).toBeVisible(); + await expect(page.getByRole("region", { name: "Local API connection state" })).toBeVisible(); + await expect(page.getByText("API target").first()).toBeVisible(); + await expect(page.getByText("Health", { exact: true }).first()).toBeVisible(); + await expect(page.getByText("Simulator-backed demo API")).toBeVisible(); await expect(page.getByRole("heading", { name: "Greenville Demo Site" })).toBeVisible(); await expect(page.getByText("Active detections")).toBeVisible(); await expect(page.getByText("Pending recommendations")).toBeVisible(); diff --git a/apps/web/lib/api-client.ts b/apps/web/lib/api-client.ts index 948ddfc..2f27294 100644 --- a/apps/web/lib/api-client.ts +++ b/apps/web/lib/api-client.ts @@ -189,6 +189,9 @@ export function formatApiError(error: unknown): string { const status = error.status ? ` (${error.status})` : ""; return `${error.message}${status}`; } + if (error instanceof TypeError) { + return "The Workbench could not reach the configured FastAPI demo backend."; + } if (error instanceof Error) { return error.message; } diff --git a/apps/web/tests/app-shell.test.mjs b/apps/web/tests/app-shell.test.mjs index 4957e99..b39865b 100644 --- a/apps/web/tests/app-shell.test.mjs +++ b/apps/web/tests/app-shell.test.mjs @@ -26,10 +26,12 @@ test("navigation includes the required demo routes", () => { assert.match(layout, /Detections/); assert.match(layout, /Recommendations/); assert.match(layout, /RCA\/CAPA Draft/); + assert.match(layout, /DemoDataBadge/); }); test("overview page contains manufacturer demo dashboard content", () => { const overview = readFileSync(join(root, "app/page.tsx"), "utf8"); + const demoState = readFileSync(join(root, "app/components/demo-state.tsx"), "utf8"); assert.match(overview, /Current demo context/); assert.match(overview, /Active detections/); @@ -37,6 +39,12 @@ test("overview page contains manufacturer demo dashboard content", () => { assert.match(overview, /Most important detection/); assert.match(overview, /Open detection/); assert.match(overview, /selectImportantDetection/); + assert.match(overview, /ApiConnectionBanner/); + assert.match(overview, /API base URL/); + assert.match(overview, /API health/); + assert.match(demoState, /Local API connected/); + assert.match(demoState, /API target/); + assert.match(demoState, /Simulator-backed demo API/); }); test("detections pages contain list and detail content", () => { @@ -167,11 +175,17 @@ test("RCA CAPA page contains selected detection draft preview", () => { test("app shell documents configurable API base URL", () => { const config = readFileSync(join(root, "lib/api-config.ts"), "utf8"); const client = readFileSync(join(root, "lib/api-client.ts"), "utf8"); + const demoState = readFileSync(join(root, "app/components/demo-state.tsx"), "utf8"); const readme = readFileSync(join(root, "README.md"), "utf8"); assert.match(config, /NEXT_PUBLIC_API_BASE_URL/); assert.match(client, /apiBaseUrl/); + assert.match(client, /configured FastAPI demo backend/); assert.match(config, /http:\/\/127\.0\.0\.1:8000/); + assert.match(demoState, /make demo/); + assert.match(demoState, /make api/); + assert.match(demoState, /NEXT_PUBLIC_API_BASE_URL/); + assert.match(demoState, /Synthetic local scenario; not real plant data/); assert.match(readme, /NEXT_PUBLIC_API_BASE_URL/); }); diff --git a/docs/LEARNING_LOG.md b/docs/LEARNING_LOG.md index 9cd6612..e4c8290 100644 --- a/docs/LEARNING_LOG.md +++ b/docs/LEARNING_LOG.md @@ -3323,3 +3323,50 @@ make test Use the current deterministic rules as the baseline before adding any new process or quality drift modes, and require each new rule to document its threshold assumptions and evidence references. + +## 2026-05-22 - Workbench demo API connection notice + +### What changed + +Added a reusable Workbench demo notice and a local API connection banner for +issue #176. The Overview page now shows the configured FastAPI target, health +state, and simulator-backed API source, and API failures show presenter-friendly +recovery steps instead of a terse fetch error. + +### Why it was built that way + +The issue is a demo-readiness UI task, so the change stays in the existing +Workbench shell, demo-state components, tests, and runbook. It does not add +production observability, retry infrastructure, an environment switcher, or any +new backend behavior. + +### How data flows through it + +The Overview page still calls `GET /health` through the existing typed API +client. A successful response feeds the API connection banner. A failed request +is converted into safe recovery copy that shows the configured +`NEXT_PUBLIC_API_BASE_URL` target and tells the presenter how to restart the +local simulator-backed demo. + +### How to run it + +```bash +make demo +make api +cd apps/web +npm run dev +``` + +### How to test it + +```bash +cd apps/web +npm test +npm run lint +npm run typecheck +``` + +### What to learn next + +Practice the demo with the API intentionally stopped once, then confirm the +presenter can recover using only the visible target and recovery guidance. diff --git a/docs/demo/OPERATIONS_WORKBENCH_DEMO_RUNBOOK.md b/docs/demo/OPERATIONS_WORKBENCH_DEMO_RUNBOOK.md index e947369..e6f9671 100644 --- a/docs/demo/OPERATIONS_WORKBENCH_DEMO_RUNBOOK.md +++ b/docs/demo/OPERATIONS_WORKBENCH_DEMO_RUNBOOK.md @@ -8,8 +8,9 @@ factory context, inspect a detection, read the evidence timeline, review a governed recommendation, record a human decision, and preview an RCA/CAPA draft. All content is simulator-backed demo data. The visible Workbench label is -`Simulator-backed demo data`. It is not production data, a validated audit -record, an electronic signature, or an industrial writeback workflow. +`Simulator-backed demo data`, with supporting copy that this is a synthetic +local scenario and not real plant data. It is not production data, a validated +audit record, an electronic signature, or an industrial writeback workflow. ## Start The Demo @@ -42,25 +43,41 @@ http://127.0.0.1:3000 1. Open the overview page and confirm it is labeled `Simulator-backed demo data`. -2. Confirm the overview shows Greenville Demo Site, current factory context, +2. Confirm the overview shows the configured API target, `/health` status, and + simulator-backed demo API source. +3. Confirm the overview shows Greenville Demo Site, current factory context, active detections, pending recommendations, and the primary detection CTA. -3. Open `/detections` and confirm the Process Sentinel detection list renders +4. Open `/detections` and confirm the Process Sentinel detection list renders from the local API. -4. Open `/detections/det_fill_weight_gradual_drift`. -5. Confirm the detection detail shows severity, confidence, status, time +5. Open `/detections/det_fill_weight_gradual_drift`. +6. Confirm the detection detail shows severity, confidence, status, time window, work order, and related assets. -6. Inspect the evidence timeline and confirm each item shows a readable title, +7. Inspect the evidence timeline and confirm each item shows a readable title, timestamp, severity, relevance score, related asset, related batch, related work order, and source event IDs. -7. Open the recommendation review page from the detection detail. -8. Enter a demo reviewer and reason, then approve, reject, or defer the +8. Open the recommendation review page from the detection detail. +9. Enter a demo reviewer and reason, then approve, reject, or defer the recommendation. -9. Confirm the decision feedback shows reviewer, decision, reason, timestamp, +10. Confirm the decision feedback shows reviewer, decision, reason, timestamp, recommendation ID, and updated status. -10. Open the RCA/CAPA draft preview and confirm the problem statement, +11. Open the RCA/CAPA draft preview and confirm the problem statement, evidence summary, recommended containment, CAPA placeholder, and human review requirement are visible. +## API Recovery Check + +If the API is stopped or `NEXT_PUBLIC_API_BASE_URL` points at the wrong port, +the Workbench should show an API connection issue panel rather than raw stack +output. Use the panel to confirm the configured target, then run: + +```bash +make demo +make api +``` + +Restart the Workbench with `NEXT_PUBLIC_API_BASE_URL` only when the API is +intentionally running on a non-default local port. + ## Expected Demo IDs ```text