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({
Local API connected - Workbench data is coming from the configured FastAPI target for this - simulator-backed demo. + Workbench data is coming from the configured FIP API target for the + external-source runtime.
@@ -65,11 +65,11 @@ export function ApiConnectionBanner({
Source
-
- {health?.simulator_backed - ? "Simulator-backed demo API" - : "Simulator-backed demo API expected"} -
+
{health?.source_mode ?? "External source expected"}
+
+
+
Connector mode
+
{health?.connector_mode ?? "Read-only runtime expected"}
@@ -84,13 +84,12 @@ export function ApiErrorPanel({
API connection issue - The Workbench could not reach the local simulator-backed API at{" "} - {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
{title} - Connecting to the local FastAPI demo backend. + Connecting to the local FIP API runtime.
); diff --git a/apps/web/app/detections/[detectionId]/page.tsx b/apps/web/app/detections/[detectionId]/page.tsx index e9dc571..2336314 100644 --- a/apps/web/app/detections/[detectionId]/page.tsx +++ b/apps/web/app/detections/[detectionId]/page.tsx @@ -34,17 +34,17 @@ export default async function DetectionDetailPage({ params }: DetectionDetailPag

Detection detail

- Read-only Process Sentinel detection context from the local demo API. + Read-only Process Sentinel detection context from the local FIP API.

{!result.ok && result.notFound ? ( - The simulator-backed demo API did not return a detection for{" "} - {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

RCA/CAPA draft

Preview the human-reviewed RCA/CAPA draft language generated - from the selected simulator-backed detection. + from the selected external-source detection.

- Simulator-backed evidence + External-source evidence

Evidence timeline

- 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 ? ( - This detection exists, but the simulator-backed API did not return - evidence items for it yet. + This detection exists, but the local FIP API did not return + evidence items from the current external-source event store yet. } title="No evidence available" @@ -294,12 +294,12 @@ function buildFlagExplanation(detection: Detection): string { detection.confidence * 100, )}% confidence over ${formatTimeWindow( detection, - )}. This is advisory simulator-backed context for human review, not an autonomous action.`; + )}. This is advisory external-source context for human review, not an autonomous action.`; } function buildTimelineMeaning(evidenceItems: EvidenceItem[]): string { if (evidenceItems.length === 0) { - return "The detection is present, but there is no supporting timeline evidence available yet in the simulator-backed demo state."; + return "The detection is present, but there is no supporting timeline evidence available yet in the current external-source event store."; } const evidenceTypes = [ diff --git a/apps/web/app/detections/page.tsx b/apps/web/app/detections/page.tsx index a292b5a..6306684 100644 --- a/apps/web/app/detections/page.tsx +++ b/apps/web/app/detections/page.tsx @@ -18,15 +18,16 @@ export default async function DetectionsPage() {

Detections

- 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.

{!result.ok ? : null} {result.ok && result.detections.length === 0 ? ( ) : null} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 516c477..cd1a70f 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -7,7 +7,7 @@ import "./globals.css"; export const metadata: Metadata = { title: "Operations Workbench", - description: "Simulator-backed Factory Intelligence Platform workbench shell.", + description: "External-source Factory Intelligence Platform workbench shell.", }; type NavItem = @@ -83,7 +83,7 @@ export default function RootLayout({ children }: { children: ReactNode }) {
Mode - Simulator-backed demo + External-source runtime
API target diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index e751ec0..34d2886 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -43,7 +43,7 @@ export default async function OverviewPage() {

{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."}

@@ -57,11 +57,15 @@ export default async function OverviewPage() { {getApiBaseUrl()}
- Demo source + Source mode - {overview.ok && overview.health.simulator_backed - ? "Synthetic Factory Simulator" - : "Synthetic Factory Simulator expected"} + {overview.ok ? overview.health.source_mode : "Unavailable"} + +
+
+ Connector mode + + {overview.ok ? overview.health.connector_mode : "Unavailable"}
@@ -111,7 +115,7 @@ export default async function OverviewPage() {
Active detections {overview.activeDetections.length} -

Open Process Sentinel cases from simulator-backed data.

+

Open Process Sentinel cases from external-source event data.

Pending recommendations @@ -150,8 +154,9 @@ export default async function OverviewPage() { <>

No active detections

- 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.

)} @@ -251,7 +256,7 @@ function buildOverviewContext({ productName: currentBatch?.product_name ?? "Demo product unavailable", siteDescription: currentSite?.description ?? - "Simulator-backed site context will appear when the local FastAPI backend is available.", + "External-source site context will appear when the local FIP API is available.", siteName: currentSite?.name ?? "Factory Intelligence Platform demo", workOrderId: importantDetection?.related_work_order_id ?? "Demo work order unavailable", }; diff --git a/apps/web/app/rca-capa-draft/page.tsx b/apps/web/app/rca-capa-draft/page.tsx index c0111af..3b1cce1 100644 --- a/apps/web/app/rca-capa-draft/page.tsx +++ b/apps/web/app/rca-capa-draft/page.tsx @@ -36,18 +36,19 @@ export default async function RcaCapaDraftPage({

RCA/CAPA Draft

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.ok && result.notFound ? ( - The simulator-backed API did not return an RCA/CAPA draft for{" "} - {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 ? : null} {result.ok && !result.draft ? ( ) : null} {result.ok && result.draft ? (
- This RCA/CAPA draft is generated from demo detection, evidence, and - recommendation state. It is advisory decision support for human + This RCA/CAPA draft is generated from runtime detection, evidence, + and recommendation state. It is advisory decision support for human review only; it is not a validated production record and it does not submit anything to QMS or MES.
@@ -100,7 +101,7 @@ export default async function RcaCapaDraftPage({
Source
- +
diff --git a/apps/web/app/recommendations/page.tsx b/apps/web/app/recommendations/page.tsx index 243a25e..1e731d7 100644 --- a/apps/web/app/recommendations/page.tsx +++ b/apps/web/app/recommendations/page.tsx @@ -25,26 +25,27 @@ export default async function RecommendationsPage({ return (
- Simulator-backed governed recommendation + External-source governed recommendation

Recommendation review

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.

{!result.ok ? : null} {result.ok && result.recommendations.length === 0 ? ( ) : null} {result.ok && result.recommendations.length > 0 && !result.selected ? ( ) : null} @@ -53,7 +54,8 @@ export default async function RecommendationsPage({
Recommendations are advisory decision support. A named human reviewer must approve, reject, or defer before any high-impact action is - considered, and this demo records only local simulator state. + considered. The Workbench does not perform industrial writeback, + product disposition, QMS/MES updates, or production CAPA creation.
diff --git a/apps/web/app/recommendations/recommendation-review-panel.tsx b/apps/web/app/recommendations/recommendation-review-panel.tsx index c84d229..0535020 100644 --- a/apps/web/app/recommendations/recommendation-review-panel.tsx +++ b/apps/web/app/recommendations/recommendation-review-panel.tsx @@ -105,7 +105,7 @@ export function RecommendationReviewPanel({ >

Enter a reviewer and decision reason before approving, rejecting, or - deferring this simulator-backed recommendation. + deferring this external-source recommendation.

diff --git a/apps/web/e2e/operations-workbench-demo.spec.ts b/apps/web/e2e/operations-workbench-demo.spec.ts index e8b9b6c..5fc2759 100644 --- a/apps/web/e2e/operations-workbench-demo.spec.ts +++ b/apps/web/e2e/operations-workbench-demo.spec.ts @@ -6,7 +6,7 @@ const repoRoot = path.resolve(__dirname, "../../.."); const detectionId = "det_fill_weight_gradual_drift"; const recommendationPath = `/recommendations?detection_id=${detectionId}`; -test("walks the simulator-backed Operations Workbench demo path", async ({ page }) => { +test("walks the external-source Operations Workbench demo path", async ({ page }) => { resetDemoState(); await page.goto("/"); @@ -83,12 +83,11 @@ test("walks the simulator-backed Operations Workbench demo path", async ({ page .getByRole("button", { name: "Reset filters" }) .click(); await page.locator('a[href="/"]').first().click(); - await expect(page.getByText("Simulator-backed demo data")).toHaveCount(0); await expect(page.getByText("Synthetic local scenario; not real plant data.")).toHaveCount(0); 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.getByText("external_demo_factory")).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(); @@ -97,7 +96,9 @@ test("walks the simulator-backed Operations Workbench demo path", async ({ page await page.locator('a[href="/detections"]').first().click(); await expect(page.getByRole("heading", { name: "Detections" })).toBeVisible(); - await expect(page.getByText("Process Sentinel detections from the local demo run")).toBeVisible(); + await expect( + page.getByText("Process Sentinel detections from the current external-source event store"), + ).toBeVisible(); await page.locator(`a[href="/detections/${detectionId}"]`).click(); await expect(page.getByRole("heading", { name: "Detection detail" })).toBeVisible(); @@ -141,7 +142,7 @@ test("walks the simulator-backed Operations Workbench demo path", async ({ page 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"); + await expect(page.getByRole("status")).toContainText("FIP Docker Compose stack"); }); async function createDuplicateSourceConnectionProfiles(page: Page) { @@ -235,6 +236,6 @@ function resetDemoState() { error instanceof Error && "stdout" in error && "stderr" in error ? `\nstdout:\n${String(error.stdout)}\nstderr:\n${String(error.stderr)}` : ""; - throw new Error(`Failed to prepare deterministic demo state with "make demo".${details}`); + throw new Error(`Failed to prepare deterministic legacy demo state.${details}`); } } diff --git a/apps/web/lib/api-client.ts b/apps/web/lib/api-client.ts index 09e87cd..760e482 100644 --- a/apps/web/lib/api-client.ts +++ b/apps/web/lib/api-client.ts @@ -2,7 +2,11 @@ import { apiBaseUrl } from "./api-config"; export type HealthResponse = { status: string; - simulator_backed: boolean; + source_mode: string; + storage_backend: string; + connector_mode: string; + simulator_backed?: boolean; + simulator_backed_migration_note?: string; events_store: string; sentinel_state_dir: string; connection_profiles_store?: string; @@ -334,7 +338,7 @@ export function formatApiError(error: unknown): string { return `${error.message}${status}`; } if (error instanceof TypeError) { - return "The Workbench could not reach the configured FastAPI demo backend."; + return "The Workbench could not reach the local FIP API."; } if (error instanceof Error) { return error.message; diff --git a/apps/web/tests/api-client.test.mjs b/apps/web/tests/api-client.test.mjs index d41dca1..16e2826 100644 --- a/apps/web/tests/api-client.test.mjs +++ b/apps/web/tests/api-client.test.mjs @@ -36,6 +36,10 @@ test("typed workbench API client covers demo endpoints", () => { } assert.match(client, /getHealth/); + assert.match(client, /source_mode: string/); + assert.match(client, /storage_backend: string/); + assert.match(client, /connector_mode: string/); + assert.match(client, /simulator_backed_migration_note/); assert.match(client, /listEvents/); assert.match(client, /listConnectionProfiles/); assert.match(client, /createConnectionProfile/); diff --git a/apps/web/tests/app-shell.test.mjs b/apps/web/tests/app-shell.test.mjs index 574696b..90465dc 100644 --- a/apps/web/tests/app-shell.test.mjs +++ b/apps/web/tests/app-shell.test.mjs @@ -50,12 +50,12 @@ test("navigation includes the required demo routes", () => { assert.match(layout, /href: "\/tag-source-browser"/); assert.match(layout, /Workbench status strip/); assert.doesNotMatch(layout, /
{ @@ -260,7 +260,8 @@ test("overview page contains manufacturer demo dashboard content", () => { assert.match(overview, /API health/); assert.match(demoState, /Local API connected/); assert.match(demoState, /API target/); - assert.match(demoState, /Simulator-backed demo API/); + assert.match(demoState, /External source expected/); + assert.match(demoState, /Read-only runtime expected/); }); test("detections pages contain list and detail content", () => { @@ -270,7 +271,10 @@ test("detections pages contain list and detail content", () => { const demoState = readFileSync(join(root, "app/components/demo-state.tsx"), "utf8"); const styles = readFileSync(join(root, "app/globals.css"), "utf8"); - assert.match(list, /Process Sentinel detections from the local demo run/); + assert.match( + list, + /Process Sentinel detections from the current external-source event\s+store/, + ); assert.match(list, /detection.summary/); assert.match(list, /detection.severity/); assert.match(list, /detection.confidence/); @@ -313,8 +317,8 @@ test("detections pages contain list and detail content", () => { assert.match(detail, /recommendations\?detection_id=/); assert.match(detail, /RCA\/CAPA draft/); assert.match(detail, /rca-capa-draft\?detection_id=/); - assert.doesNotMatch(detail, /Simulator-backed demo data/); - assert.match(detail, /Simulator-backed evidence/); + assert.doesNotMatch(detail, new RegExp("simulator-" + "backed", "i")); + assert.match(detail, /External-source evidence/); assert.match(demoState, /function StatusBadge/); assert.match(styles, /status-badge/); assert.match(styles, /status-badge-danger/); @@ -396,10 +400,10 @@ test("app shell documents configurable API base URL", () => { assert.match(config, /NEXT_PUBLIC_API_BASE_URL/); assert.match(client, /apiBaseUrl/); - assert.match(client, /configured FastAPI demo backend/); + assert.match(client, /local FIP API/); assert.match(config, /http:\/\/127\.0\.0\.1:8000/); - assert.match(demoState, /make demo/); - assert.match(demoState, /make api/); + assert.match(demoState, /Start Demo-Factory/); + assert.match(demoState, /FIP Docker Compose stack/); assert.match(demoState, /NEXT_PUBLIC_API_BASE_URL/); assert.doesNotMatch(demoState, /Synthetic local scenario; not real plant data/); assert.match(readme, /NEXT_PUBLIC_API_BASE_URL/); @@ -419,22 +423,46 @@ test("workbench state panels distinguish local demo data gaps from API failures" assert.match(demoState, /aria-busy="true"/); assert.match(demoState, /MissingDataPanel/); assert.match(demoState, /loading-panel/); - assert.match(overview, /Run 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