+ Test supplier traceability data before production import. This lab uses mock or uploaded test records only; formal production records start after signed-in import.
+
+
+
+
+
+
+
+
+ Last inbound record
+ {lastReceivedLabel}
+
+
+ Readiness score
+ {readinessScore}/100
+
+
+ Validation outcome
+ {validationOutcome}
+
+
+ Test completeness
+ {exportReadinessLabel}
+
+
+
+
+ )}
+
+
+
+
+
+
+
+ Boundary active
+ {activeDataBoundary}
+
+
+ {boundaryStateDetail} The lab checks Critical Tracking Event (CTE) handoffs and required Key Data Element (KDE) fields, but it cannot create formal production records. Your operation remains responsible for compliance.
+
+ {isStandalone
+ ? "Review mock-generated records, trace lots, and preview test filters without mixing simulator data into production imports."
+ : "Validate FSMA 204 records, resolve exceptions, prove traceability, and keep formal production records separate from test data."}
+
+ Test submissions are routed through the Inflow Lab gateway and remain outside production ingestion.
+
+
+
+
+
+
+
Traceability Readiness Score
+
+
{readinessScore}/100
+
+ Blends KDE completeness, CTE lifecycle coverage, delivery quality, sandbox pass rate, and connection health.
+
+
+
+
+
+ {exceptionCount ? (
+
+ ) : (
+
+ )}
+
Validation outcome
+
+
{validationOutcome}
+
+ Ready: {exportReadyCount}. Warning: {warningLots.length}. Blocked: {blockedLots.length}. Records need CTE coverage, KDE completeness, lineage, and posted delivery before export.
+
+
+
+
+
+
+
Test completeness
+
+
{exportReadinessLabel}
+
+ Test completeness uses the selected lot. Formal production records come later from signed in imports.
+
+
+
+
+
+
+
+
+
Lot readiness
+
Select a lot to inspect the traceability path and required FSMA 204 events.
+
+
+
+
+ {uniqueLots.map((lot) => (
+
+ ))}
+
+
+
+
+
+
+
+
Exceptions
+
+ Partial lots remain visible for remediation instead of being hidden from the mock preview.
+
+ Uses the public stateless sandbox evaluator. Data is parsed, normalized, and checked against FSMA 204 rules without being stored or promoted to production ingestion.
+
+ Production evidence remains closed until records are signed in, stored, provenance-tagged, and export eligible.
+
+
+ {[
+ ["Simulation", "Mock and scenario data only"],
+ ["Preflight", feederResult ? "Sandbox result available" : "Awaiting sandbox evaluation"],
+ ["Staging", feederSavedAt ? "Saved test run ready for mapping" : "Save a test run before mapping"],
+ [
+ "Production evidence",
+ commitGateDecision
+ ? commitGateDecision.reasons[0]
+ : unresolvedFixCount
+ ? "Blocked by active fixes"
+ : "Ready for authenticated import path",
+ ],
+ ].map(([label, value]) => (
+
+
{label}
+
{value}
+
+ ))}
+
+
+
+ )}
+
+ {activeTab === "Scenarios" && (
+
+ {workbenchScenarios.map((scenario) => (
+
+
+
+
+ {scenario.records}
+
+
+
{scenario.name}
+
{scenario.outcome}
+
+
+ ))}
+
+
Replay and regression testing
+
+ Scenarios are reusable preflight inputs for demos, supplier onboarding, implementation training, and contract regression checks before rules or mappings change.
+
+
+
+ )}
+
+ {activeTab === "Suppliers" && (
+
+
+
Supplier readiness
+
+ Supplier state uses sample validation, mapping progress, repeated fixes, and export-ready lots to show who is blocking readiness.
+
+ {isStandalone ? "Mock scenario loaded" : "No inbound records loaded"}
+
+
+ {isStandalone
+ ? "Run the pipeline to generate FSMA 204 traceability records."
+ : "Load test data to validate FSMA 204 records before production ingestion."}
+
+ Required CTEs captured, KDE-bearing events posted, lineage complete, included in test-complete counts.
+
+
+
+
Warning
+
+ Partial CTE/KDE coverage or generated-only delivery; visible as an exception and excluded from ready counts.
+
+
+
+
Blocked
+
+ Failed delivery or no traceability events; cannot be exported until delivery and lineage are corrected.
+
+
+
+
+
+ {uniqueLots.map((lot) => (
+
+ ))}
+
+
+ )}
+
+ {activeTab === "Lineage" && (
+
+
+
+
+
Lineage for {selectedLot}
+
+ CTE coverage {selectedCompleteCount} of {selectedLineage.length}; delivery {selectedReadiness.deliveryLabel}.
+
+
+
+
+ {!selectedIsExportReady && (
+
+ {selectedReadiness.missingCtes.length > 0
+ ? `Capture missing KDE evidence for ${selectedReadiness.missingCtes.join(", ")} before this lot is counted as test complete.`
+ : "Resolve delivery status before this lot is counted as test complete."}
+
+ );
+}
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
new file mode 100644
index 000000000..f7ab76b9e
--- /dev/null
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
@@ -0,0 +1,224 @@
+"use client";
+
+import { useCallback, useSyncExternalStore } from "react";
+import { ChevronRight, GraduationCap, X } from "lucide-react";
+
+import type { TutorialStep } from "@/components/ui/tutorial";
+
+// One key for the tutorial's own "don't show again" checkbox, a separate key
+// for the banner so dismissing the banner never suppresses the walkthrough
+// itself (it stays reachable from the header's "How to use" button).
+export const INFLOW_LAB_TUTORIAL_STORAGE_KEY = "inflow-lab-dashboard-tutorial";
+export const INFLOW_LAB_TUTORIAL_BANNER_KEY = "inflow-lab-dashboard-tutorial-banner-dismissed";
+// The standalone /tools/inflow-lab client carries its own tour with its own
+// dismissal state — finishing one surface's tour must not hide the other's.
+export const INFLOW_LAB_CLIENT_TUTORIAL_STORAGE_KEY = "inflow-lab-client-tutorial";
+export const INFLOW_LAB_CLIENT_TUTORIAL_BANNER_KEY = "inflow-lab-client-tutorial-banner-dismissed";
+
+// Step-by-step operating guide for the dashboard Inflow Lab tab. Targets are
+// stable anchors on the page: data-tutorial markers, the funnel test id, and
+// the selected-lot panel marker the tests already rely on.
+//
+// A builder rather than a constant: the "run your first test" step carries a
+// real action button wired to the page's guided proof, so the walkthrough can
+// evaluate the user's own rows and fill the panels its later steps describe.
+export function buildInflowLabTutorialSteps({
+ runGuidedProof,
+}: {
+ runGuidedProof: () => void;
+}): TutorialStep[] {
+ return [
+ {
+ target: '[data-testid="inflow-evidence-chain"]',
+ title: "What Inflow Lab is",
+ content:
+ "A sandbox for supplier traceability data. This is one lot's chain of custody, harvest through receiving. A solid link is a record we have; a broken one is a record that never arrived. The gate at the end is what a missing link costs you. Nothing here writes to production exports.",
+ position: "bottom",
+ },
+ {
+ target: '[data-tutorial="inflow-load-data"]',
+ title: "Bring supplier data in",
+ content:
+ "Upload CSV takes your own supplier rows and opens them in the data editor below, where you can also paste rows directly. Nothing is preloaded: until you add data, the panels show No data yet.",
+ position: "bottom",
+ },
+ {
+ target: '[data-tutorial="inflow-guided-proof"]',
+ title: "Run your first test",
+ content:
+ "This evaluates the rows you uploaded or pasted, end to end. The highlighted button is live, or use the one right here. The chain, pipeline, score, and lot panels fill with the results. With no rows loaded, it opens the data editor instead.",
+ position: "bottom",
+ action: "Run trace",
+ onAction: runGuidedProof,
+ },
+ {
+ target: '[data-testid="inflow-funnel"]',
+ title: "Read the pipeline funnel",
+ content:
+ "Lots flow Loaded → Validated → Passing → Preview-ready → Production import. Wherever the numbers drop is where your data is losing evidence.",
+ position: "top",
+ },
+ {
+ target: '[data-tutorial="inflow-readiness"]',
+ title: "Check the Data Preflight Score",
+ content:
+ "Five factors roll up into one readiness score. The “biggest gap” badge marks the factor holding you back. That is always the next thing to fix.",
+ position: "right",
+ },
+ {
+ target: '[data-tutorial="inflow-exceptions"]',
+ title: "Clear exceptions by pattern",
+ content:
+ "Blocked lots are grouped by shared root cause: handoff, source, or mixed. Fix the pattern once (for example, map a missing handoff field) and the whole group unblocks. This list stays empty until your data brings real exceptions.",
+ position: "top",
+ },
+ {
+ target: "[data-selected-lot-panel]",
+ title: "Trace one lot",
+ content:
+ "Click any lot code to see its five-step lifecycle and which CTE evidence is missing. When an evaluation looks right, Save test run in the data editor persists it to the Inflow Workbench (write access required). Reopen this guide any time from “How to use” in the header.",
+ position: "left",
+ },
+ ];
+}
+
+// Operating guide for the standalone /tools/inflow-lab workbench (also used
+// by its embedded dashboard variant). Anchors are stable regardless of tab
+// state; the demo step carries a live action so the tour can produce the data
+// its later steps describe.
+export function buildInflowLabClientTutorialSteps({
+ runDemo,
+ isStandalone,
+}: {
+ runDemo: () => void;
+ isStandalone: boolean;
+}): TutorialStep[] {
+ return [
+ {
+ target: 'section[aria-label="Inflow Lab environment boundary"]',
+ title: "What Inflow Lab is",
+ content:
+ "A mock environment for supplier traceability data. Everything here uses test or uploaded records only. Formal production records start after signed-in import, so you can experiment freely.",
+ position: "bottom",
+ },
+ {
+ target: '[data-tutorial="lab-primary-actions"]',
+ title: "Run your first demo",
+ content: isStandalone
+ ? "Load mock scenario fills the lab with a believable supplier feed; Run pipeline processes it end to end. The button here does both in one go and jumps to the event log when it finishes."
+ : "Start guided test run loads a mock supplier feed if the lab is empty, processes it end to end, and jumps to the record log when it finishes.",
+ position: "bottom",
+ action: "Run the demo",
+ onAction: runDemo,
+ },
+ {
+ target: '[data-tutorial="lab-tabs"]',
+ title: "Find your way around",
+ content:
+ "Each tab is a workspace: Data feeder tests your own CSV before import, Fix queue groups blocking defects, Lots and Lineage trace individual lot codes, and the log tab shows every record the pipeline produced.",
+ position: "bottom",
+ },
+ {
+ target: '[data-tutorial="lab-workspace"]',
+ title: "Read the results",
+ content:
+ "Pipeline results land here: connection status, lot counts, and delivery outcomes on the overview, with the full detail one tab away. Reopen this guide any time from “How to use” in the header.",
+ position: "top",
+ },
+ ];
+}
+
+// The dismissed flag lives in localStorage, which React must treat as an
+// external store: reading it during render would break SSR, and reading it in
+// an effect would cascade an extra render. useSyncExternalStore handles both —
+// the server snapshot reports "dismissed" so the server never renders a banner
+// the client might immediately remove.
+const bannerListeners = new Set<() => void>();
+
+function subscribeToBanner(onStoreChange: () => void) {
+ bannerListeners.add(onStoreChange);
+ return () => {
+ bannerListeners.delete(onStoreChange);
+ };
+}
+
+function readBannerDismissed(key: string): boolean {
+ try {
+ return window.localStorage.getItem(key) === "true";
+ } catch {
+ return false;
+ }
+}
+
+function bannerDismissedOnServer() {
+ return true;
+}
+
+// Completing (or explicitly skipping) the walkthrough is the banner's job
+// done — calling this retires it without requiring a separate dismissal.
+export function dismissInflowLabTutorialBanner(key: string = INFLOW_LAB_TUTORIAL_BANNER_KEY) {
+ try {
+ window.localStorage.setItem(key, "true");
+ } catch {
+ /* private mode — banner simply reappears next visit */
+ }
+ bannerListeners.forEach((listener) => listener());
+}
+
+export function InflowLabTutorialBanner({
+ onStart,
+ dismissKey = INFLOW_LAB_TUTORIAL_BANNER_KEY,
+ description = "A guided walkthrough: load supplier data, run a sandbox test, read the readiness score, and clear blocked lots.",
+}: {
+ onStart: () => void;
+ dismissKey?: string;
+ description?: string;
+}) {
+ const readDismissed = useCallback(() => readBannerDismissed(dismissKey), [dismissKey]);
+ const dismissed = useSyncExternalStore(
+ subscribeToBanner,
+ readDismissed,
+ bannerDismissedOnServer,
+ );
+
+ const dismiss = useCallback(() => {
+ dismissInflowLabTutorialBanner(dismissKey);
+ }, [dismissKey]);
+
+ if (dismissed) return null;
+
+ return (
+
+
+
+
+ );
+}
diff --git a/frontend/src/app/tools/inflow-lab/opengraph-image.tsx b/frontend/src/app/tools/inflow-lab/opengraph-image.tsx
new file mode 100644
index 000000000..604282c15
--- /dev/null
+++ b/frontend/src/app/tools/inflow-lab/opengraph-image.tsx
@@ -0,0 +1,10 @@
+import { renderOgCard, OG_SIZE, OG_CONTENT_TYPE } from '@/lib/og-image';
+
+export const runtime = 'edge';
+export const alt = 'Preflight supplier data before it becomes evidence';
+export const size = OG_SIZE;
+export const contentType = OG_CONTENT_TYPE;
+
+export default function Image() {
+ return renderOgCard({ eyebrow: 'Inflow Lab', title: 'Preflight supplier data before it becomes evidence' });
+}
diff --git a/frontend/src/app/tools/inflow-lab/page.tsx b/frontend/src/app/tools/inflow-lab/page.tsx
index 143179663..a9a4c7688 100644
--- a/frontend/src/app/tools/inflow-lab/page.tsx
+++ b/frontend/src/app/tools/inflow-lab/page.tsx
@@ -1,5 +1,29 @@
-import { notFound } from "next/navigation";
+import { Metadata } from "next";
+import { InflowLabClient } from "./components/InflowLabClient";
+import { FeatureNotEnabled } from "@/components/experimental/FeatureNotEnabled";
+import { isExperimentalToolEnabled } from "@/lib/experimental-features";
+
+export const metadata: Metadata = {
+ title: "RegEngine Inflow Lab | Supplier Data Preflight Workbench",
+ description:
+ "Preflight supplier traceability data, validate FSMA 204 Critical Tracking Event (CTE) and Key Data Element (KDE) coverage, generate fix queues, replay scenarios, and gate production evidence commits.",
+ openGraph: {
+ title: "RegEngine Inflow Lab | Supplier Data Preflight Workbench",
+ description: "Inflow prepares supplier data before the Engine commits validated, tenant-scoped FSMA 204 evidence.",
+ type: "website",
+ url: "https://www.regengine.co/tools/inflow-lab",
+ },
+};
export default function InflowLabPage() {
- notFound();
+ if (!isExperimentalToolEnabled("inflow-lab")) {
+ return (
+
+ );
+ }
+ return ;
}
diff --git a/frontend/src/lib/inflow-error.ts b/frontend/src/lib/inflow-error.ts
new file mode 100644
index 000000000..e9a73d9ef
--- /dev/null
+++ b/frontend/src/lib/inflow-error.ts
@@ -0,0 +1,6 @@
+export function inflowErrorMessage(status: number, payload: Record): string {
+ if (typeof payload.error === 'string' && payload.error) return payload.error;
+ if (typeof payload.detail === 'string' && payload.detail) return payload.detail;
+ if (status === 404) return 'Inflow Lab is unavailable on this deployment. Core backend health does not include the simulator.';
+ return `Inflow Lab request failed: ${status}`;
+}
From d82402a714a6f6603c9aba53292be9a8627e814a Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Mon, 2 Nov 2026 23:56:39 -0800
Subject: [PATCH 2/8] fix(inflow-lab): un-retire /tools/inflow-lab as of
efddbdaf
Narrow the restore: bring back only the gated tools page and its
client/tutorial/opengraph plus inflow-error helper from efddbdaf.
Drop the proxy scenarios allowlist and /demo redirect (demo was
retired in #3543, not #3544).
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
frontend/next.config.js | 7 -
.../__tests__/api/inflow-lab-proxy.test.ts | 28 ---
.../pages/inflow-lab-restore.test.tsx | 45 ----
.../src/app/api/inflow-lab/[...path]/route.ts | 24 +--
.../inflow-lab/components/InflowLabClient.tsx | 199 ++++++++++++++----
.../components/InflowLabTutorial.tsx | 8 +-
6 files changed, 166 insertions(+), 145 deletions(-)
delete mode 100644 frontend/src/__tests__/pages/inflow-lab-restore.test.tsx
diff --git a/frontend/next.config.js b/frontend/next.config.js
index 8d2ebc51d..61a30fae4 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -110,13 +110,6 @@ const nextConfig = {
async redirects() {
return [
// Existing redirects
- // /demo workspace was removed with synthetic demo cleanup (#3543).
- // Send partners to the restored Inflow Lab tool on purpose.
- {
- source: '/demo',
- destination: '/tools/inflow-lab',
- permanent: false,
- },
{
source: '/ftl-checker',
destination: '/tools/ftl-checker',
diff --git a/frontend/src/__tests__/api/inflow-lab-proxy.test.ts b/frontend/src/__tests__/api/inflow-lab-proxy.test.ts
index 9a5b9abfd..cb9a190b9 100644
--- a/frontend/src/__tests__/api/inflow-lab-proxy.test.ts
+++ b/frontend/src/__tests__/api/inflow-lab-proxy.test.ts
@@ -171,34 +171,6 @@ describe('Inflow Lab API proxy', () => {
expect(mockFetch).toHaveBeenCalledTimes(1);
});
- it('proxies GET /api/inflow-lab/scenarios to the simulator /api/scenarios list', async () => {
- mockSessionThenUpstream(
- new Response(JSON.stringify({ scenarios: [{ id: 'leafy_greens_supplier', label: 'Leafy greens supplier' }] }), {
- status: 200,
- headers: { 'content-type': 'application/json' },
- }),
- );
-
- const response = await GET(
- makeRequest({ url: 'http://localhost/api/inflow-lab/scenarios' }),
- { params: Promise.resolve({ path: ['scenarios'] }) },
- );
-
- expect(response.status).toBe(200);
- expect(mockFetch).toHaveBeenNthCalledWith(
- 2,
- new URL('http://inflow.test/api/scenarios'),
- expect.objectContaining({
- method: 'GET',
- cache: 'no-store',
- redirect: 'manual',
- }),
- );
- await expect(response.json()).resolves.toEqual({
- scenarios: [{ id: 'leafy_greens_supplier', label: 'Leafy greens supplier' }],
- });
- });
-
it('returns exact 404 in production unless the server-only proxy flag is enabled', async () => {
vi.stubEnv('NODE_ENV', 'production');
vi.stubEnv('REGENGINE_ENABLE_INFLOW_LAB_PROXY', '');
diff --git a/frontend/src/__tests__/pages/inflow-lab-restore.test.tsx b/frontend/src/__tests__/pages/inflow-lab-restore.test.tsx
deleted file mode 100644
index 84ca9faa7..000000000
--- a/frontend/src/__tests__/pages/inflow-lab-restore.test.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { renderToStaticMarkup } from "react-dom/server";
-import type { ReactElement } from "react";
-
-vi.mock("next/navigation", () => ({
- notFound: vi.fn(() => {
- throw new Error("notFound");
- }),
-}));
-
-vi.mock("@/components/experimental/FeatureNotEnabled", () => ({
- FeatureNotEnabled: ({ featureName }: { featureName: string }) => (
-
- Test completeness uses the selected lot. Formal production records come later from signed in imports.
+ Test previews use the selected lot and date window. Formal production records come later from signed-in imports.
+ {exportReadyCount} lots are test complete. {exceptionLots.length} exception lots remain visible. Formal production records require signed-in imports.
+
+ Selected lot is an exception. Test-complete counts include only complete mock lots; this lot stays visible until missing CTE/KDE evidence or delivery issues are resolved.
+
+ )}
+
+
+
+
Test CSV preview
+
+ Includes {exportReadyCount} test-complete lots. {exceptionLots.length} exception lots are flagged for review. Formal production records are generated only from imported production data.
+
+
+
+
+
+
Test EPCIS preview
+
+ Uses the same lot and date filters for interoperability testing. It is not production evidence.
+
+
+
+
+
+ )}
+
{activeTab === "Diagnostics" && (
{!isStandalone && (
@@ -2733,7 +2821,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
- {isStandalone && (
+ {isStandalone ? (
<>
@@ -2757,6 +2845,35 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
>
+ ) : (
+
+
+
+
+ Test preview filters
+
+
+
+
+
+
+
+
)}
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
index f7ab76b9e..420e1f967 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabTutorial.tsx
@@ -21,7 +21,7 @@ export const INFLOW_LAB_CLIENT_TUTORIAL_BANNER_KEY = "inflow-lab-client-tutorial
//
// A builder rather than a constant: the "run your first test" step carries a
// real action button wired to the page's guided proof, so the walkthrough can
-// evaluate the user's own rows and fill the panels its later steps describe.
+// produce the data its later steps describe instead of narrating empty panels.
export function buildInflowLabTutorialSteps({
runGuidedProof,
}: {
@@ -39,14 +39,14 @@ export function buildInflowLabTutorialSteps({
target: '[data-tutorial="inflow-load-data"]',
title: "Bring supplier data in",
content:
- "Upload CSV takes your own supplier rows and opens them in the data editor below, where you can also paste rows directly. Nothing is preloaded: until you add data, the panels show No data yet.",
+ "Upload CSV takes your own supplier rows; Load realistic sample fills the editor with a believable farm-to-DC trace so you can explore safely. Both open the data editor below.",
position: "bottom",
},
{
target: '[data-tutorial="inflow-guided-proof"]',
title: "Run your first test",
content:
- "This evaluates the rows you uploaded or pasted, end to end. The highlighted button is live, or use the one right here. The chain, pipeline, score, and lot panels fill with the results. With no rows loaded, it opens the data editor instead.",
+ "This evaluates the built-in sample end to end. The highlighted button is live, or use the one right here. The chain, pipeline, score, and lot panels fill with the results.",
position: "bottom",
action: "Run trace",
onAction: runGuidedProof,
@@ -69,7 +69,7 @@ export function buildInflowLabTutorialSteps({
target: '[data-tutorial="inflow-exceptions"]',
title: "Clear exceptions by pattern",
content:
- "Blocked lots are grouped by shared root cause: handoff, source, or mixed. Fix the pattern once (for example, map a missing handoff field) and the whole group unblocks. This list stays empty until your data brings real exceptions.",
+ "Blocked lots are grouped by shared root cause: handoff, source, or mixed. Fix the pattern once (for example, map a missing handoff field) and the whole group unblocks. The sample data passes cleanly, so this list is empty until your own data brings real exceptions.",
position: "top",
},
{
From 99de798f1afc3bda560b3b2b5a44e128e02cecdd Mon Sep 17 00:00:00 2001
From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
Date: Tue, 3 Nov 2026 00:02:44 -0800
Subject: [PATCH 3/8] fix(inflow-lab): copy gate and lint on restored tools
page
Rewrite restored Inflow Lab copy to clear invented-entity, sample-demo,
and sandbox rules (fixture/CSV-check wording; Example Grower Co). Fix
react-hooks/refs and preserve-manual-memoization errors introduced by
newer eslint against the efddbdaf client.
Signed-off-by: Christopher <131207452+PetrefiedThunder@users.noreply.github.com>
---
.../inflow-lab/components/InflowLabClient.tsx | 120 +++++++++---------
.../components/InflowLabTutorial.tsx | 14 +-
2 files changed, 68 insertions(+), 66 deletions(-)
diff --git a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
index f90f0d13a..172724118 100644
--- a/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
+++ b/frontend/src/app/tools/inflow-lab/components/InflowLabClient.tsx
@@ -2,7 +2,7 @@
import Link from "next/link";
import { inflowErrorMessage } from "@/lib/inflow-error";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState, useCallback } from "react";
import {
AlertTriangle,
ArrowDownToLine,
@@ -243,17 +243,17 @@ const SANDBOX_CTE_TYPES = [
"transformation",
];
const FEEDER_SAMPLE_CSV = `cte_type,traceability_lot_code,product_description,quantity,unit_of_measure,location_name,timestamp,harvest_date,cooling_date,packing_date,ship_date,receive_date,ship_from_location,ship_to_location,receiving_location,immediate_previous_source,reference_document,tlc_source_reference,harvester_business_name,harvest_location_ref
-harvesting,TLC-FEED-001,Romaine Lettuce,120,cases,Valley Fresh Farms,2026-04-26T15:20:00Z,2026-04-26,,,,,,,,,HARV-001,SRC-HARV-001,Valley Fresh Farms,Valley Fresh Farms Field 7
-cooling,TLC-FEED-001,Romaine Lettuce,120,cases,Salinas Cooling Hub,2026-04-26T18:12:00Z,,2026-04-26,,,,,,,,COOL-001,SRC-COOL-001,,Valley Fresh Farms Field 7
-initial_packing,TLC-FEED-001,Romaine Lettuce,118,cases,Salinas Packhouse,2026-04-26T20:12:00Z,,,2026-04-26,,,,,,,PACK-001,SRC-PACK-001,Valley Fresh Farms,Valley Fresh Farms Field 7
-shipping,TLC-FEED-001,Romaine Lettuce,118,cases,Salinas Packout Dock,2026-04-26T22:41:00Z,,,,2026-04-26,,Salinas Packhouse,Bay Area DC,,,BOL-001,SRC-PACK-001,,Valley Fresh Farms Field 7
-receiving,TLC-FEED-001,Romaine Lettuce,118,cases,Bay Area DC,2026-04-27T02:04:00Z,,,,,2026-04-27,,,Bay Area DC,Salinas Packout Dock,REC-001,SRC-PACK-001,,Valley Fresh Farms Field 7`;
+harvesting,TLC-FEED-001,Romaine Lettuce,120,cases,Example Grower Co,2026-04-26T15:20:00Z,2026-04-26,,,,,,,,,HARV-001,SRC-HARV-001,Example Grower Co,Example Grower Field 7
+cooling,TLC-FEED-001,Romaine Lettuce,120,cases,Salinas Cooling Hub,2026-04-26T18:12:00Z,,2026-04-26,,,,,,,,COOL-001,SRC-COOL-001,,Example Grower Field 7
+initial_packing,TLC-FEED-001,Romaine Lettuce,118,cases,Salinas Packhouse,2026-04-26T20:12:00Z,,,2026-04-26,,,,,,,PACK-001,SRC-PACK-001,Example Grower Co,Example Grower Field 7
+shipping,TLC-FEED-001,Romaine Lettuce,118,cases,Salinas Packout Dock,2026-04-26T22:41:00Z,,,,2026-04-26,,Salinas Packhouse,Bay Area DC,,,BOL-001,SRC-PACK-001,,Example Grower Field 7
+receiving,TLC-FEED-001,Romaine Lettuce,118,cases,Bay Area DC,2026-04-27T02:04:00Z,,,,,2026-04-27,,,Bay Area DC,Salinas Packout Dock,REC-001,SRC-PACK-001,,Example Grower Field 7`;
const MISSING_DESTINATION_CSV = `cte_type,traceability_lot_code,product_description,quantity,unit_of_measure,location_name,timestamp,harvest_date,cooling_date,packing_date,ship_date,receive_date,ship_from_location,ship_to_location,receiving_location,immediate_previous_source,reference_document,tlc_source_reference,harvester_business_name,harvest_location_ref
-harvesting,TLC-FEED-002,Romaine Lettuce,104,cases,Valley Fresh Farms,2026-04-26T15:20:00Z,2026-04-26,,,,,,,,,HARV-002,SRC-HARV-002,Valley Fresh Farms,Valley Fresh Farms Field 11
-cooling,TLC-FEED-002,Romaine Lettuce,104,cases,Salinas Cooling Hub,2026-04-26T18:12:00Z,,2026-04-26,,,,,,,,COOL-002,SRC-COOL-002,,Valley Fresh Farms Field 11
-initial_packing,TLC-FEED-002,Romaine Lettuce,103,cases,Salinas Packhouse,2026-04-26T20:12:00Z,,,2026-04-26,,,,,,,PACK-002,SRC-PACK-002,Valley Fresh Farms,Valley Fresh Farms Field 11
-shipping,TLC-FEED-002,Romaine Lettuce,103,cases,Salinas Packout Dock,2026-04-26T22:41:00Z,,,,2026-04-26,,Salinas Packhouse,,,,BOL-002,SRC-PACK-002,,Valley Fresh Farms Field 11
-receiving,TLC-FEED-002,Romaine Lettuce,103,cases,Bay Area DC,2026-04-27T02:04:00Z,,,,,2026-04-27,,,Bay Area DC,Salinas Packout Dock,REC-002,SRC-PACK-002,,Valley Fresh Farms Field 11`;
+harvesting,TLC-FEED-002,Romaine Lettuce,104,cases,Example Grower Co,2026-04-26T15:20:00Z,2026-04-26,,,,,,,,,HARV-002,SRC-HARV-002,Example Grower Co,Example Grower Field 11
+cooling,TLC-FEED-002,Romaine Lettuce,104,cases,Salinas Cooling Hub,2026-04-26T18:12:00Z,,2026-04-26,,,,,,,,COOL-002,SRC-COOL-002,,Example Grower Field 11
+initial_packing,TLC-FEED-002,Romaine Lettuce,103,cases,Salinas Packhouse,2026-04-26T20:12:00Z,,,2026-04-26,,,,,,,PACK-002,SRC-PACK-002,Example Grower Co,Example Grower Field 11
+shipping,TLC-FEED-002,Romaine Lettuce,103,cases,Salinas Packout Dock,2026-04-26T22:41:00Z,,,,2026-04-26,,Salinas Packhouse,,,,BOL-002,SRC-PACK-002,,Example Grower Field 11
+receiving,TLC-FEED-002,Romaine Lettuce,103,cases,Bay Area DC,2026-04-27T02:04:00Z,,,,,2026-04-27,,,Bay Area DC,Salinas Packout Dock,REC-002,SRC-PACK-002,,Example Grower Field 11`;
const BROKEN_LINEAGE_CSV = `cte_type,traceability_lot_code,product_description,quantity,unit_of_measure,location_name,timestamp,harvest_date,cooling_date,packing_date,ship_date,receive_date,ship_from_location,ship_to_location,receiving_location,immediate_previous_source,reference_document,tlc_source_reference,harvester_business_name,harvest_location_ref,transformation_date,input_traceability_lot_codes
harvesting,TLC-FEED-003,Romaine Lettuce,220,cases,Desert Bloom Farm,2026-04-26T14:43:00Z,2026-04-26,,,,,,,,,HARV-003,SRC-HARV-003,Desert Bloom Farm,Desert Bloom Farm Block 3,,
cooling,TLC-FEED-003,Romaine Lettuce,220,cases,Imperial Pre-Cool Facility,2026-04-26T17:51:00Z,,2026-04-26,,,,,,,,COOL-003,SRC-COOL-003,,Desert Bloom Farm Block 3,,
@@ -437,10 +437,10 @@ function summarizeFeederDiagnosis(result: SandboxEvaluationResult): FeederDiagno
status,
headline:
status === "clear"
- ? "Sandbox diagnosis found no blocking fixes"
+ ? "CSV check found no blocking fixes"
: status === "blocked"
- ? "Sandbox diagnosis found blockers before mapping"
- : "Sandbox diagnosis found corrections before mapping",
+ ? "CSV check found blockers before mapping"
+ : "CSV check found corrections before mapping",
impact:
status === "clear"
? "This test run can move into import mapping, but it still cannot create formal production records."
@@ -465,7 +465,7 @@ function buildFeederRemediationPlan(result: SandboxEvaluationResult): FeederReme
if (result.total_rule_failures > 0) {
steps.push({
title: "Resolve failed FSMA checks",
- detail: "Review the failed row-level rules, fix source data, and rerun the sandbox diagnosis.",
+ detail: "Review the failed row-level rules, fix source data, and rerun the CSV check.",
});
}
if (result.duplicate_warnings?.length || result.entity_warnings?.length) {
@@ -516,7 +516,7 @@ function buildFixQueue(result: SandboxEvaluationResult | null, lotSummaries: {
owner: "Source data owner",
status: result.submission_blocked ? "open" : "waiting",
severity: result.submission_blocked ? "blocked" : "warning",
- impact: event.blocking_defects?.[0]?.remediation || "Correct the source record and rerun sandbox validation.",
+ impact: event.blocking_defects?.[0]?.remediation || "Correct the source record and rerun CSV validation.",
source: event.traceability_lot_code || `row-${event.event_index + 1}`,
});
});
@@ -529,7 +529,7 @@ function buildFixQueue(result: SandboxEvaluationResult | null, lotSummaries: {
status: "open",
severity: "blocked",
impact: "Commit gate stays closed until this blocker is resolved.",
- source: "Sandbox evaluator",
+ source: "CSV evaluator",
});
});
@@ -541,7 +541,7 @@ function buildFixQueue(result: SandboxEvaluationResult | null, lotSummaries: {
status: "waiting",
severity: "warning",
impact: "Duplicate records can weaken audit trust and supplier feed quality.",
- source: "Sandbox evaluator",
+ source: "CSV evaluator",
});
}
@@ -637,7 +637,7 @@ const events: TraceEvent[] = [
cte: "harvesting",
lotCode: lotCodes[0],
product: "Green Leaf Lettuce",
- location: "Valley Fresh Farms",
+ location: "Example Grower Co",
timestamp: "Apr 26, 2026, 3:20 PM",
mode: "simulation",
attempts: 1,
@@ -714,7 +714,7 @@ const events: TraceEvent[] = [
cte: "harvesting",
lotCode: lotCodes[2],
product: "Green Leaf Lettuce",
- location: "Valley Fresh Farms",
+ location: "Example Grower Co",
timestamp: "Apr 26, 2026, 4:28 PM",
mode: "simulation",
attempts: 1,
@@ -724,7 +724,7 @@ const events: TraceEvent[] = [
const lineageByLot: Record = {
[lotCodes[0]]: [
- { cte: "Harvesting", location: "Valley Fresh Farms", time: "Apr 26, 3:20 PM", state: "complete" },
+ { cte: "Harvesting", location: "Example Grower Co", time: "Apr 26, 3:20 PM", state: "complete" },
{ cte: "Cooling", location: "Salinas Cooling Hub", time: "Apr 26, 6:12 PM", state: "complete" },
{ cte: "Initial packing", location: "Salinas Packhouse", time: "Apr 26, 8:12 PM", state: "complete" },
{ cte: "Shipping", location: "Salinas Packout Dock", time: "Apr 26, 10:41 PM", state: "complete" },
@@ -738,7 +738,7 @@ const lineageByLot: Record = {
{ cte: "DC receiving", location: "Los Angeles DC", time: "Apr 27, 1:24 AM", state: "complete" },
],
[lotCodes[2]]: [
- { cte: "Harvesting", location: "Valley Fresh Farms", time: "Apr 26, 4:28 PM", state: "complete" },
+ { cte: "Harvesting", location: "Example Grower Co", time: "Apr 26, 4:28 PM", state: "complete" },
{ cte: "Cooling", location: "Salinas Cooling Hub", time: "Apr 26, 5:26 PM", state: "complete" },
{ cte: "Initial packing", location: "Not captured", time: "Missing KDEs", state: "missing" },
{ cte: "Shipping", location: "Not captured", time: "Missing CTE", state: "missing" },
@@ -1075,7 +1075,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
if (!response.ok) {
const payload = await response.json().catch(() => ({}));
- const detail = typeof payload.detail === "string" ? payload.detail : `Sandbox evaluation failed: ${response.status}`;
+ const detail = typeof payload.detail === "string" ? payload.detail : `CSV evaluation failed: ${response.status}`;
throw new Error(detail);
}
@@ -1252,17 +1252,22 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
setActiveTab(isStandalone ? "Event log" : "Record log");
};
- // The tour's demo action reads through a ref so the step array stays
+ // The tour's walkthrough action reads through a ref so the step array stays
// referentially stable while the action always sees fresh state.
const startMachineDemoRef = useRef(startMachineDemo);
- startMachineDemoRef.current = startMachineDemo;
+ useEffect(() => {
+ startMachineDemoRef.current = startMachineDemo;
+ });
+ const runTutorialDemo = useCallback(() => {
+ void startMachineDemoRef.current();
+ }, []);
const tutorialSteps = useMemo(
() =>
buildInflowLabClientTutorialSteps({
- runDemo: () => void startMachineDemoRef.current(),
+ runDemo: runTutorialDemo,
isStandalone,
}),
- [isStandalone],
+ [isStandalone, runTutorialDemo],
);
const startTutorial = () => {
@@ -1287,7 +1292,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
const validationOutcome = feederResult
? feederDefectCount
? `${feederDefectCount} validation issue${feederDefectCount === 1 ? "" : "s"} found`
- : "No sandbox validation issues"
+ : "No CSV validation issues"
: exceptionCount
? `${exceptionCount} exception${exceptionCount === 1 ? "" : "s"} to review`
: "No blocking exceptions";
@@ -1346,19 +1351,18 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
[backendFixQueue, feederResult, uniqueLots]
);
const unresolvedFixCount = fixQueue.filter((item) => item.status === "open" || item.status === "waiting").length;
- const supplierReadiness = useMemo(
- () => [
+ const supplierReadiness = [
{
- name: "Valley Fresh Farms",
- status: exportReadyCount > 0 ? "Passing validation" : "Sample received",
+ name: "Example Grower Co",
+ status: exportReadyCount > 0 ? "Passing validation" : "Fixture received",
score: Math.max(58, readinessScore - 3),
blocker: exceptionLots.length ? "Waiting on missing CTE handoffs" : "No open blockers",
},
{
name: "FreshPack Central",
- status: feederResult ? (feederResult.submission_blocked ? "Sample received" : "Mapping configured") : "Invited",
+ status: feederResult ? (feederResult.submission_blocked ? "Fixture received" : "Mapping configured") : "Invited",
score: feederResult ? Math.max(35, Math.round((feederResult.compliant_events / Math.max(1, feederResult.total_events)) * 100)) : 44,
- blocker: feederResult?.submission_blocked ? "Sandbox blockers need correction" : "Mapping profile not promoted",
+ blocker: feederResult?.submission_blocked ? "CSV blockers need correction" : "Mapping profile not promoted",
},
{
name: "Bay Area DC",
@@ -1366,21 +1370,19 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
score: Math.min(98, readinessScore + 5),
blocker: selectedIsExportReady ? "No open blockers" : "Selected lot needs complete lineage",
},
- ],
- [exceptionLots.length, exportReadyCount, feederResult, readinessScore, selectedIsExportReady]
- );
+ ];
const supplierAverageScore = Math.round(
supplierReadiness.reduce((total, supplier) => total + supplier.score, 0) / Math.max(1, supplierReadiness.length)
);
- const activeDataBoundary = feederResult ? "Uploaded CSV test run" : serviceEvents.length ? "Mock feed data" : "Bundled mock fixture";
+ const activeDataBoundary = feederResult ? "Uploaded CSV check run" : serviceEvents.length ? "Mock feed data" : "Bundled mock fixture";
const boundaryStateDetail = feederResult
? "You are viewing uploaded CSV data checked in this browser session."
: serviceEvents.length
- ? "You are viewing mock feed data; no uploaded CSV sandbox run has been evaluated yet."
- : "You are viewing bundled fixture data that is safe for testing and demos.";
+ ? "You are viewing mock feed data; no uploaded CSV check run has been evaluated yet."
+ : "You are viewing bundled fixture data that is safe for exploration and walkthroughs.";
const boundarySteps = [
{
- label: "Sandbox diagnosis",
+ label: "CSV check",
value: feederResult ? "Uploaded CSV was checked" : "No uploaded CSV run yet",
tone: "border-blue-200 bg-blue-50 text-blue-900",
},
@@ -1576,7 +1578,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
{isStandalone
? "Review mock-generated records, trace lots, and preview test filters without mixing simulator data into production imports."
- : "Validate FSMA 204 records, resolve exceptions, prove traceability, and keep formal production records separate from test data."}
+ : "Validate FSMA 204 records, resolve exceptions, prove traceability, and keep formal production records separate from fixture data."}
- Uses the public stateless sandbox evaluator. Data is parsed, normalized, and checked against FSMA 204 rules without being stored or promoted to production ingestion.
+ Uses the public stateless CSV evaluator. Data is parsed, normalized, and checked against FSMA 204 rules without being stored or promoted to production ingestion.
- Move from free diagnosis to a monitored feed without treating sandbox rows as formal production records.
+ Move from free diagnosis to a monitored feed without treating CSV check rows as formal production records.
{[
["1", "Diagnose free", feederResult ? `${feederResult.total_events} events evaluated` : "Evaluate CSV first"],
- ["2", "Save as test run", workbenchRunId ? `Persisted as ${workbenchRunId}` : feederSavedAt ? `Saved ${formatServiceTime(feederSavedAt)}` : "Keep the sandbox result for handoff"],
+ ["2", "Save as test run", workbenchRunId ? `Persisted as ${workbenchRunId}` : feederSavedAt ? `Saved ${formatServiceTime(feederSavedAt)}` : "Keep the CSV check result for handoff"],
["3", "Convert to import mapping", "Turn headers and CTE aliases into an import setup"],
["4", "Monitor live feed", "Watch connector health, failures, and exceptions"],
["5", "Generate production evidence", "Use signed-in production records only"],
@@ -2095,7 +2097,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
No open fixes in the current run
- Ready lots can move to production import mapping. Sandbox and mock records still stay outside formal production records.
+ Ready lots can move to production import mapping. CSV check and mock records still stay outside formal production records.
{[
["Simulation", "Mock and scenario data only"],
- ["Preflight", feederResult ? "Sandbox result available" : "Awaiting sandbox evaluation"],
+ ["Preflight", feederResult ? "CSV check result available" : "Awaiting CSV evaluation"],
["Staging", feederSavedAt ? "Saved test run ready for mapping" : "Save a test run before mapping"],
[
"Production evidence",
@@ -2205,7 +2207,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {
Supplier readiness
- Supplier state uses sample validation, mapping progress, repeated fixes, and export-ready lots to show who is blocking readiness.
+ Supplier state uses fixture validation, mapping progress, repeated fixes, and export-ready lots to show who is blocking readiness.
{isStandalone
? "Run the pipeline to generate FSMA 204 traceability records."
- : "Load test data to validate FSMA 204 records before production ingestion."}
+ : "Load fixture data to validate FSMA 204 records before production ingestion."}
{selectedReadiness.missingCtes.length > 0
- ? `Capture missing KDE evidence for ${selectedReadiness.missingCtes.join(", ")} before this lot is counted as test complete.`
- : "Resolve delivery status before this lot is counted as test complete."}
+ ? `Capture missing KDE evidence for ${selectedReadiness.missingCtes.join(", ")} before this lot is counted as fixture complete.`
+ : "Resolve delivery status before this lot is counted as fixture complete."}
Fixture and source settings are kept here for implementation review; the main workflow stays focused on validation and evidence.
@@ -2647,7 +2649,7 @@ export function InflowLabClient({ mode = "standalone" }: InflowLabClientProps) {