Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ is running on a non-default port.

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.
states, empty states, and simple user-readable API error states. Empty and
missing demo-data states show a short `Next step:` line so a presenter can
recover without treating local setup drift as a product failure.

## Governance Decision Feedback

Expand Down
28 changes: 24 additions & 4 deletions apps/web/app/components/demo-state.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ReactNode } from "react";

import type { HealthResponse } from "../../lib/api-client";
import { getApiBaseUrl } from "../../lib/api-client";

Expand All @@ -16,6 +18,7 @@ type DemoDataBadgeProps = {
};

type EmptyStateProps = {
nextStep?: string;
title: string;
text: string;
};
Expand All @@ -24,6 +27,12 @@ type LoadingStateProps = {
title?: string;
};

type MissingDataPanelProps = {
nextStep: string;
title: string;
text: ReactNode;
};

type StatusBadgeProps = {
label?: string;
tone?:
Expand Down Expand Up @@ -106,20 +115,31 @@ export function DemoDataBadge({
);
}

export function EmptyState({ title, text }: EmptyStateProps) {
export function EmptyState({ nextStep, title, text }: EmptyStateProps) {
return (
<div className="state-panel" role="status">
<strong>{title}</strong>
<span>{text}</span>
{nextStep ? <span>Next step: {nextStep}</span> : null}
</div>
);
}

export function MissingDataPanel({ nextStep, text, title }: MissingDataPanelProps) {
return (
<div className="state-panel">
<div className="state-panel missing-data-panel" role="status">
<strong>{title}</strong>
<span>{text}</span>
<span>Next step: {nextStep}</span>
</div>
);
}

export function LoadingState({ title = "Loading simulator-backed demo data" }: LoadingStateProps) {
return (
<section className="content-panel" aria-busy="true">
<section className="content-panel loading-panel" aria-busy="true">
<DemoDataBadge />
<div className="state-panel">
<div className="state-panel" role="status">
<strong>{title}</strong>
<span>Connecting to the local FastAPI demo backend.</span>
</div>
Expand Down
32 changes: 21 additions & 11 deletions apps/web/app/detections/[detectionId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import Link from "next/link";

import { ApiErrorPanel, StatusBadge } from "../../components/demo-state";
import {
ApiErrorPanel,
MissingDataPanel,
StatusBadge,
} from "../../components/demo-state";
import {
ApiClientError,
type Detection,
Expand Down Expand Up @@ -35,14 +39,17 @@ export default async function DetectionDetailPage({ params }: DetectionDetailPag
</p>
</div>
{!result.ok && result.notFound ? (
<div className="state-panel error-panel" role="alert">
<strong>Detection not found</strong>
<span>
<MissingDataPanel
nextStep="Open the detection list and choose a current demo detection, or rerun make demo if local state was reset."
text={
<>
The simulator-backed demo API did not return a detection for{" "}
<code>{detectionId}</code>. Open the detection list and choose a
current demo detection.
</span>
</div>
</>
}
title="Detection not found"
/>
) : null}
{!result.ok && !result.notFound ? <ApiErrorPanel message={result.message} /> : null}
{result.ok ? (
Expand Down Expand Up @@ -198,13 +205,16 @@ function EvidenceTimeline({ evidenceItems }: { evidenceItems: EvidenceItem[] })
<p>{buildTimelineMeaning(evidenceItems)}</p>
</section>
{evidenceItems.length === 0 ? (
<div className="state-panel">
<strong>No evidence available</strong>
<span>
<MissingDataPanel
nextStep="Rerun make demo to rebuild the detection and evidence timeline, then refresh this detail page."
text={
<>
This detection exists, but the simulator-backed API did not return
evidence items for it yet.
</span>
</div>
</>
}
title="No evidence available"
/>
) : (
<ol className="timeline-list">
{evidenceItems.map((item) => (
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/detections/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ export default async function DetectionsPage() {
{!result.ok ? <ApiErrorPanel message={result.message} /> : null}
{result.ok && result.detections.length === 0 ? (
<EmptyState
text="Run make demo-data, make demo-ingest, and make demo-sentinel-run, then start the API to populate this list."
nextStep="Run make demo from the repository root, start the local API, then refresh this page."
text="The local API is reachable, but it did not return any Process Sentinel detections for the current simulator-backed demo state."
title="No detections returned"
/>
) : null}
Expand Down
14 changes: 14 additions & 0 deletions apps/web/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,11 @@ h3 {
padding: 28px;
}

.loading-panel {
min-height: 260px;
align-content: start;
}

.placeholder-list {
display: grid;
gap: 12px;
Expand Down Expand Up @@ -868,6 +873,15 @@ h3 {
color: #8a2b2b;
}

.missing-data-panel {
border-color: #d9c18c;
background: #fffaf0;
}

.missing-data-panel strong {
color: var(--warning);
}

.decision-note {
border-left: 4px solid var(--accent);
background: #edf6f1;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,10 @@ export default async function OverviewPage() {
) : (
<>
<h2>No active detections</h2>
<p>Run the demo simulator and Process Sentinel to populate this panel.</p>
<p>
Run <code>make demo</code>, start the local API, and refresh this
page to populate this panel.
</p>
</>
)}
</div>
Expand Down
23 changes: 16 additions & 7 deletions apps/web/app/rca-capa-draft/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import Link from "next/link";

import { ApiErrorPanel, EmptyState, StatusBadge } from "../components/demo-state";
import {
ApiErrorPanel,
EmptyState,
MissingDataPanel,
StatusBadge,
} from "../components/demo-state";
import {
ApiClientError,
type RcaCapaDraft,
Expand Down Expand Up @@ -36,19 +41,23 @@ export default async function RcaCapaDraftPage({
</p>
</div>
{!result.ok && result.notFound ? (
<div className="state-panel error-panel" role="alert">
<strong>Draft not found</strong>
<span>
<MissingDataPanel
nextStep="Open a current demo detection and use its RCA/CAPA draft link, or rerun make demo if local state was reset."
text={
<>
The simulator-backed API did not return an RCA/CAPA draft for{" "}
<code>{result.detectionId}</code>. Open a current demo detection and
use its RCA/CAPA draft link.
</span>
</div>
</>
}
title="Draft not found"
/>
) : null}
{!result.ok && !result.notFound ? <ApiErrorPanel message={result.message} /> : null}
{result.ok && !result.draft ? (
<EmptyState
text="Run the demo state setup and Process Sentinel before previewing the draft."
nextStep="Run make demo, then open the draft from a current detection detail page."
text="The local API is reachable, but no demo detection is available for draft preview."
title="No detection available for draft preview"
/>
) : null}
Expand Down
4 changes: 3 additions & 1 deletion apps/web/app/recommendations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export default async function RecommendationsPage({
{!result.ok ? <ApiErrorPanel message={result.message} /> : null}
{result.ok && result.recommendations.length === 0 ? (
<EmptyState
text="Run Process Sentinel against the simulator-backed demo state to populate recommendations."
nextStep="Run make demo so Process Sentinel can create the demo recommendation, then refresh this page."
text="The local API is reachable, but it did not return any governed recommendations for the current simulator-backed demo state."
title="No recommendations returned"
/>
) : null}
{result.ok && result.recommendations.length > 0 && !result.selected ? (
<EmptyState
nextStep="Return to the detection list and choose a detection from the current demo run."
text={`No recommendation is linked to detection ${detectionId}. Return to the detection list and choose a current demo detection.`}
title="No linked recommendation found"
/>
Expand Down
6 changes: 6 additions & 0 deletions apps/web/e2e/operations-workbench-demo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ test("walks the simulator-backed Operations Workbench demo path", async ({ page
await expect(page.getByRole("heading", { name: "CAPA placeholder" })).toBeVisible();
await expect(page.getByText("Human review", { exact: true })).toBeVisible();
await expect(page.getByText("Required", { exact: true })).toBeVisible();

await page.goto("/detections/not-a-current-demo-detection");
await expect(page.getByRole("heading", { name: "Detection detail" })).toBeVisible();
await expect(page.getByRole("status")).toContainText("Detection not found");
await expect(page.getByRole("status")).toContainText("Next step:");
await expect(page.getByRole("status")).toContainText("rerun make demo");
});

async function recordDecision(
Expand Down
30 changes: 30 additions & 0 deletions apps/web/tests/app-shell.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,36 @@ test("app shell documents configurable API base URL", () => {
assert.match(readme, /NEXT_PUBLIC_API_BASE_URL/);
});

test("workbench state panels distinguish local demo data gaps from API failures", () => {
const demoState = readFileSync(join(root, "app/components/demo-state.tsx"), "utf8");
const overview = readFileSync(join(root, "app/page.tsx"), "utf8");
const detections = readFileSync(join(root, "app/detections/page.tsx"), "utf8");
const detail = readFileSync(join(root, "app/detections/[detectionId]/page.tsx"), "utf8");
const recommendations = readFileSync(join(root, "app/recommendations/page.tsx"), "utf8");
const draft = readFileSync(join(root, "app/rca-capa-draft/page.tsx"), "utf8");
const styles = readFileSync(join(root, "app/globals.css"), "utf8");

assert.match(demoState, /role="alert"/);
assert.match(demoState, /role="status"/);
assert.match(demoState, /aria-busy="true"/);
assert.match(demoState, /MissingDataPanel/);
assert.match(demoState, /loading-panel/);
assert.match(overview, /Run <code>make demo<\/code>/);
assert.match(detections, /The local API is reachable, but it did not return any Process Sentinel detections/);
assert.match(detections, /Run make demo from the repository root/);
assert.match(detail, /Detection not found/);
assert.match(detail, /No evidence available/);
assert.match(detail, /rerun make demo if local state was reset/);
assert.match(recommendations, /No recommendations returned/);
assert.match(recommendations, /No linked recommendation found/);
assert.match(recommendations, /Run make demo so Process Sentinel can create the demo recommendation/);
assert.match(draft, /Draft not found/);
assert.match(draft, /No detection available for draft preview/);
assert.match(draft, /Run make demo, then open the draft/);
assert.match(styles, /missing-data-panel/);
assert.match(styles, /min-height: 260px/);
});

test("operations workbench docs link to the demo runbook", async () => {
const rootReadme = readFileSync(join(root, "..", "..", "README.md"), "utf8");
const appReadme = readFileSync(join(root, "README.md"), "utf8");
Expand Down
48 changes: 48 additions & 0 deletions docs/LEARNING_LOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3370,3 +3370,51 @@ npm run typecheck

Practice the demo with the API intentionally stopped once, then confirm the
presenter can recover using only the visible target and recovery guidance.

## 2026-05-22 - Workbench empty and error state audit

### What changed

Tightened the Operations Workbench loading, empty, missing-data, and API-error
states for issue #177. Empty and missing demo-data panels now use status
semantics and short `Next step:` recovery copy, while API connection failures
remain alert panels with local API guidance.

### Why it was built that way

The Workbench already had the required routes and most state panels, so the
smallest useful change was to refine existing components and route copy instead
of adding a global error boundary or production monitoring feature. Missing demo
data is separated from API connectivity to keep local rehearsal issues from
looking like product failures.

### How data flows through it

Each route still calls the existing local FastAPI endpoints. Empty arrays and
404-style missing IDs render local demo recovery panels. Network/API failures
flow through `ApiErrorPanel`, preserving `role="alert"` and the configured API
target guidance.

### 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
make test-e2e
```

### What to learn next

During rehearsal, intentionally visit a stale detection URL and confirm the
presenter can recover from the visible `Next step:` text without opening docs.
28 changes: 27 additions & 1 deletion docs/demo/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ make demo

- `make demo-sentinel-run` prints `detections=0`.
- `/sentinel/detections` returns an empty list.
- The Workbench detection list has no Process Sentinel case.
- The Workbench detection list shows `No detections returned`.
- The recommendation queue is empty because no detection was created.

### Common causes
Expand Down Expand Up @@ -149,6 +149,9 @@ Confirm:
sentinel complete: detections=1 evidence=2 recommendations=1
```

Refresh the Workbench after the API is started. Empty-state panels indicate the
local API is reachable but the expected simulator-backed demo state is missing.

## API Not Running

### Symptoms
Expand Down Expand Up @@ -319,6 +322,29 @@ Expected fields include:
The draft is advisory decision support only. It does not create, close, or
submit a CAPA.

## Workbench Missing-State Panels

### Symptoms

- A page shows `Detection not found`, `No evidence available`,
`No linked recommendation found`, or `No detection available for draft
preview`.
- The API target is reachable, but the selected ID or generated local state does
not match the current demo run.

### Recovery

Use the visible `Next step:` line in the Workbench panel first. For a clean
manufacturer demo reset, rebuild deterministic state:

```bash
make demo
```

Restart the API if it was already running, then refresh the Workbench. These
missing-state panels are local demo recovery states; they are not production
incident handling or real plant failure modes.

## Recommendation Decision Not Updating

### Symptoms
Expand Down
Loading