Open Process Sentinel cases from simulator-backed data.
+Open Process Sentinel cases from external-source event data.
diff --git a/.env.example b/.env.example index 189be6c..b610c2b 100644 --- a/.env.example +++ b/.env.example @@ -2,6 +2,8 @@ APP_ENV=development LOG_LEVEL=info DATABASE_URL=postgresql://postgres:postgres@localhost:5432/factory_intelligence FACTORY_STORAGE_BACKEND=jsonl +FACTORY_SOURCE_MODE=external_demo_factory +FACTORY_CONNECTOR_MODE=read_only NEXT_PUBLIC_API_BASE_URL=http://localhost:8000 FIP_API_URL=http://localhost:8000 FACTORY_CONNECTION_PROFILES_STORE=.local/storage/connection_profiles.json diff --git a/apps/web/README.md b/apps/web/README.md index 45aabfa..3076958 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -68,6 +68,14 @@ curl http://localhost:8000/health docker compose -f infra/docker/docker-compose.yml down ``` +The Workbench expects the API health endpoint to describe the active runtime +with `source_mode`, `storage_backend`, and `connector_mode`. In the Docker path, +that means external Demo-Factory/local protocol sources, Postgres-backed state, +and read-only connector behavior. Empty detection, evidence, recommendation, or +RCA/CAPA draft panels should be treated as missing runtime data until +Demo-Factory, FIP Compose, connector ingestion, and Process Sentinel have all +run. + For focused Workbench development before all Compose services land, run the API directly from the repository root: diff --git a/apps/web/app/components/demo-state.tsx b/apps/web/app/components/demo-state.tsx index 77c058c..e97bfbc 100644 --- a/apps/web/app/components/demo-state.tsx +++ b/apps/web/app/components/demo-state.tsx @@ -50,8 +50,8 @@ export function ApiConnectionBanner({
{apiBaseUrl}.
+ The Workbench could not reach the local FIP 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{" "}
+ Start Demo-Factory, then start the FIP Docker Compose stack. Check{" "}
+ curl http://localhost:8000/health, 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}
@@ -123,7 +122,7 @@ export function LoadingState({ title = "Loading local demo data" }: LoadingState
- Read-only Process Sentinel detection context from the local demo API. + Read-only Process Sentinel detection context from the local FIP API.
{detectionId}. Open the detection list and choose a
- current demo detection.
+ The local FIP API did not return a detection for{" "}
+ {detectionId}. Open the detection list and choose a
+ current runtime detection.
>
}
title="Detection not found"
@@ -131,7 +131,7 @@ export default async function DetectionDetailPage({ params }: DetectionDetailPag
Preview the human-reviewed RCA/CAPA draft language generated - from the selected simulator-backed detection. + from the selected external-source detection.
- Chronological Process Sentinel evidence from the local demo API. Use + Chronological Process Sentinel evidence from the local FIP API. Use this to understand why the finding exists before reviewing any recommendation.
@@ -205,11 +205,11 @@ function EvidenceTimeline({ evidenceItems }: { evidenceItems: EvidenceItem[] }) {evidenceItems.length === 0 ? (- Process Sentinel detections from the local demo run. Open a detection to - inspect the current summary and routing context. + Process Sentinel detections from the current external-source event + store. Open a detection to inspect the current summary and routing + context.
{overview.ok ? overview.context.siteDescription - : "The manufacturer demo overview needs the local FastAPI backend to show current factory context."} + : "The manufacturer demo overview needs the local FIP API to show current factory context."}
Open Process Sentinel cases from simulator-backed data.
+Open Process Sentinel cases from external-source event data.
- Run make demo, start the local API, and refresh this
- page to populate this panel.
+ Start Demo-Factory, then start the FIP Docker Compose stack
+ and refresh this page after Process Sentinel stores runtime
+ detections.
Preview investigation-ready RCA/CAPA draft language for the selected - simulator-backed Process Sentinel detection. Draft content is human-review - required and is not automatically submitted to QMS or MES systems. + external-source Process Sentinel detection. Draft content is + human-review required and is not automatically submitted to QMS or MES + systems.
{result.detectionId}. Open a current demo detection and
- use its RCA/CAPA draft link.
+ The local FIP API did not return an RCA/CAPA draft for{" "}
+ {result.detectionId}. Open a current runtime detection
+ and use its RCA/CAPA draft link.
>
}
title="Draft not found"
@@ -56,16 +57,16 @@ export default async function RcaCapaDraftPage({
{!result.ok && !result.notFound ? Review the advisory Process Sentinel recommendation and record a human - decision. The demo does not execute industrial writeback. + decision. This recommendation is advisory and human-reviewed; no + industrial writeback is performed.
Enter a reviewer and decision reason before approving, rejecting, or - deferring this simulator-backed recommendation. + deferring this external-source recommendation.
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(overview, /Start Demo-Factory/);
+ assert.match(detections, /No Process Sentinel detections are available for the current external-source event store yet/);
+ assert.match(detections, /curl http:\/\/localhost:8000\/health/);
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(detail, /FIP Docker Compose stack/);
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(recommendations, /current external-source event store yet/);
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(draft, /Start Demo-Factory/);
assert.match(styles, /missing-data-panel/);
assert.match(styles, /min-height: 260px/);
});
+test("workbench runtime copy points to external sources and Docker recovery", () => {
+ const files = [
+ "app/layout.tsx",
+ "app/page.tsx",
+ "app/components/demo-state.tsx",
+ "app/detections/page.tsx",
+ "app/detections/[detectionId]/page.tsx",
+ "app/recommendations/page.tsx",
+ "app/recommendations/recommendation-review-panel.tsx",
+ "app/rca-capa-draft/page.tsx",
+ "lib/api-client.ts",
+ ];
+ const combined = files.map((file) => readFileSync(join(root, file), "utf8")).join("\n");
+
+ assert.match(combined, /Start Demo-Factory/);
+ assert.match(combined, /FIP Docker Compose stack/);
+ assert.match(combined, /The Workbench could not reach the local FIP API/);
+ assert.match(combined, /advisory and human-reviewed/);
+ assert.doesNotMatch(combined, new RegExp("simulator-" + "backed", "i"));
+ assert.doesNotMatch(combined, new RegExp("rerun make" + " demo", "i"));
+ assert.doesNotMatch(combined, new RegExp("make " + "demo-data", "i"));
+ assert.doesNotMatch(combined, new RegExp("local simulator-" + "backed API", "i"));
+});
+
test("accessibility baseline covers landmarks, focus, forms, badges, and timeline order", () => {
const layout = readFileSync(join(root, "app/layout.tsx"), "utf8");
const demoState = readFileSync(join(root, "app/components/demo-state.tsx"), "utf8");
diff --git a/docs/LEARNING_LOG.md b/docs/LEARNING_LOG.md
index 4acad1d..875bd01 100644
--- a/docs/LEARNING_LOG.md
+++ b/docs/LEARNING_LOG.md
@@ -5007,3 +5007,54 @@ make test-e2e
Before the external demo, walk the route flow with only the keyboard and record
any remaining non-blocking accessibility debt in the runbook.
+
+## 2026-05-27 - Runtime wording alignment
+
+### What changed
+
+Updated issue #240 runtime wording across the API and Workbench. The API health
+response now reports `source_mode`, `storage_backend`, and `connector_mode`, and
+the Workbench status, recovery, empty, evidence, recommendation, and RCA/CAPA
+draft copy now points to Demo-Factory, the FIP Docker Compose stack, and local
+API health checks.
+
+### Why it was built that way
+
+The runtime now treats Demo-Factory and local protocol services as external
+sources. The smallest useful change was to keep the existing endpoints and page
+structure while replacing source/recovery language and adding tests that guard
+against the old default wording returning.
+
+### How data flows through it
+
+Demo-Factory or local protocol sources feed read-only connector ingestion.
+FactoryEvents are stored in the configured backend, Process Sentinel creates
+detections, evidence, recommendations, and draft investigation text, and the
+Workbench reads that state through the FIP API. No Workbench action performs
+industrial writeback, product disposition, QMS/MES updates, or production CAPA
+creation.
+
+### How to run it
+
+```bash
+cd ../Demo-Factory
+docker compose up -d --build
+cd ../Factory-Intelligence-Platform
+docker compose -f infra/docker/docker-compose.yml up --build
+curl http://localhost:8000/health
+```
+
+### How to test it
+
+```bash
+cd apps/web && npm test
+make test
+make lint
+make typecheck
+```
+
+### What to learn next
+
+Run the full Compose smoke path from issue #241 and confirm the visible
+Workbench recovery copy matches the real failure modes for Demo-Factory,
+connector ingestion, Process Sentinel, API, and web startup.
diff --git a/services/api/README.md b/services/api/README.md
index 558aa9e..59cc3fd 100644
--- a/services/api/README.md
+++ b/services/api/README.md
@@ -17,6 +17,19 @@ DATABASE_URL=postgresql://postgres:postgres@postgres:5432/factory_intelligence
JSONL storage remains available for lightweight local tests.
+`GET /health` reports runtime metadata that the Workbench uses for status and
+recovery copy:
+
+- `source_mode` - default `external_demo_factory`, meaning source readings are
+ expected to come from Demo-Factory or local protocol sources outside this API.
+- `storage_backend` - `postgres` in Docker, or `jsonl` for lightweight local
+ tests.
+- `connector_mode` - default `read_only`, matching the MVP safety boundary.
+
+The older `simulator_backed` health field is retained only as a deprecated
+compatibility field and is always `false`. New clients should use
+`source_mode`, `storage_backend`, and `connector_mode`.
+
The API currently exposes:
- Health and event query endpoints.
@@ -25,7 +38,7 @@ The API currently exposes:
- Local connection profile CRUD endpoints for OPC-UA, MQTT, and BACnet
definitions.
- Process Sentinel detections, evidence, recommendations, and RCA/CAPA draft
- endpoints over local demo state.
+ endpoints over the configured runtime event store.
- Governed recommendation review endpoints, including status-filtered
recommendation lists, decision history, and local audit events.
diff --git a/services/api/factory_api/demo_smoke.py b/services/api/factory_api/demo_smoke.py
index 7c463fe..1bce1f2 100644
--- a/services/api/factory_api/demo_smoke.py
+++ b/services/api/factory_api/demo_smoke.py
@@ -132,13 +132,13 @@ def _require_demo_state(events_store_path: Path, sentinel_state_dir: Path) -> No
if not events_store_path.is_file():
msg = (
f"Missing demo events store: {events_store_path}. "
- "Run make demo-data, make demo-ingest, and make demo-sentinel-run first."
+ "Prepare legacy generated events, ingestion, and Sentinel state first."
)
raise DemoSmokeError(msg)
if not sentinel_state_dir.is_dir():
msg = (
f"Missing demo Sentinel state directory: {sentinel_state_dir}. "
- "Run make demo-sentinel-run first."
+ "Prepare legacy Sentinel state first."
)
raise DemoSmokeError(msg)
@@ -155,7 +155,7 @@ def _require_demo_state(events_store_path: Path, sentinel_state_dir: Path) -> No
if missing_state_files:
msg = (
f"Demo Sentinel state is missing files: {missing_state_files}. "
- "Run make demo-sentinel-run first."
+ "Prepare legacy Sentinel state first."
)
raise DemoSmokeError(msg)
diff --git a/services/api/factory_api/domain.py b/services/api/factory_api/domain.py
index c9a697b..3557053 100644
--- a/services/api/factory_api/domain.py
+++ b/services/api/factory_api/domain.py
@@ -192,7 +192,9 @@ def build_demo_domain_data() -> DomainData:
site_id="greenville_demo_site",
name="Greenville Demo Site",
timezone="America/New_York",
- description="Simulator-backed Greenville site used for the manufacturer demo.",
+ description=(
+ "External-source Greenville site context used for the manufacturer demo."
+ ),
)
],
areas=[
diff --git a/services/api/factory_api/main.py b/services/api/factory_api/main.py
index 329de91..00e80b7 100644
--- a/services/api/factory_api/main.py
+++ b/services/api/factory_api/main.py
@@ -68,6 +68,8 @@ def create_app(
domain_data: DomainData | None = None,
storage_backend: str | None = None,
database_url: str | None = None,
+ source_mode: str | None = None,
+ connector_mode: str | None = None,
) -> FastAPI:
resolved_events_store = events_store_path or Path(
os.getenv("FACTORY_EVENTS_STORE", ".local/storage/events.jsonl")
@@ -83,11 +85,17 @@ def create_app(
storage_backend or os.getenv("FACTORY_STORAGE_BACKEND", "jsonl")
).strip().lower()
resolved_database_url = database_url or os.getenv("DATABASE_URL")
+ resolved_source_mode = (
+ source_mode or os.getenv("FACTORY_SOURCE_MODE", "external_demo_factory")
+ ).strip().lower()
+ resolved_connector_mode = (
+ connector_mode or os.getenv("FACTORY_CONNECTOR_MODE", "read_only")
+ ).strip().lower()
app = FastAPI(
title="Factory Intelligence Platform API",
version="0.1.0",
- description="Simulator-backed Process Sentinel MVP API.",
+ description="External-source Process Sentinel runtime API.",
)
cors_origins = configured_cors_origins()
if cors_origins:
@@ -156,8 +164,14 @@ def domain() -> DomainData:
def health() -> dict:
return {
"status": "ok",
- "simulator_backed": True,
+ "source_mode": resolved_source_mode,
"storage_backend": resolved_storage_backend,
+ "connector_mode": resolved_connector_mode,
+ "simulator_backed": False,
+ "simulator_backed_migration_note": (
+ "Deprecated compatibility field. Use source_mode and connector_mode "
+ "for runtime source metadata."
+ ),
"events_store": str(resolved_events_store),
"sentinel_state_dir": str(resolved_state_dir),
"connection_profiles_store": str(resolved_connection_profiles_store),
diff --git a/services/api/tests/test_api.py b/services/api/tests/test_api.py
index a07c73b..60c1154 100644
--- a/services/api/tests/test_api.py
+++ b/services/api/tests/test_api.py
@@ -29,7 +29,13 @@ def test_health_endpoint(tmp_path: Path) -> None:
response = client.get("/health")
assert response.status_code == 200
- assert response.json()["status"] == "ok"
+ health = response.json()
+ assert health["status"] == "ok"
+ assert health["source_mode"] == "external_demo_factory"
+ assert health["storage_backend"] == "jsonl"
+ assert health["connector_mode"] == "read_only"
+ assert health["simulator_backed"] is False
+ assert "Deprecated compatibility field" in health["simulator_backed_migration_note"]
def test_event_and_detection_query_endpoints(tmp_path: Path) -> None:
diff --git a/services/api/tests/test_demo_smoke.py b/services/api/tests/test_demo_smoke.py
index 916eb8a..e7c78f4 100644
--- a/services/api/tests/test_demo_smoke.py
+++ b/services/api/tests/test_demo_smoke.py
@@ -52,6 +52,4 @@ def test_demo_api_smoke_fails_clearly_when_state_is_missing(
assert exit_code == 1
assert "demo api smoke failed: Missing demo events store" in captured.err
- assert "Run make demo-data, make demo-ingest, and make demo-sentinel-run first" in (
- captured.err
- )
+ assert "Prepare legacy generated events, ingestion, and Sentinel state first" in captured.err