BlueprintGraph is a real, interactive AntV G6 canvas — nodes are clickable and route into /code/blueprint/step/:stepId, hover populates a live info panel. It looks entirely production-grade. Its data source is a static fixture:
const nodeId = evt?.target?.id
if (nodeId) {
const node = mockBlueprint.nodes.find(n => n.id === nodeId)
(frontend/src/components/code/BlueprintGraph.tsx:186-188)
imported from frontend/src/pages/Code/blueprintMockData.ts, and reachable from a routed page — App.tsx:96 maps /code/blueprint to CodeBlueprint.
Trigger: a user opens the Blueprint view expecting to see the workflow for their run.
Observed: a convincing graph of somebody else's pipeline, with statuses and results that correspond to nothing. Because the node statuses render as real badges and the info panel shows a result field (BlueprintGraph.tsx:196), there is no visual cue distinguishing this from live data.
Expected: either live blueprint data from GET /api/faros/blueprints (documented at README.md:283), or an unmissable "Demo data" banner.
README.md:80 lists "FAROS frontend console" under Not Yet Included, so mock-backed views are presumably a known and accepted state for this RC. That is fine — but it needs to be legible from inside the running app, not only from a line in the README that a user of the deployed console will never read. A <Badge variant="outline">Sample data</Badge> in the page header would cost one line and remove the entire class of "why does the blueprint not match my run" confusion.
Two implementation notes while this file is open:
Event handlers are untyped. (evt: any) at BlueprintGraph.tsx:186 (and the sibling handler above it) opts out of type checking on the G6 event object, then reaches through it with optional chaining — evt?.target?.id — which is exactly the defensive shape you write when you do not know what you are receiving. G6 v5 (package.json:18) exports event types; using them would make the ?. chain unnecessary and catch shape changes on the next upgrade instead of at runtime.
The init/destroy effect is keyed on a callback that changes.
useEffect(() => {
const timer = setTimeout(initGraph, 50)
return () => {
clearTimeout(timer)
if (graphRef.current) {
graphRef.current.destroy()
graphRef.current = null
}
}
}, [initGraph])
(frontend/src/components/code/BlueprintGraph.tsx:209-217)
with initGraph memoised on [navigate, onNodeClick] (:207). If a parent passes onNodeClick as an inline arrow — the common case — its identity changes on every parent render, so the entire graph is destroyed and rebuilt on each one: layout re-runs, zoom and pan reset, hover state drops. Wrapping onNodeClick in useCallback at the call site fixes the symptom; keeping the graph instance in a ref and pushing handler updates via graph.off/graph.on fixes the cause. The setTimeout(initGraph, 50) is also worth revisiting — a 50ms delay to wait for container layout is a race that a ResizeObserver would settle deterministically.
BlueprintGraphis a real, interactive AntV G6 canvas — nodes are clickable and route into/code/blueprint/step/:stepId, hover populates a live info panel. It looks entirely production-grade. Its data source is a static fixture:(
frontend/src/components/code/BlueprintGraph.tsx:186-188)imported from
frontend/src/pages/Code/blueprintMockData.ts, and reachable from a routed page —App.tsx:96maps/code/blueprinttoCodeBlueprint.Trigger: a user opens the Blueprint view expecting to see the workflow for their run.
Observed: a convincing graph of somebody else's pipeline, with statuses and results that correspond to nothing. Because the node statuses render as real badges and the info panel shows a
resultfield (BlueprintGraph.tsx:196), there is no visual cue distinguishing this from live data.Expected: either live blueprint data from
GET /api/faros/blueprints(documented atREADME.md:283), or an unmissable "Demo data" banner.README.md:80lists "FAROS frontend console" under Not Yet Included, so mock-backed views are presumably a known and accepted state for this RC. That is fine — but it needs to be legible from inside the running app, not only from a line in the README that a user of the deployed console will never read. A<Badge variant="outline">Sample data</Badge>in the page header would cost one line and remove the entire class of "why does the blueprint not match my run" confusion.Two implementation notes while this file is open:
Event handlers are untyped.
(evt: any)atBlueprintGraph.tsx:186(and the sibling handler above it) opts out of type checking on the G6 event object, then reaches through it with optional chaining —evt?.target?.id— which is exactly the defensive shape you write when you do not know what you are receiving. G6 v5 (package.json:18) exports event types; using them would make the?.chain unnecessary and catch shape changes on the next upgrade instead of at runtime.The init/destroy effect is keyed on a callback that changes.
(
frontend/src/components/code/BlueprintGraph.tsx:209-217)with
initGraphmemoised on[navigate, onNodeClick](:207). If a parent passesonNodeClickas an inline arrow — the common case — its identity changes on every parent render, so the entire graph is destroyed and rebuilt on each one: layout re-runs, zoom and pan reset, hover state drops. WrappingonNodeClickinuseCallbackat the call site fixes the symptom; keeping the graph instance in a ref and pushing handler updates viagraph.off/graph.onfixes the cause. ThesetTimeout(initGraph, 50)is also worth revisiting — a 50ms delay to wait for container layout is a race that aResizeObserverwould settle deterministically.