diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md index e4c03200..5395d4f5 100644 --- a/apps/dashboard/README.md +++ b/apps/dashboard/README.md @@ -1,6 +1,6 @@ -# FlowMemory Dashboard V0 +# FlowMemory Dashboard / FlowChain Workbench V0 -Local operator/explorer app for inspecting FlowMemory V0 fixture output. It is intentionally fixture-backed and does not claim production live data, wallet support, token pricing, or hosted deployment. +Local operator/explorer app for inspecting FlowMemory V0 fixture output and the FlowChain private/local testnet workbench surface. It is intentionally local-first and does not claim value-bearing wallet support, token pricing, or hosted deployment. ## Run Locally @@ -25,6 +25,28 @@ npm test npm run build ``` +From the repo root, the same checks are: + +```powershell +npm test --prefix apps/dashboard +npm run build --prefix apps/dashboard +``` + +The first route is the FlowChain workbench. It tries the local control-plane API at: + +```text +http://127.0.0.1:8787 +``` + +Override that endpoint when needed: + +```powershell +$env:VITE_FLOWCHAIN_CONTROL_PLANE_URL="http://127.0.0.1:8787" +npm run dev +``` + +If the API is not running, the workbench marks the control-plane as offline, shows stale fixture fallback where appropriate, and keeps rendering deterministic local data. This app is for private/local validation and canary review only; it does not initiate value-bearing wallet flows. + ## Data Boundary The canonical dashboard fixture is generated by the launch-core command and lives at: @@ -33,13 +55,27 @@ The canonical dashboard fixture is generated by the launch-core command and live fixtures/dashboard/flowmemory-dashboard-v0.json ``` +The guarded Base canary review fixture is separate: + +```text +fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json +``` + The app loads its runtime copy from: ```text apps/dashboard/public/data/flowmemory-dashboard-v0.json +apps/dashboard/public/data/flowmemory-dashboard-base-canary-v0.json ``` -The `dev` and `build` scripts run `npm run sync:fixtures`, which copies the canonical fixture into the Vite public data folder before the app starts or builds. +The `dev` and `build` scripts run `npm run sync:fixtures`, which copies the canonical dashboard fixture and the existing launch-core devnet state into the Vite public data folder before the app starts or builds. + +Workbench fixture fallback paths: + +```text +apps/dashboard/public/data/flowchain-local-devnet-state.json +apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json +``` Generated local source outputs land under the fixture boundary first: @@ -54,7 +90,9 @@ fixtures/dashboard/generated/hardware-heartbeats.json ## Current Views +- FlowChain workbench - Overview +- Base canary review - Flow Memory / Rootflow - FlowPulse stream - Rootfields @@ -67,6 +105,16 @@ fixtures/dashboard/generated/hardware-heartbeats.json Every displayed record carries source subsystem, fixture/local origin, chain context, ID/hash, status, and last-updated metadata when available. +The workbench adds local setup/API status plus object views for blocks, transactions, agents, models, receipts, memory cells, artifacts, verifier reports, challenges, finality, provenance, and raw JSON. When a current fixture does not yet contain a private-testnet object type, the view stays empty and names the expected control-plane endpoint. + +Workbench object coverage: + +```text +node/chain status, blocks, transactions, rootfields, agents, models, work receipts, +memory cells, artifacts, verifier modules, verifier reports, challenges, finality, +provenance/source, hardware signals, raw JSON +``` + ## Status Vocabulary Dashboard V0 visually distinguishes: diff --git a/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json new file mode 100644 index 00000000..ef04135f --- /dev/null +++ b/apps/dashboard/public/data/flowchain-local-devnet-dashboard-state.json @@ -0,0 +1,62 @@ +{ + "artifactCommitments": { + "artifact:demo:001": { + "artifactId": "artifact:demo:001", + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "rootfieldId": "rootfield:demo:alpha", + "uriHint": "fixture://artifact/demo/001" + } + }, + "baseAnchors": { + "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7": { + "anchorId": "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7", + "appchainChainId": "flowmemory-local-devnet-v0", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "blockRangeEnd": 1, + "blockRangeStart": 1, + "finalityStatus": "local-placeholder", + "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023" + } + }, + "blockHeight": 2, + "rootfields": { + "rootfield:demo:alpha": { + "active": true, + "latestRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "metadataHash": "0x514006e494877d3d6a69848ed6264b152ebe6b73b1112d8ff1b9b48860509a2f", + "owner": "operator:local-demo", + "pulseCount": 2, + "rootCount": 1, + "rootfieldId": "rootfield:demo:alpha", + "schemaHash": "0x5909a6dc30ffe1fcd89eebc118f6d2096c4d4c3ccdcc851dc0e4386fe997c6d7" + } + }, + "schema": "flowmemory.dashboard_state.local_devnet.v0", + "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "verifierReports": { + "report:demo:001": { + "reasonCodes": [], + "receiptId": "receipt:demo:001", + "reportDigest": "0xe75619ea62e7a6d9593debe0123d366ae0f0104cff86d9a69391fb5c1e074f4c", + "reportId": "report:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "status": "verified", + "verifierId": "verifier:local-demo" + } + }, + "workReceipts": { + "receipt:demo:001": { + "artifactCommitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "inputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "outputRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0", + "workerId": "worker:local-demo" + } + } +} diff --git a/apps/dashboard/public/data/flowchain-local-devnet-state.json b/apps/dashboard/public/data/flowchain-local-devnet-state.json new file mode 100644 index 00000000..56b211b6 --- /dev/null +++ b/apps/dashboard/public/data/flowchain-local-devnet-state.json @@ -0,0 +1,130 @@ +{ + "schema": "flowmemory.local_devnet.state.v0", + "chainId": "flowmemory-local-devnet-v0", + "genesisHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "nextBlockNumber": 3, + "logicalTime": 1778688002, + "parentHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6", + "rootfields": { + "rootfield:demo:alpha": { + "rootfieldId": "rootfield:demo:alpha", + "owner": "operator:local-demo", + "schemaHash": "0x5909a6dc30ffe1fcd89eebc118f6d2096c4d4c3ccdcc851dc0e4386fe997c6d7", + "metadataHash": "0x514006e494877d3d6a69848ed6264b152ebe6b73b1112d8ff1b9b48860509a2f", + "latestRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "pulseCount": 2, + "rootCount": 1, + "active": true + } + }, + "artifactCommitments": { + "artifact:demo:001": { + "artifactId": "artifact:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "commitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "uriHint": "fixture://artifact/demo/001" + } + }, + "workReceipts": { + "receipt:demo:001": { + "receiptId": "receipt:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "workerId": "worker:local-demo", + "inputRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "outputRoot": "0xbdb66f777635a2426a834652f8efee40db4f3e0b9ddd2af15f15fd065a7fe4f4", + "artifactCommitment": "0x4de1ac0e70ce73c0a03df255d1ea2a7bbcb40f05c60f1b0c1b73e0b4577c537a", + "ruleSet": "flowmemory.work.rule_set.local_demo.v0" + } + }, + "verifierReports": { + "report:demo:001": { + "reportId": "report:demo:001", + "rootfieldId": "rootfield:demo:alpha", + "receiptId": "receipt:demo:001", + "verifierId": "verifier:local-demo", + "reportDigest": "0xe75619ea62e7a6d9593debe0123d366ae0f0104cff86d9a69391fb5c1e074f4c", + "status": "verified", + "reasonCodes": [] + } + }, + "importedObservations": {}, + "importedVerifierReports": {}, + "baseAnchors": { + "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7": { + "anchorId": "0x08530e63dacb23a630bbbbd56ffc4dead54aca6d7e7ee7d920d7376eb9340ae7", + "appchainChainId": "flowmemory-local-devnet-v0", + "blockRangeStart": 1, + "blockRangeEnd": 1, + "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "workReceiptRoot": "0x8b3ef5650c9eea2f608ad9c7cb73df3c289fc0ac72ed04f46e6ae4bce0a1f023", + "verifierReportRoot": "0x4facd21e55423e182eba87355482a35daa93f53190fbd3a8d2969f9d55bc5373", + "rootfieldStateRoot": "0xb72a851dca1103410484e3272945bae5e87fc39b8f32f77d2991959b60d3bfbf", + "artifactCommitmentRoot": "0xb772a9f7273032fd3ba2da8b6476d4715bbbafbd2a7eed21ecd0d558bde3beab", + "previousAnchorId": "0x0000000000000000000000000000000000000000000000000000000000000000", + "finalityStatus": "local-placeholder" + } + }, + "blocks": [ + { + "schema": "flowmemory.local_devnet.block.v0", + "blockNumber": 1, + "parentHash": "0x0f23c892cbd2d00c10839d97ddab833698a83f8df8d6df27ceac03cfdd4b7bc9", + "logicalTime": 1778688000, + "txIds": [ + "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", + "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", + "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", + "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46", + "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa" + ], + "receipts": [ + { + "txId": "0x2cffda58c783dc026978b06a681587b19d9536ae4e158a69be855da1200f3189", + "status": "applied", + "error": null + }, + { + "txId": "0xb9f435aceb1bedb86dce821743769b28c02a42002c9cd41f2df1ea0279462ab2", + "status": "applied", + "error": null + }, + { + "txId": "0xef6df43993478d8f14d609732c7260fa08861ecc17e74137b83beda8d50931d2", + "status": "applied", + "error": null + }, + { + "txId": "0x73b81134901c2ce13e575f161d82a404c6f7cd1ef2e8ee17beb6697062175c46", + "status": "applied", + "error": null + }, + { + "txId": "0x3ac0b196a212a0e77d0a0c4b60e2283d2994b09993971b95427996700f5b92aa", + "status": "applied", + "error": null + } + ], + "stateRoot": "0x76ec5260c34184b6bb54ca406a43fc1f9591a47f37f71583a7620ef4a4065aff", + "blockHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235" + }, + { + "schema": "flowmemory.local_devnet.block.v0", + "blockNumber": 2, + "parentHash": "0xf76ac3652230cae4a4b5afcd54b0dcec9219f20ad71e21c497264668fb30f235", + "logicalTime": 1778688001, + "txIds": [ + "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7" + ], + "receipts": [ + { + "txId": "0x8f719c880f17b5d4fb6d9efd54ac276d0dd8050d11c2c7870c36a79b66bc49d7", + "status": "applied", + "error": null + } + ], + "stateRoot": "0x3e1f5fddd84f9d460ee30a380ff700b17611891b8c03eb320edf1baefe003ef9", + "blockHash": "0x7515374a9b020a6d271820031738a5190cb0fc374adcd74a88a32c0fd0d5c7a6" + } + ], + "pendingTxs": [] +} diff --git a/apps/dashboard/scripts/sync-fixtures.mjs b/apps/dashboard/scripts/sync-fixtures.mjs index c576d3f1..5a73153c 100644 --- a/apps/dashboard/scripts/sync-fixtures.mjs +++ b/apps/dashboard/scripts/sync-fixtures.mjs @@ -5,18 +5,35 @@ import { fileURLToPath } from "node:url"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const repoRoot = resolve(scriptDir, "../../.."); const destinationDir = resolve(repoRoot, "apps/dashboard/public/data"); -const fixtures = [ - "flowmemory-dashboard-v0.json", - "flowmemory-dashboard-base-canary-v0.json", +const fixtureCopies = [ + { + label: "dashboard fixture", + source: resolve(repoRoot, "fixtures/dashboard/flowmemory-dashboard-v0.json"), + destination: resolve(destinationDir, "flowmemory-dashboard-v0.json"), + }, + { + label: "Base canary dashboard fixture", + source: resolve(repoRoot, "fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json"), + destination: resolve(destinationDir, "flowmemory-dashboard-base-canary-v0.json"), + }, + { + label: "FlowChain local devnet state", + source: resolve(repoRoot, "fixtures/launch-core/generated/devnet/state.json"), + destination: resolve(destinationDir, "flowchain-local-devnet-state.json"), + }, + { + label: "FlowChain local devnet dashboard state", + source: resolve(repoRoot, "fixtures/launch-core/generated/devnet/dashboard-state.json"), + destination: resolve(destinationDir, "flowchain-local-devnet-dashboard-state.json"), + }, ]; mkdirSync(destinationDir, { recursive: true }); -for (const fixture of fixtures) { - const source = resolve(repoRoot, "fixtures/dashboard", fixture); - const destination = resolve(destinationDir, fixture); - if (existsSync(source)) { - copyFileSync(source, destination); - console.log(`Synced dashboard fixture: ${destination}`); +for (const fixture of fixtureCopies) { + if (!existsSync(fixture.source)) { + throw new Error(`Missing ${fixture.label}: ${fixture.source}`); } + copyFileSync(fixture.source, fixture.destination); + console.log(`Synced ${fixture.label}: ${fixture.destination}`); } diff --git a/apps/dashboard/src/App.tsx b/apps/dashboard/src/App.tsx index aa2be8ab..b1b1792b 100644 --- a/apps/dashboard/src/App.tsx +++ b/apps/dashboard/src/App.tsx @@ -4,6 +4,7 @@ import { AlertTriangle, RefreshCw } from "lucide-react"; import { AppShell } from "./components/AppShell"; import { DEFAULT_CANARY_DASHBOARD_DATA_PATH, fetchDashboardData } from "./data/loadDashboardData"; import type { DashboardData } from "./data/types"; +import { buildWorkbenchSnapshot, fetchWorkbenchSnapshot, type WorkbenchSnapshot } from "./data/workbench"; import { AlertsView } from "./views/AlertsView"; import { CanaryDeploymentView } from "./views/CanaryDeploymentView"; import { DevnetBlocksView } from "./views/DevnetBlocksView"; @@ -14,6 +15,7 @@ import { OverviewView } from "./views/OverviewView"; import { RawJsonInspectorView } from "./views/RawJsonInspectorView"; import { RootfieldsView } from "./views/RootfieldsView"; import { VerifierReportsView } from "./views/VerifierReportsView"; +import { WorkbenchView } from "./views/WorkbenchView"; import { WorkReceiptsView } from "./views/WorkReceiptsView"; function LoadingState() { @@ -54,6 +56,7 @@ function ErrorState({ message, onRetry }: { message: string; onRetry: () => void export default function App() { const [data, setData] = useState(null); const [canaryData, setCanaryData] = useState(null); + const [workbench, setWorkbench] = useState(null); const [error, setError] = useState(null); const [version, setVersion] = useState(0); @@ -64,10 +67,17 @@ export default function App() { fetchDashboardData(), fetchDashboardData(DEFAULT_CANARY_DASHBOARD_DATA_PATH), ]) - .then(([nextData, nextCanaryData]) => { + .then(async ([nextData, nextCanaryData]) => { + const nextWorkbench = await fetchWorkbenchSnapshot(nextData).catch((nextError: unknown) => + buildWorkbenchSnapshot(nextData, { + loadIssues: [nextError instanceof Error ? nextError.message : "Unknown workbench load error."], + }), + ); + if (!cancelled) { setData(nextData); setCanaryData(nextCanaryData); + setWorkbench(nextWorkbench); setError(null); } }) @@ -90,10 +100,15 @@ export default function App() { return ; } + if (workbench === null) { + return ; + } + return ( - + - } /> + } /> + } /> } /> } /> } /> @@ -103,7 +118,7 @@ export default function App() { } /> } /> } /> - } /> + } /> ); diff --git a/apps/dashboard/src/components/AppShell.tsx b/apps/dashboard/src/components/AppShell.tsx index 624d1f2d..cdbf4fb6 100644 --- a/apps/dashboard/src/components/AppShell.tsx +++ b/apps/dashboard/src/components/AppShell.tsx @@ -10,21 +10,25 @@ import { ClipboardCheck, RadioReceiver, LayoutDashboard, + Monitor, Network, RadioTower, ShieldCheck, } from "lucide-react"; import type { DashboardData } from "../data/types"; +import type { WorkbenchSnapshot } from "../data/workbench"; import { StatusBadge } from "./StatusBadge"; interface AppShellProps { data: DashboardData; canaryData?: DashboardData; + workbench: WorkbenchSnapshot; children: ReactNode; } const NAV_ITEMS = [ - { to: "/", label: "Overview", icon: LayoutDashboard }, + { to: "/", label: "Workbench", icon: Monitor }, + { to: "/overview", label: "Overview", icon: LayoutDashboard }, { to: "/canary", label: "Base canary", icon: RadioReceiver }, { to: "/flowmemory", label: "Flow Memory", icon: BrainCircuit }, { to: "/flowpulse", label: "FlowPulse", icon: Activity }, @@ -37,7 +41,7 @@ const NAV_ITEMS = [ { to: "/raw", label: "Raw JSON", icon: Braces }, ]; -export function AppShell({ data, canaryData, children }: AppShellProps) { +export function AppShell({ data, canaryData, workbench, children }: AppShellProps) { return (
FlowMemory - Dashboard V0 + Workbench V0
- - {data.metadata.mode} data + + {workbench.source} {canaryData ? {canaryData.metadata.mode} data ready : null} {data.chain.name}
@@ -74,15 +78,16 @@ export function AppShell({ data, canaryData, children }: AppShellProps) {
chain {data.chain.chainId} + {workbench.controlPlane.status} {data.chain.source} updated {new Date(data.chain.lastUpdated).toLocaleString()}
- Fixture/local data only. + {workbench.source === "control-plane" ? "Local API detected." : "Fixture fallback active."} - Runtime JSON is loaded from {data.metadata.runtimeDataPath}; future generated outputs should land in the - documented fixture boundary before becoming live APIs. + Runtime JSON is loaded from {data.metadata.runtimeDataPath}; control-plane integration probes{" "} + {workbench.controlPlane.url} and falls back to deterministic fixture data when unavailable.
{children}
diff --git a/apps/dashboard/src/data/workbench.ts b/apps/dashboard/src/data/workbench.ts new file mode 100644 index 00000000..0a678dd3 --- /dev/null +++ b/apps/dashboard/src/data/workbench.ts @@ -0,0 +1,1298 @@ +import type { DashboardData, DashboardStatus, Provenance, SourceSubsystem } from "./types"; + +export const DEFAULT_CONTROL_PLANE_URL = "http://127.0.0.1:8787"; +export const WORKBENCH_DEVNET_STATE_PATH = "/data/flowchain-local-devnet-state.json"; +export const WORKBENCH_DEVNET_DASHBOARD_STATE_PATH = "/data/flowchain-local-devnet-dashboard-state.json"; + +const FIXTURE_CHAIN_CONTEXT = "flowchain-private-local-testnet"; +const CONTROL_PLANE_TIMEOUT_MS = 900; + +export type WorkbenchSource = "control-plane" | "fixture-fallback"; +export type WorkbenchSectionKey = + | "blocks" + | "transactions" + | "rootfields" + | "agents" + | "models" + | "receipts" + | "memoryCells" + | "artifacts" + | "verifierModules" + | "verifierReports" + | "challenges" + | "finality" + | "provenance" + | "hardwareSignals" + | "rawJson"; + +export interface WorkbenchFact { + label: string; + value: string; +} + +export interface WorkbenchRecord { + id: string; + kind: string; + title: string; + summary: string; + status: DashboardStatus; + facts: WorkbenchFact[]; + provenance: Provenance; + raw: unknown; +} + +export interface WorkbenchSectionDefinition { + key: WorkbenchSectionKey; + label: string; + detail: string; + expectedEndpoint: string; +} + +export interface ControlPlaneProbe { + url: string; + status: "available" | "not-detected"; + checkedAt: string; + endpoints: string[]; + error?: string; + health?: unknown; + state?: unknown; +} + +export interface WorkbenchNodeStatus { + status: DashboardStatus; + title: string; + summary: string; + facts: WorkbenchFact[]; +} + +export interface WorkbenchSetupStep { + command: string; + label: string; + state: "available" | "expected"; + detail: string; +} + +export interface WorkbenchSnapshot { + source: WorkbenchSource; + generatedAt: string; + controlPlane: ControlPlaneProbe; + node: WorkbenchNodeStatus; + setupSteps: WorkbenchSetupStep[]; + sections: Record; + loadIssues: string[]; + raw: { + dashboard: DashboardData; + devnetState: unknown | null; + devnetDashboardState: unknown | null; + controlPlaneHealth: unknown | null; + controlPlaneState: unknown | null; + }; +} + +type UnknownRecord = Record; + +export const WORKBENCH_SECTIONS: WorkbenchSectionDefinition[] = [ + { + key: "blocks", + label: "Blocks", + detail: "Private/local chain blocks, state roots, parent hashes, and receipt counts.", + expectedEndpoint: "GET /blocks", + }, + { + key: "transactions", + label: "Transactions", + detail: "Smoke-flow transaction ids and receipt application status.", + expectedEndpoint: "GET /transactions", + }, + { + key: "rootfields", + label: "Rootfields", + detail: "Rootfield namespaces, owners, compact roots, schema hashes, and active state.", + expectedEndpoint: "GET /rootfields", + }, + { + key: "agents", + label: "Agents", + detail: "Operators, workers, verifier identities, and observed contract actors.", + expectedEndpoint: "GET /agents", + }, + { + key: "models", + label: "Models", + detail: "ModelPassport objects when the private testnet runtime exports them.", + expectedEndpoint: "GET /models", + }, + { + key: "receipts", + label: "Work Receipts", + detail: "Work receipts from the launch fixture and local devnet handoff.", + expectedEndpoint: "GET /receipts", + }, + { + key: "memoryCells", + label: "Memory Cells", + detail: "Native MemoryCell records or rootfield-bundle projections while the API is pending.", + expectedEndpoint: "GET /memory-cells", + }, + { + key: "artifacts", + label: "Artifacts", + detail: "Artifact availability commitments and receipt-linked artifact URIs.", + expectedEndpoint: "GET /artifacts", + }, + { + key: "verifierModules", + label: "Verifier Modules", + detail: "Verifier module identities or derived module projections from local reports.", + expectedEndpoint: "GET /verifier-modules", + }, + { + key: "verifierReports", + label: "Verifier Reports", + detail: "Verifier reports, report digests, policies, checks, and reason codes.", + expectedEndpoint: "GET /verifier-reports", + }, + { + key: "challenges", + label: "Challenges", + detail: "Challenge lifecycle objects once the runtime/control-plane exports them.", + expectedEndpoint: "GET /challenges", + }, + { + key: "finality", + label: "Finality", + detail: "Local finality distance, anchor placeholders, and latest finalized state.", + expectedEndpoint: "GET /finality", + }, + { + key: "provenance", + label: "Provenance / Source", + detail: "Source paths, API probe result, and fixture fallback boundary.", + expectedEndpoint: "GET /raw", + }, + { + key: "hardwareSignals", + label: "Hardware Signals", + detail: "FlowRouter, gateway, and low-bandwidth sidecar heartbeat/control-signal records.", + expectedEndpoint: "GET /hardware-signals", + }, + { + key: "rawJson", + label: "Raw JSON", + detail: "Loaded dashboard, devnet, and control-plane payloads for direct inspection.", + expectedEndpoint: "GET /raw", + }, +]; + +function isRecord(value: unknown): value is UnknownRecord { + return value !== null && typeof value === "object" && !Array.isArray(value); +} + +function recordValues(value: unknown): UnknownRecord[] { + if (Array.isArray(value)) { + return value.filter(isRecord); + } + + if (isRecord(value)) { + return Object.values(value).filter(isRecord); + } + + return []; +} + +function collectionFrom(root: unknown, keys: string[]): UnknownRecord[] { + if (!isRecord(root)) { + return []; + } + + for (const key of keys) { + const values = recordValues(root[key]); + if (values.length > 0) { + return values; + } + } + + return []; +} + +function text(value: unknown, fallback = "not recorded"): string { + if (value === null || value === undefined || value === "") { + return fallback; + } + + return String(value); +} + +function numberValue(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + + if (typeof value === "string") { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + + return null; +} + +function stringArray(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + + return value.map((item) => text(item)).filter((item) => item !== "not recorded"); +} + +function statusFrom(value: unknown, fallback: DashboardStatus = "observed"): DashboardStatus { + const normalized = text(value, fallback).toLowerCase(); + if (normalized === "applied" || normalized === "success" || normalized === "active") { + return "verified"; + } + if (normalized === "finalized") { + return "finalized"; + } + if (normalized === "failed" || normalized === "invalid" || normalized === "reverted") { + return "failed"; + } + if (normalized === "pending" || normalized === "local-placeholder") { + return "pending"; + } + if (normalized === "stale" || normalized === "not-detected") { + return "stale"; + } + if (normalized === "unsupported") { + return "unsupported"; + } + if (normalized === "reorged") { + return "reorged"; + } + if (normalized === "unresolved") { + return "unresolved"; + } + if (normalized === "offline") { + return "offline"; + } + if (normalized === "verified") { + return "verified"; + } + + return fallback; +} + +function fixtureProvenance(subsystem: SourceSubsystem, fixturePath: string): Provenance { + return { + subsystem, + origin: "fixture", + chainContext: FIXTURE_CHAIN_CONTEXT, + fixturePath, + }; +} + +function localProvenance(subsystem: SourceSubsystem, localPathHint: string, capturedAt?: string): Provenance { + return { + subsystem, + origin: "local", + chainContext: FIXTURE_CHAIN_CONTEXT, + localPathHint, + capturedAt, + }; +} + +function makeRecord( + subsystem: SourceSubsystem, + fixturePath: string, + record: Omit, +): WorkbenchRecord { + return { + ...record, + provenance: fixtureProvenance(subsystem, fixturePath), + }; +} + +function makeLocalRecord( + subsystem: SourceSubsystem, + localPathHint: string, + record: Omit, + capturedAt?: string, +): WorkbenchRecord { + return { + ...record, + provenance: localProvenance(subsystem, localPathHint, capturedAt), + }; +} + +function relabelDevnetRecordsAsControlPlane( + sections: Record, + controlPlane: ControlPlaneProbe, +): Record { + return Object.fromEntries( + Object.entries(sections).map(([key, records]) => [ + key, + records.map((record) => { + const fromFallbackFile = + record.provenance.fixturePath === WORKBENCH_DEVNET_STATE_PATH || + record.provenance.fixturePath === WORKBENCH_DEVNET_DASHBOARD_STATE_PATH; + + if (!fromFallbackFile) { + return record; + } + + return { + ...record, + provenance: localProvenance(record.provenance.subsystem, controlPlane.url, controlPlane.checkedAt), + }; + }), + ]), + ) as Record; +} + +function latestBlockFromDevnet(devnetState: unknown): UnknownRecord | null { + const blocks = collectionFrom(devnetState, ["blocks"]); + return blocks.length > 0 ? blocks[blocks.length - 1] : null; +} + +function extractControlPlaneState(state: unknown): unknown { + if (isRecord(state) && isRecord(state.state)) { + return state.state; + } + + return state; +} + +function getControlPlaneUrl(): string { + const env = (import.meta as ImportMeta & { env?: Record }).env; + const configured = env?.VITE_FLOWCHAIN_CONTROL_PLANE_URL?.trim(); + return configured && configured.length > 0 ? configured.replace(/\/+$/, "") : DEFAULT_CONTROL_PLANE_URL; +} + +async function fetchJsonWithTimeout(url: string, timeoutMs: number): Promise { + const controller = new AbortController(); + const timeout = globalThis.setTimeout(() => controller.abort(), timeoutMs); + + try { + const response = await fetch(url, { cache: "no-store", signal: controller.signal }); + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText}`.trim()); + } + return response.json(); + } finally { + globalThis.clearTimeout(timeout); + } +} + +async function fetchOptionalJson(path: string): Promise<{ value: unknown | null; error?: string }> { + try { + return { value: await fetchJsonWithTimeout(path, CONTROL_PLANE_TIMEOUT_MS) }; + } catch (error) { + return { + value: null, + error: error instanceof Error ? error.message : "unknown load error", + }; + } +} + +async function probeControlPlane(): Promise { + const url = getControlPlaneUrl(); + const checkedAt = new Date().toISOString(); + const endpoints = ["GET /health", "GET /state"]; + + try { + const health = await fetchJsonWithTimeout(`${url}/health`, CONTROL_PLANE_TIMEOUT_MS); + let state: unknown | undefined; + + try { + state = await fetchJsonWithTimeout(`${url}/state`, CONTROL_PLANE_TIMEOUT_MS); + } catch (error) { + return { + url, + status: "available", + checkedAt, + endpoints, + health, + error: `Health endpoint responded, but state endpoint was not loaded: ${ + error instanceof Error ? error.message : "unknown state error" + }`, + }; + } + + return { + url, + status: "available", + checkedAt, + endpoints, + health, + state, + }; + } catch (error) { + return { + url, + status: "not-detected", + checkedAt, + endpoints, + error: error instanceof Error ? error.message : "control-plane API not detected", + }; + } +} + +function buildBlockRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const blocks = collectionFrom(devnetState, ["blocks"]); + + if (blocks.length > 0) { + return blocks + .map((block) => { + const receipts = recordValues(block.receipts); + const txIds = stringArray(block.txIds); + const failedReceipt = receipts.some((receipt) => statusFrom(receipt.status) === "failed"); + const blockNumber = text(block.blockNumber); + + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: text(block.blockHash, `block:${blockNumber}`), + kind: "Block", + title: `Block ${blockNumber}`, + summary: `${txIds.length} transactions, ${receipts.length} receipts, state root ${text(block.stateRoot)}`, + status: failedReceipt ? "failed" : "finalized", + facts: [ + { label: "block hash", value: text(block.blockHash) }, + { label: "parent hash", value: text(block.parentHash) }, + { label: "state root", value: text(block.stateRoot) }, + { label: "logical time", value: text(block.logicalTime) }, + { label: "transactions", value: txIds.length.toString() }, + { label: "receipts", value: receipts.length.toString() }, + ], + raw: block, + }); + }) + .sort((left, right) => Number(right.title.replace("Block ", "")) - Number(left.title.replace("Block ", ""))); + } + + return data.devnetBlocks.map((block) => + makeRecord("devnet", data.metadata.fixturePath, { + id: block.blockHash, + kind: "Block", + title: `Block ${block.blockNumber}`, + summary: `${block.observationCount} observations and ${block.reportCount} reports in the dashboard fixture window.`, + status: block.status, + facts: [ + { label: "block hash", value: block.blockHash }, + { label: "parent hash", value: block.parentHash }, + { label: "state root", value: block.stateRoot }, + { label: "receipts root", value: block.receiptsRoot }, + { label: "finality distance", value: block.finalityDistance.toString() }, + ], + raw: block, + }), + ); +} + +function buildTransactionRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const blocks = collectionFrom(devnetState, ["blocks"]); + const records: WorkbenchRecord[] = []; + + for (const block of blocks) { + const txIds = stringArray(block.txIds); + const receipts = recordValues(block.receipts); + const blockNumber = text(block.blockNumber); + + txIds.forEach((txId, index) => { + const receipt = receipts.find((candidate) => text(candidate.txId) === txId) ?? receipts[index]; + const receiptStatus = statusFrom(receipt?.status, "pending"); + records.push( + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: txId, + kind: "Transaction", + title: txId, + summary: receipt ? `Receipt ${text(receipt.status)} in block ${blockNumber}.` : `Pending transaction in block ${blockNumber}.`, + status: receiptStatus === "verified" ? "finalized" : receiptStatus, + facts: [ + { label: "block", value: blockNumber }, + { label: "receipt status", value: text(receipt?.status, "pending") }, + { label: "error", value: text(receipt?.error, "none") }, + { label: "block hash", value: text(block.blockHash) }, + ], + raw: { txId, receipt, block }, + }), + ); + }); + } + + if (records.length > 0) { + return records; + } + + return data.flowPulseObservations.map((observation) => + makeRecord("indexer", data.metadata.fixturePath, { + id: `${observation.txHash}:${observation.logIndex}`, + kind: "Receipt-derived transaction", + title: observation.txHash, + summary: observation.summary, + status: statusFrom(observation.receiptStatus, observation.status), + facts: [ + { label: "block", value: observation.blockNumber }, + { label: "log index", value: observation.logIndex }, + { label: "pulse type", value: observation.pulseType }, + { label: "rootfield", value: observation.rootfieldId }, + ], + raw: observation, + }), + ); +} + +function buildRootfieldRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const devnetRootfields = collectionFrom(devnetState, ["rootfields", "rootfieldState", "rootfieldsById"]).map((rootfield, index) => { + const id = text(rootfield.rootfieldId ?? rootfield.id, `rootfield:${index + 1}`); + const isActive = rootfield.active === true; + + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Local Rootfield", + title: id, + summary: isActive + ? `Active local namespace owned by ${text(rootfield.owner)}.` + : `Local namespace exported by the private testnet state.`, + status: rootfield.active === false ? "stale" : statusFrom(rootfield.status, isActive ? "verified" : "observed"), + facts: [ + { label: "owner", value: text(rootfield.owner) }, + { label: "latest root", value: text(rootfield.latestRoot) }, + { label: "schema hash", value: text(rootfield.schemaHash) }, + { label: "metadata hash", value: text(rootfield.metadataHash) }, + { label: "pulse count", value: text(rootfield.pulseCount) }, + { label: "root count", value: text(rootfield.rootCount) }, + ], + raw: rootfield, + }); + }); + + const dashboardRootfields = data.rootfields.map((rootfield) => + makeRecord("devnet", data.metadata.fixturePath, { + id: rootfield.rootfieldId, + kind: "Dashboard Rootfield", + title: rootfield.rootfieldId, + summary: `Dashboard fixture namespace with latest root ${rootfield.latestRoot}.`, + status: rootfield.status, + facts: [ + { label: "owner", value: rootfield.owner }, + { label: "latest root", value: rootfield.latestRoot }, + { label: "schema hash", value: rootfield.schemaHash }, + { label: "metadata hash", value: rootfield.metadataHash }, + { label: "pulse count", value: rootfield.pulseCount.toString() }, + { label: "work lanes", value: rootfield.workLaneIds.join(", ") || "none" }, + ], + raw: rootfield, + }), + ); + + return [...devnetRootfields, ...dashboardRootfields]; +} + +function buildAgentRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const agents = new Map; raw: unknown[] }>(); + + const addAgent = (id: unknown, role: string, raw: unknown) => { + const agentId = text(id, ""); + if (agentId.length === 0 || agentId === "not recorded") { + return; + } + const current = agents.get(agentId) ?? { roles: new Set(), raw: [] }; + current.roles.add(role); + current.raw.push(raw); + agents.set(agentId, current); + }; + + data.rootfields.forEach((rootfield) => addAgent(rootfield.owner, "rootfield owner", rootfield)); + data.workLanes.forEach((lane) => addAgent(lane.operator, "work-lane operator", lane)); + data.flowPulseObservations.forEach((observation) => addAgent(observation.actor, "FlowPulse actor", observation)); + + collectionFrom(devnetState, ["workReceipts"]).forEach((receipt) => addAgent(receipt.workerId, "worker", receipt)); + collectionFrom(devnetState, ["verifierReports"]).forEach((report) => addAgent(report.verifierId, "verifier", report)); + + return [...agents.entries()].map(([agentId, agent]) => + makeRecord("devnet", data.metadata.fixturePath, { + id: agentId, + kind: "Agent identity", + title: agentId, + summary: [...agent.roles].join(", "), + status: "verified", + facts: [ + { label: "roles", value: [...agent.roles].join(", ") }, + { label: "references", value: agent.raw.length.toString() }, + { label: "source", value: "fixture projection" }, + ], + raw: agent.raw, + }), + ); +} + +function buildModelRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["models", "modelPassports", "modelPassportsById"]).map((model, index) => { + const id = text(model.modelId ?? model.passportId ?? model.id, `model:${index + 1}`); + + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "ModelPassport", + title: id, + summary: text(model.summary ?? model.description ?? model.name, "Model passport exported by the control-plane/devnet state."), + status: statusFrom(model.status, "observed"), + facts: [ + { label: "publisher", value: text(model.publisher ?? model.owner ?? model.agentId) }, + { label: "model hash", value: text(model.modelHash ?? model.commitment ?? model.digest) }, + { label: "created", value: text(model.createdAt ?? model.registeredAt) }, + ], + raw: model, + }); + }); +} + +function buildReceiptRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const dashboardReceipts = data.workReceipts.map((receipt) => + makeRecord("worker", data.metadata.fixturePath, { + id: receipt.receiptId, + kind: "WorkReceipt", + title: receipt.receiptId, + summary: `${receipt.workType} for lane ${receipt.laneId}.`, + status: receipt.status, + facts: [ + { label: "lane", value: receipt.laneId }, + { label: "rootfield", value: receipt.rootfieldId }, + { label: "artifact", value: receipt.artifactUri }, + { label: "result hash", value: receipt.resultHash }, + { label: "report", value: text(receipt.reportId) }, + ], + raw: receipt, + }), + ); + + const devnetReceipts = collectionFrom(devnetState, ["workReceipts"]).map((receipt) => + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: text(receipt.receiptId), + kind: "Devnet WorkReceipt", + title: text(receipt.receiptId), + summary: `Worker ${text(receipt.workerId)} moved ${text(receipt.inputRoot)} to ${text(receipt.outputRoot)}.`, + status: "verified", + facts: [ + { label: "worker", value: text(receipt.workerId) }, + { label: "rootfield", value: text(receipt.rootfieldId) }, + { label: "input root", value: text(receipt.inputRoot) }, + { label: "output root", value: text(receipt.outputRoot) }, + { label: "artifact commitment", value: text(receipt.artifactCommitment) }, + ], + raw: receipt, + }), + ); + + return [...dashboardReceipts, ...devnetReceipts]; +} + +function buildMemoryCellRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const nativeCells = collectionFrom(devnetState, ["memoryCells", "memoryCellState", "cells"]); + if (nativeCells.length > 0) { + return nativeCells.map((cell, index) => { + const id = text(cell.cellId ?? cell.memoryCellId ?? cell.id, `memory-cell:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "MemoryCell", + title: id, + summary: text(cell.summary ?? cell.description, "Native MemoryCell exported by the local/private testnet state."), + status: statusFrom(cell.status, "observed"), + facts: [ + { label: "rootfield", value: text(cell.rootfieldId) }, + { label: "latest root", value: text(cell.latestRoot ?? cell.root) }, + { label: "receipt", value: text(cell.receiptId) }, + { label: "updated", value: text(cell.updatedAt) }, + ], + raw: cell, + }); + }); + } + + return data.rootfieldBundles.map((bundle) => + makeRecord("devnet", data.metadata.fixturePath, { + id: `memory-cell-projection:${bundle.rootfieldId}`, + kind: "Memory cell projection", + title: bundle.rootfieldId, + summary: "Derived from the existing RootfieldBundle until native MemoryCell objects are exported by the runtime API.", + status: bundle.status, + facts: [ + { label: "latest root", value: bundle.latestRoot }, + { label: "signals", value: bundle.memorySignalIds.length.toString() }, + { label: "receipts", value: bundle.memoryReceiptIds.length.toString() }, + { label: "transitions", value: bundle.transitionIds.length.toString() }, + { label: "latest transition", value: text(bundle.latestTransitionId) }, + ], + raw: bundle, + }), + ); +} + +function buildArtifactRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const artifacts = collectionFrom(devnetState, ["artifactCommitments", "artifacts", "artifactAvailabilityProofs"]).map((artifact) => + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: text(artifact.artifactId ?? artifact.id ?? artifact.proofId), + kind: "Artifact", + title: text(artifact.artifactId ?? artifact.id ?? artifact.proofId), + summary: text(artifact.uriHint ?? artifact.uri ?? artifact.evidenceURI, "Artifact commitment from local devnet state."), + status: statusFrom(artifact.status, "verified"), + facts: [ + { label: "rootfield", value: text(artifact.rootfieldId) }, + { label: "commitment", value: text(artifact.commitment ?? artifact.artifactCommitment) }, + { label: "uri", value: text(artifact.uriHint ?? artifact.uri ?? artifact.evidenceURI) }, + ], + raw: artifact, + }), + ); + + const receiptArtifacts = data.workReceipts.map((receipt) => + makeRecord("worker", data.metadata.fixturePath, { + id: `${receipt.receiptId}:artifact`, + kind: "Receipt artifact reference", + title: receipt.artifactUri, + summary: `Referenced by work receipt ${receipt.receiptId}.`, + status: receipt.status, + facts: [ + { label: "receipt", value: receipt.receiptId }, + { label: "result hash", value: receipt.resultHash }, + { label: "rootfield", value: receipt.rootfieldId }, + ], + raw: receipt, + }), + ); + + return [...artifacts, ...receiptArtifacts]; +} + +function buildVerifierModuleRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const nativeModules = collectionFrom(devnetState, [ + "verifierModules", + "verifierModuleRegistry", + "verifierModulesById", + "verifiers", + ]).map((module, index) => { + const id = text(module.moduleId ?? module.verifierModuleId ?? module.verifierId ?? module.id, `verifier-module:${index + 1}`); + + return makeRecord("verifier", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "VerifierModule", + title: id, + summary: text(module.summary ?? module.description, "Verifier module exported by the local control-plane/devnet state."), + status: statusFrom(module.status, "observed"), + facts: [ + { label: "owner", value: text(module.owner ?? module.operator ?? module.verifierId) }, + { label: "spec", value: text(module.specVersion ?? module.verifierSpecVersion ?? module.version) }, + { label: "policy", value: text(module.resolverPolicyId ?? module.policyId) }, + { label: "registered", value: text(module.registeredAt ?? module.createdAt) }, + ], + raw: module, + }); + }); + + if (nativeModules.length > 0) { + return nativeModules; + } + + const derived = new Map; raw: unknown[]; policy: string; spec: string }>(); + + for (const report of data.verifierReports) { + const key = `${report.resolverPolicyId}:${report.verifierSpecVersion}`; + const current = + derived.get(key) ?? + ({ + reports: 0, + statuses: new Set(), + raw: [], + policy: report.resolverPolicyId, + spec: report.verifierSpecVersion, + } satisfies { reports: number; statuses: Set; raw: unknown[]; policy: string; spec: string }); + current.reports += 1; + current.statuses.add(report.status); + current.raw.push(report); + derived.set(key, current); + } + + for (const report of collectionFrom(devnetState, ["verifierReports"])) { + const verifierId = text(report.verifierId, "verifier:local"); + const key = `${verifierId}:${text(report.rootfieldId)}`; + const current = + derived.get(key) ?? + ({ + reports: 0, + statuses: new Set(), + raw: [], + policy: text(report.rootfieldId), + spec: "local-devnet", + } satisfies { reports: number; statuses: Set; raw: unknown[]; policy: string; spec: string }); + current.reports += 1; + current.statuses.add(statusFrom(report.status, "observed")); + current.raw.push(report); + derived.set(key, current); + } + + return [...derived.entries()].map(([id, module]) => + makeRecord("verifier", data.metadata.fixturePath, { + id, + kind: "Verifier module projection", + title: id, + summary: `Derived from ${module.reports} verifier report(s) until explicit VerifierModule objects are exported.`, + status: module.statuses.has("failed") + ? "failed" + : module.statuses.has("unresolved") + ? "unresolved" + : module.statuses.has("unsupported") + ? "unsupported" + : "verified", + facts: [ + { label: "reports", value: module.reports.toString() }, + { label: "policy", value: module.policy }, + { label: "spec", value: module.spec }, + { label: "statuses", value: [...module.statuses].join(", ") || "none" }, + ], + raw: module.raw, + }), + ); +} + +function buildVerifierRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const dashboardReports = data.verifierReports.map((report) => + makeRecord("verifier", data.metadata.fixturePath, { + id: report.reportId, + kind: "VerifierReport", + title: report.reportId, + summary: `${report.checksPassed}/${report.checksTotal} checks passed; ${ + report.reasonCodes.join(", ") || "no reason codes" + }.`, + status: report.status, + facts: [ + { label: "rootfield", value: report.rootfieldId }, + { label: "observation", value: report.observationId }, + { label: "policy", value: report.resolverPolicyId }, + { label: "report hash", value: report.reportHash }, + ], + raw: report, + }), + ); + + const devnetReports = collectionFrom(devnetState, ["verifierReports"]).map((report) => + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: text(report.reportId), + kind: "Devnet VerifierReport", + title: text(report.reportId), + summary: `Verifier ${text(report.verifierId)} reported ${text(report.status)} for ${text(report.receiptId)}.`, + status: statusFrom(report.status, "observed"), + facts: [ + { label: "verifier", value: text(report.verifierId) }, + { label: "receipt", value: text(report.receiptId) }, + { label: "rootfield", value: text(report.rootfieldId) }, + { label: "digest", value: text(report.reportDigest) }, + { label: "reasons", value: stringArray(report.reasonCodes).join(", ") || "none" }, + ], + raw: report, + }), + ); + + return [...dashboardReports, ...devnetReports]; +} + +function buildChallengeRecords(devnetState: unknown): WorkbenchRecord[] { + return collectionFrom(devnetState, ["challenges", "challengeState", "openChallenges"]).map((challenge, index) => { + const id = text(challenge.challengeId ?? challenge.id, `challenge:${index + 1}`); + return makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Challenge", + title: id, + summary: text(challenge.summary ?? challenge.reason, "Challenge exported by local/private testnet state."), + status: statusFrom(challenge.status, "pending"), + facts: [ + { label: "receipt", value: text(challenge.receiptId) }, + { label: "report", value: text(challenge.reportId) }, + { label: "opened by", value: text(challenge.openedBy ?? challenge.challenger) }, + { label: "resolved at", value: text(challenge.resolvedAt) }, + ], + raw: challenge, + }); + }); +} + +function buildFinalityRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const records: WorkbenchRecord[] = [ + makeRecord("devnet", data.metadata.fixturePath, { + id: "dashboard-finality-window", + kind: "Finality window", + title: `${data.chain.finalizedBlock} finalized`, + summary: `Dashboard fixture head is ${data.chain.currentBlock}; finalized through ${data.chain.finalizedBlock}.`, + status: data.chain.currentBlock > data.chain.finalizedBlock ? "pending" : "finalized", + facts: [ + { label: "chain", value: data.chain.chainId }, + { label: "head", value: data.chain.currentBlock.toString() }, + { label: "finalized", value: data.chain.finalizedBlock.toString() }, + { label: "settlement context", value: data.chain.settlementContext }, + ], + raw: data.chain, + }), + ]; + + collectionFrom(devnetState, ["baseAnchors"]).forEach((anchor) => { + records.push( + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: text(anchor.anchorId), + kind: "Local anchor", + title: text(anchor.anchorId), + summary: `Blocks ${text(anchor.blockRangeStart)}-${text(anchor.blockRangeEnd)} are marked ${text(anchor.finalityStatus)}.`, + status: statusFrom(anchor.finalityStatus, "pending"), + facts: [ + { label: "appchain", value: text(anchor.appchainChainId) }, + { label: "state root", value: text(anchor.stateRoot) }, + { label: "work receipt root", value: text(anchor.workReceiptRoot) }, + { label: "verifier root", value: text(anchor.verifierReportRoot) }, + { label: "previous anchor", value: text(anchor.previousAnchorId) }, + ], + raw: anchor, + }), + ); + }); + + return records; +} + +function buildHardwareSignalRecords(data: DashboardData, devnetState: unknown): WorkbenchRecord[] { + const nativeSignals = collectionFrom(devnetState, [ + "hardwareSignals", + "hardwareHeartbeats", + "heartbeats", + "signals", + "loraSignals", + "meshtasticSignals", + ]).map((signal, index) => { + const id = text(signal.signalId ?? signal.heartbeatId ?? signal.nodeId ?? signal.id, `hardware-signal:${index + 1}`); + + return makeRecord("hardware", WORKBENCH_DEVNET_STATE_PATH, { + id, + kind: "Hardware signal", + title: id, + summary: text(signal.summary ?? signal.message, "Low-bandwidth hardware/control signal from local state."), + status: statusFrom(signal.status, "observed"), + facts: [ + { label: "node", value: text(signal.nodeId ?? signal.callsign) }, + { label: "transport", value: text(signal.transport ?? signal.radio ?? signal.channel) }, + { label: "received", value: text(signal.receivedAt ?? signal.lastHeartbeatAt ?? signal.timestamp) }, + { label: "rssi", value: text(signal.signalDbm ?? signal.rssiDbm) }, + ], + raw: signal, + }); + }); + + const dashboardSignals = data.hardwareNodes.map((node) => + makeRecord("hardware", data.metadata.fixturePath, { + id: node.nodeId, + kind: "Hardware heartbeat", + title: node.callsign, + summary: `${node.role} over ${node.transport}; ${node.locationHint}.`, + status: node.status, + facts: [ + { label: "node id", value: node.nodeId }, + { label: "firmware", value: node.firmware }, + { label: "transport", value: node.transport }, + { label: "heartbeat", value: text(node.lastHeartbeatAt) }, + { label: "battery", value: node.batteryPercent === undefined ? "not recorded" : `${node.batteryPercent}%` }, + { label: "signal", value: node.signalDbm === undefined ? "not recorded" : `${node.signalDbm} dBm` }, + ], + raw: node, + }), + ); + + return [...nativeSignals, ...dashboardSignals]; +} + +function topLevelKeys(value: unknown): string { + return isRecord(value) ? Object.keys(value).sort().join(", ") : "not loaded"; +} + +function buildRawJsonRecords( + data: DashboardData, + controlPlane: ControlPlaneProbe, + devnetState: unknown | null, + devnetDashboardState: unknown | null, +): WorkbenchRecord[] { + return [ + makeRecord("indexer", data.metadata.fixturePath, { + id: "raw-dashboard-fixture", + kind: "Raw JSON", + title: data.metadata.runtimeDataPath, + summary: "Primary dashboard runtime fixture loaded by the app.", + status: "verified", + facts: [ + { label: "schema", value: data.metadata.schema }, + { label: "mode", value: data.metadata.mode }, + { label: "keys", value: topLevelKeys(data) }, + ], + raw: data, + }), + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: "raw-devnet-state", + kind: "Raw JSON", + title: WORKBENCH_DEVNET_STATE_PATH, + summary: devnetState ? "Launch-core local devnet state loaded." : "Launch-core local devnet state was not loaded.", + status: devnetState ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(devnetState) ? text(devnetState.schema) : "missing" }, + { label: "keys", value: topLevelKeys(devnetState) }, + ], + raw: devnetState, + }), + makeRecord("devnet", WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, { + id: "raw-devnet-dashboard-state", + kind: "Raw JSON", + title: WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, + summary: devnetDashboardState + ? "Launch-core devnet dashboard projection loaded." + : "Launch-core devnet dashboard projection was not loaded.", + status: devnetDashboardState ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(devnetDashboardState) ? text(devnetDashboardState.schema) : "missing" }, + { label: "keys", value: topLevelKeys(devnetDashboardState) }, + ], + raw: devnetDashboardState, + }), + makeLocalRecord( + "indexer", + controlPlane.url, + { + id: "raw-control-plane", + kind: "Raw JSON", + title: controlPlane.url, + summary: + controlPlane.status === "available" + ? "Control-plane health/state payloads loaded or partially loaded." + : "Control-plane API not detected; no local API JSON was loaded.", + status: controlPlane.status === "available" ? "verified" : "offline", + facts: [ + { label: "status", value: controlPlane.status }, + { label: "health keys", value: topLevelKeys(controlPlane.health) }, + { label: "state keys", value: topLevelKeys(controlPlane.state) }, + { label: "error", value: text(controlPlane.error, "none") }, + ], + raw: { + health: controlPlane.health ?? null, + state: controlPlane.state ?? null, + error: controlPlane.error ?? null, + }, + }, + controlPlane.checkedAt, + ), + ]; +} + +function buildProvenanceRecords( + data: DashboardData, + controlPlane: ControlPlaneProbe, + devnetState: unknown | null, + devnetDashboardState: unknown | null, +): WorkbenchRecord[] { + return [ + makeLocalRecord( + "indexer", + controlPlane.url, + { + id: "control-plane-api", + kind: "Control-plane integration", + title: controlPlane.url, + summary: + controlPlane.status === "available" + ? "Control-plane health endpoint responded; state endpoint is used when present." + : "Control-plane API was not detected; the workbench is rendering deterministic fixture fallback.", + status: controlPlane.status === "available" ? "verified" : "offline", + facts: [ + { label: "status", value: controlPlane.status }, + { label: "checked", value: controlPlane.checkedAt }, + { label: "endpoints", value: controlPlane.endpoints.join(", ") }, + { label: "error", value: text(controlPlane.error, "none") }, + ], + raw: controlPlane, + }, + controlPlane.checkedAt, + ), + makeRecord("indexer", data.metadata.fixturePath, { + id: "dashboard-fixture", + kind: "Dashboard fixture", + title: data.metadata.fixturePath, + summary: data.metadata.description, + status: "verified", + facts: [ + { label: "runtime copy", value: data.metadata.runtimeDataPath }, + { label: "generated", value: data.metadata.generatedAt }, + { label: "mode", value: data.metadata.mode }, + ], + raw: data.metadata, + }), + makeRecord("devnet", WORKBENCH_DEVNET_STATE_PATH, { + id: "devnet-state-fixture", + kind: "Devnet state fixture", + title: WORKBENCH_DEVNET_STATE_PATH, + summary: devnetState ? "Existing launch-core devnet state loaded into the workbench." : "Devnet state fixture was not loaded.", + status: devnetState ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(devnetState) ? text(devnetState.schema) : "missing" }, + { label: "source", value: "fixtures/launch-core/generated/devnet/state.json" }, + ], + raw: devnetState, + }), + makeRecord("devnet", WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, { + id: "devnet-dashboard-state-fixture", + kind: "Devnet dashboard-state fixture", + title: WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, + summary: devnetDashboardState + ? "Existing devnet dashboard-state fixture loaded for raw/provenance inspection." + : "Devnet dashboard-state fixture was not loaded.", + status: devnetDashboardState ? "verified" : "unresolved", + facts: [ + { label: "schema", value: isRecord(devnetDashboardState) ? text(devnetDashboardState.schema) : "missing" }, + { label: "source", value: "fixtures/launch-core/generated/devnet/dashboard-state.json" }, + ], + raw: devnetDashboardState, + }), + ]; +} + +function buildNodeStatus(data: DashboardData, devnetState: unknown, controlPlane: ControlPlaneProbe): WorkbenchNodeStatus { + const latestBlock = latestBlockFromDevnet(devnetState); + const devnet = isRecord(devnetState) ? devnetState : {}; + const nextBlockNumber = numberValue(devnet.nextBlockNumber); + const blockHeight = nextBlockNumber !== null ? Math.max(0, nextBlockNumber - 1) : data.chain.currentBlock; + const stateRoot = text(latestBlock?.stateRoot ?? devnet.stateRoot ?? data.devnetBlocks[0]?.stateRoot); + const pendingTxs = recordValues(devnet.pendingTxs).length; + const status: DashboardStatus = controlPlane.status === "available" ? "verified" : "offline"; + + return { + status, + title: controlPlane.status === "available" ? "Control-plane detected" : "Control-plane offline", + summary: + controlPlane.status === "available" + ? "The local API health endpoint responded. The workbench will use API state when the state endpoint is available." + : "No local API responded at the configured URL, so this screen is rendering committed deterministic fixtures.", + facts: [ + { label: "chain id", value: text(devnet.chainId ?? data.chain.chainId) }, + { label: "block height", value: blockHeight.toString() }, + { label: "genesis hash", value: text(devnet.genesisHash) }, + { label: "parent/head hash", value: text(devnet.parentHash ?? latestBlock?.blockHash) }, + { label: "state root", value: stateRoot }, + { label: "pending txs", value: pendingTxs.toString() }, + { label: "api url", value: controlPlane.url }, + ], + }; +} + +function buildSetupSteps(controlPlane: ControlPlaneProbe): WorkbenchSetupStep[] { + return [ + { + command: "npm install", + label: "Install workspace dependencies", + state: "available", + detail: "Required before running launch, service, or dashboard commands on a clean machine.", + }, + { + command: "npm run launch:candidate", + label: "Refresh V0 fixtures", + state: "available", + detail: "Current root command validates launch-core data before the workbench consumes it.", + }, + { + command: "npm run flowchain:start", + label: "Start private testnet services", + state: "expected", + detail: "Integration point for the runtime/control-plane package; not provided by this dashboard change.", + }, + { + command: "npm run flowchain:smoke", + label: "Run full object smoke flow", + state: "expected", + detail: "Expected to populate agents, models, receipts, artifacts, challenges, finality, and API state.", + }, + { + command: "npm run dev --prefix apps/dashboard", + label: "Open the workbench", + state: "available", + detail: + controlPlane.status === "available" + ? "Runs the browser workbench against the detected local API and synced fixtures." + : "Runs the browser workbench with deterministic fixture fallback.", + }, + ]; +} + +export function buildWorkbenchSnapshot( + data: DashboardData, + options: { + controlPlane?: ControlPlaneProbe; + devnetState?: unknown | null; + devnetDashboardState?: unknown | null; + loadIssues?: string[]; + } = {}, +): WorkbenchSnapshot { + const controlPlane = + options.controlPlane ?? + ({ + url: DEFAULT_CONTROL_PLANE_URL, + status: "not-detected", + checkedAt: new Date().toISOString(), + endpoints: ["GET /health", "GET /state"], + error: "not probed", + } satisfies ControlPlaneProbe); + const controlPlaneState = extractControlPlaneState(controlPlane.state); + const activeDevnetState = controlPlaneState ?? options.devnetState ?? null; + const source: WorkbenchSource = controlPlane.status === "available" && controlPlaneState ? "control-plane" : "fixture-fallback"; + + const sections: Record = { + blocks: buildBlockRecords(data, activeDevnetState), + transactions: buildTransactionRecords(data, activeDevnetState), + rootfields: buildRootfieldRecords(data, activeDevnetState), + agents: buildAgentRecords(data, activeDevnetState), + models: buildModelRecords(activeDevnetState), + receipts: buildReceiptRecords(data, activeDevnetState), + memoryCells: buildMemoryCellRecords(data, activeDevnetState), + artifacts: buildArtifactRecords(data, activeDevnetState), + verifierModules: buildVerifierModuleRecords(data, activeDevnetState), + verifierReports: buildVerifierRecords(data, activeDevnetState), + challenges: buildChallengeRecords(activeDevnetState), + finality: buildFinalityRecords(data, activeDevnetState), + provenance: [], + hardwareSignals: buildHardwareSignalRecords(data, activeDevnetState), + rawJson: [], + }; + + sections.provenance = buildProvenanceRecords(data, controlPlane, options.devnetState ?? null, options.devnetDashboardState ?? null); + sections.rawJson = buildRawJsonRecords(data, controlPlane, options.devnetState ?? null, options.devnetDashboardState ?? null); + const displayedSections = source === "control-plane" ? relabelDevnetRecordsAsControlPlane(sections, controlPlane) : sections; + + return { + source, + generatedAt: new Date().toISOString(), + controlPlane, + node: buildNodeStatus(data, activeDevnetState, controlPlane), + setupSteps: buildSetupSteps(controlPlane), + sections: displayedSections, + loadIssues: options.loadIssues ?? [], + raw: { + dashboard: data, + devnetState: options.devnetState ?? null, + devnetDashboardState: options.devnetDashboardState ?? null, + controlPlaneHealth: controlPlane.health ?? null, + controlPlaneState: controlPlane.state ?? null, + }, + }; +} + +export async function fetchWorkbenchSnapshot(data: DashboardData): Promise { + const [controlPlane, devnetStateResult, devnetDashboardStateResult] = await Promise.all([ + probeControlPlane(), + fetchOptionalJson(WORKBENCH_DEVNET_STATE_PATH), + fetchOptionalJson(WORKBENCH_DEVNET_DASHBOARD_STATE_PATH), + ]); + const loadIssues = [devnetStateResult.error, devnetDashboardStateResult.error].filter( + (issue): issue is string => typeof issue === "string" && issue.length > 0, + ); + + return buildWorkbenchSnapshot(data, { + controlPlane, + devnetState: devnetStateResult.value, + devnetDashboardState: devnetDashboardStateResult.value, + loadIssues, + }); +} diff --git a/apps/dashboard/src/styles.css b/apps/dashboard/src/styles.css index baac96ed..06cb1b8d 100644 --- a/apps/dashboard/src/styles.css +++ b/apps/dashboard/src/styles.css @@ -698,6 +698,187 @@ dd { background: #f7f8f4; } +.workbench-command-center { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(360px, 0.92fr); + gap: 14px; +} + +.workbench-node-body { + display: grid; + gap: 16px; + padding-top: 14px; +} + +.workbench-node-body h3 { + margin: 0 0 7px; + font-size: 1.22rem; +} + +.workbench-node-body p, +.workbench-section-detail, +.boundary-copy p { + margin: 0; + color: #465047; + line-height: 1.48; +} + +.workbench-fact-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + margin: 0; +} + +.workbench-fact-grid div { + min-width: 0; + padding: 10px; + border: 1px solid var(--line); + border-radius: 7px; + background: #f7f8f4; +} + +.setup-step-list { + display: grid; + padding-top: 6px; +} + +.setup-step-list article { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 9px; + padding: 11px 0; + border-bottom: 1px solid var(--line); +} + +.setup-step-list article:last-child { + border-bottom: 0; +} + +.setup-step-list strong, +.setup-step-list code, +.setup-step-list small { + display: block; + overflow-wrap: anywhere; +} + +code { + width: fit-content; + max-width: 100%; + margin: 5px 0; + padding: 3px 6px; + color: #25332d; + border: 1px solid var(--line); + border-radius: 6px; + background: #eef1eb; + font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; + font-size: 0.78rem; +} + +.workbench-warning { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 9px; + align-items: start; + padding: 11px 12px; + color: #6b4a16; + border: 1px solid #d9c393; + border-radius: 8px; + background: #fbf6e8; +} + +.workbench-warning strong, +.workbench-warning span { + display: block; + overflow-wrap: anywhere; +} + +.workbench-layout { + display: grid; + grid-template-columns: 222px minmax(0, 1fr); + gap: 14px; + align-items: start; +} + +.workbench-switcher { + position: sticky; + top: 88px; + display: grid; + gap: 7px; +} + +.workbench-switch { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 10px; + align-items: center; + min-height: 38px; + padding: 8px 10px; + text-align: left; + transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; +} + +.workbench-switch.active, +.workbench-switch:hover { + border-color: #9fb4a8; + background: #eaf1eb; +} + +.workbench-switch span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workbench-switch strong { + font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace; + font-size: 0.78rem; +} + +.workbench-record-panel { + display: grid; + gap: 14px; +} + +.workbench-record-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.workbench-record { + display: grid; + gap: 11px; + min-width: 0; + padding: 13px; + border: 1px solid var(--line); + border-radius: 7px; + background: #f7f8f4; +} + +.workbench-record h3 { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; + font-size: 0.96rem; +} + +.workbench-record p { + margin: 0; + color: #465047; + line-height: 1.44; +} + +.workbench-boundary-panel { + display: grid; + gap: 12px; +} + +.boundary-copy { + display: grid; + gap: 9px; +} + .table-panel { min-width: 0; overflow: hidden; @@ -1031,7 +1212,9 @@ dd { .overview-grid, .rootfield-grid, .hardware-grid, - .lane-grid { + .lane-grid, + .workbench-command-center, + .workbench-record-grid { grid-template-columns: 1fr; } @@ -1052,6 +1235,15 @@ dd { border-left: 0; border-top: 1px solid var(--line); } + + .workbench-layout { + grid-template-columns: 1fr; + } + + .workbench-switcher { + position: static; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 860px) { @@ -1095,6 +1287,10 @@ dd { border-left: 0; border-top: 1px solid var(--line); } + + .workbench-fact-grid { + grid-template-columns: 1fr; + } } @media (max-width: 560px) { @@ -1103,7 +1299,8 @@ dd { .block-strip, .definition-grid, .lane-stats, - .record-facts { + .record-facts, + .workbench-switcher { grid-template-columns: 1fr; } diff --git a/apps/dashboard/src/test/dashboardData.test.ts b/apps/dashboard/src/test/dashboardData.test.ts index a7734198..3e8264d8 100644 --- a/apps/dashboard/src/test/dashboardData.test.ts +++ b/apps/dashboard/src/test/dashboardData.test.ts @@ -1,14 +1,33 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; import canaryFixture from "../../../../fixtures/dashboard/flowmemory-dashboard-base-canary-v0.json"; import fixture from "../../../../fixtures/dashboard/flowmemory-dashboard-v0.json"; +import devnetDashboardState from "../../../../fixtures/launch-core/generated/devnet/dashboard-state.json"; +import devnetState from "../../../../fixtures/launch-core/generated/devnet/state.json"; import { validateDashboardData } from "../data/loadDashboardData"; import { DASHBOARD_STATUSES } from "../data/status"; import { computeOverviewMetrics, searchRecords } from "../data/selectors"; import type { DashboardData, ProvenancedRecord } from "../data/types"; +import { + DEFAULT_CONTROL_PLANE_URL, + WORKBENCH_DEVNET_DASHBOARD_STATE_PATH, + WORKBENCH_DEVNET_STATE_PATH, + WORKBENCH_SECTIONS, + buildWorkbenchSnapshot, + fetchWorkbenchSnapshot, +} from "../data/workbench"; +import { WorkbenchView } from "../views/WorkbenchView"; describe("dashboard fixture", () => { const data = validateDashboardData(fixture) as DashboardData; const canaryData = validateDashboardData(canaryFixture) as DashboardData; + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); it("loads the V0 dashboard fixture shape", () => { expect(data.metadata.schema).toBe("flowmemory.dashboard.fixture.v0"); @@ -85,4 +104,102 @@ describe("dashboard fixture", () => { expect(metrics).toHaveLength(5); expect(matches.map((match) => match.status)).toContain("failed"); }); + + it("builds a FlowChain workbench from existing dashboard and devnet fixtures", () => { + const workbench = buildWorkbenchSnapshot(data, { + devnetState, + devnetDashboardState, + }); + + expect(workbench.source).toBe("fixture-fallback"); + expect(workbench.controlPlane.url).toBe(DEFAULT_CONTROL_PLANE_URL); + expect(workbench.sections.blocks).toHaveLength(2); + expect(workbench.sections.transactions).toHaveLength(6); + expect(workbench.sections.rootfields.length).toBeGreaterThan(0); + expect(workbench.sections.agents.length).toBeGreaterThan(0); + expect(workbench.sections.receipts.length).toBeGreaterThan(data.workReceipts.length); + expect(workbench.sections.memoryCells.length).toBeGreaterThan(0); + expect(workbench.sections.artifacts.length).toBeGreaterThan(0); + expect(workbench.sections.verifierModules.length).toBeGreaterThan(0); + expect(workbench.sections.hardwareSignals.length).toBeGreaterThan(0); + expect(workbench.sections.finality.length).toBeGreaterThan(1); + expect(workbench.sections.provenance.map((record) => record.id)).toContain("control-plane-api"); + expect(workbench.sections.rawJson.map((record) => record.id)).toContain("raw-dashboard-fixture"); + expect(workbench.sections.models).toEqual([]); + expect(workbench.sections.challenges).toEqual([]); + expect(workbench.node.status).toBe("offline"); + + for (const section of WORKBENCH_SECTIONS) { + expect(workbench.sections[section.key], `${section.key} should be a defined workbench view`).toBeDefined(); + } + }); + + it("switches workbench provenance to local when control-plane state is available", () => { + const workbench = buildWorkbenchSnapshot(data, { + controlPlane: { + url: "http://127.0.0.1:8787", + status: "available", + checkedAt: "2026-05-13T15:00:00.000Z", + endpoints: ["GET /health", "GET /state"], + health: { status: "ok" }, + state: devnetState, + }, + devnetState, + devnetDashboardState, + }); + + expect(workbench.source).toBe("control-plane"); + expect(workbench.node.status).toBe("verified"); + expect(workbench.sections.blocks[0].provenance.origin).toBe("local"); + expect(workbench.sections.blocks[0].provenance.localPathHint).toBe("http://127.0.0.1:8787"); + expect(workbench.sections.provenance.find((record) => record.id === "control-plane-api")?.status).toBe("verified"); + }); + + it("fetches control-plane state while keeping deterministic fixture payloads available", async () => { + const fetchMock = vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + + if (url.endsWith("/health")) { + return Response.json({ status: "ok" }); + } + if (url.endsWith("/state")) { + return Response.json({ state: devnetState }); + } + if (url === WORKBENCH_DEVNET_STATE_PATH) { + return Response.json(devnetState); + } + if (url === WORKBENCH_DEVNET_DASHBOARD_STATE_PATH) { + return Response.json(devnetDashboardState); + } + + return new Response("not found", { status: 404 }); + }); + globalThis.fetch = fetchMock as typeof fetch; + + const workbench = await fetchWorkbenchSnapshot(data); + + expect(workbench.source).toBe("control-plane"); + expect(workbench.raw.controlPlaneHealth).toEqual({ status: "ok" }); + expect(workbench.raw.controlPlaneState).toEqual({ state: devnetState }); + expect(workbench.raw.devnetState).toEqual(devnetState); + expect(workbench.loadIssues).toEqual([]); + expect(fetchMock).toHaveBeenCalledWith("http://127.0.0.1:8787/health", expect.any(Object)); + expect(fetchMock).toHaveBeenCalledWith(WORKBENCH_DEVNET_STATE_PATH, expect.any(Object)); + }); + + it("renders the critical workbench view labels from fixture fallback", () => { + const workbench = buildWorkbenchSnapshot(data, { + devnetState, + devnetDashboardState, + }); + const html = renderToStaticMarkup(createElement(WorkbenchView, { data, workbench })); + + expect(html).toContain("Local explorer workbench"); + expect(html).toContain("Node and API status"); + expect(html).toContain("Control-plane offline"); + expect(html).toContain("Rootfields"); + expect(html).toContain("Verifier Modules"); + expect(html).toContain("Hardware Signals"); + expect(html).toContain("Raw JSON"); + }); }); diff --git a/apps/dashboard/src/views/RawJsonInspectorView.tsx b/apps/dashboard/src/views/RawJsonInspectorView.tsx index 6a4f00ce..85ebb077 100644 --- a/apps/dashboard/src/views/RawJsonInspectorView.tsx +++ b/apps/dashboard/src/views/RawJsonInspectorView.tsx @@ -2,13 +2,20 @@ import { useMemo, useState } from "react"; import { Braces } from "lucide-react"; import { SectionHeader } from "../components/SectionHeader"; import type { DashboardData } from "../data/types"; +import type { WorkbenchSnapshot } from "../data/workbench"; const DATASET_LABELS = [ "all", + "workbench", "metadata", "chain", "flowPulseObservations", "rootfields", + "rootflowTransitions", + "memorySignals", + "memoryReceipts", + "rootfieldBundles", + "agentMemoryViews", "workLanes", "workReceipts", "verifierReports", @@ -19,13 +26,13 @@ const DATASET_LABELS = [ type DatasetKey = (typeof DATASET_LABELS)[number]; -export function RawJsonInspectorView({ data }: { data: DashboardData }) { +export function RawJsonInspectorView({ data, workbench }: { data: DashboardData; workbench: WorkbenchSnapshot }) { const [dataset, setDataset] = useState("all"); const rawJson = useMemo(() => { - const value = dataset === "all" ? data : data[dataset]; + const value = dataset === "all" ? data : dataset === "workbench" ? workbench : data[dataset]; return JSON.stringify(value, null, 2); - }, [data, dataset]); + }, [data, dataset, workbench]); return (
diff --git a/apps/dashboard/src/views/WorkbenchView.tsx b/apps/dashboard/src/views/WorkbenchView.tsx new file mode 100644 index 00000000..a05f4aa5 --- /dev/null +++ b/apps/dashboard/src/views/WorkbenchView.tsx @@ -0,0 +1,244 @@ +import { useMemo, useState } from "react"; +import { Activity, Database, Network, Search, Server, Terminal } from "lucide-react"; +import { EmptyState } from "../components/EmptyState"; +import { HashValue } from "../components/HashValue"; +import { ProvenanceLine } from "../components/ProvenanceLine"; +import { SectionHeader } from "../components/SectionHeader"; +import { StatusBadge } from "../components/StatusBadge"; +import type { DashboardData, DashboardStatus } from "../data/types"; +import { WORKBENCH_SECTIONS, type WorkbenchRecord, type WorkbenchSectionKey, type WorkbenchSnapshot } from "../data/workbench"; + +const DEFAULT_SECTION: WorkbenchSectionKey = "blocks"; + +function displayValue(value: string) { + if (value.startsWith("0x") && value.length > 18) { + return ; + } + + return value; +} + +function setupStatus(state: "available" | "expected"): DashboardStatus { + return state === "available" ? "verified" : "pending"; +} + +function recordMatches(record: WorkbenchRecord, query: string): boolean { + const normalized = query.trim().toLowerCase(); + if (normalized.length === 0) { + return true; + } + + return JSON.stringify(record).toLowerCase().includes(normalized); +} + +export function WorkbenchView({ data, workbench }: { data: DashboardData; workbench: WorkbenchSnapshot }) { + const [activeSection, setActiveSection] = useState(DEFAULT_SECTION); + const [query, setQuery] = useState(""); + const activeDefinition = WORKBENCH_SECTIONS.find((section) => section.key === activeSection) ?? WORKBENCH_SECTIONS[0]; + const activeRecords = workbench.sections[activeSection] ?? []; + const filteredRecords = useMemo( + () => activeRecords.filter((record) => recordMatches(record, query)), + [activeRecords, query], + ); + const sourceStatus: DashboardStatus = workbench.source === "control-plane" ? "verified" : "stale"; + + return ( +
+ +
+ ); +}