From 3d5d462de0862845c8ca47b12648dc924f6eac67 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:29:30 +0100 Subject: [PATCH] fix: ENDPOINT node execution state, badge position, and edge traversal - Synthesize COMPLETED state for ENDPOINT nodes when overlay is active (endpoints are route entry points, not in the processor execution tree) - Move status badge (check/error) inside the card (top-right, below top bar) to avoid collision with ConfigBadge (TRACE/TAP) badges - Include ENDPOINT nodes in edge traversal check so the edge from endpoint to first processor renders as green/traversed Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/ProcessDiagram/DiagramNode.tsx | 18 ++++----- .../ProcessDiagram/ProcessDiagram.tsx | 39 +++++++++++++++++-- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/ui/src/components/ProcessDiagram/DiagramNode.tsx b/ui/src/components/ProcessDiagram/DiagramNode.tsx index ec21587b..e4efe737 100644 --- a/ui/src/components/ProcessDiagram/DiagramNode.tsx +++ b/ui/src/components/ProcessDiagram/DiagramNode.tsx @@ -129,16 +129,16 @@ export function DiagramNode({ {/* Config badges */} {config && } - {/* Execution overlay: status badge at top-right */} + {/* Execution overlay: status badge inside card, top-right corner */} {isCompleted && ( <> - + ✓ @@ -147,13 +147,13 @@ export function DiagramNode({ )} {isFailed && ( <> - + ! diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx index f3cadaac..30fa88f1 100644 --- a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import type { ProcessDiagramProps } from './types'; import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import { useDiagramData } from './useDiagramData'; @@ -58,6 +58,7 @@ export function ProcessDiagram({ onIterationChange, }: ProcessDiagramProps) { const overlayActive = !!executionOverlay; + // Route stack for drill-down navigation const [routeStack, setRouteStack] = useState([routeId]); @@ -72,6 +73,19 @@ export function ProcessDiagram({ application, currentRouteId, direction, diagramLayout, ); + // Collect ENDPOINT node IDs — these are always "traversed" when overlay is active + // because the endpoint is the route entry point (not in the processor execution tree). + const endpointNodeIds = useMemo(() => { + const ids = new Set(); + if (!overlayActive || !sections.length) return ids; + for (const section of sections) { + for (const node of section.nodes) { + if (node.type === 'ENDPOINT' && node.id) ids.add(node.id); + } + } + return ids; + }, [overlayActive, sections]); + const zoom = useZoomPan(); const toolbar = useToolbarHover(); @@ -85,6 +99,23 @@ export function ProcessDiagram({ } }, [totalWidth, totalHeight, currentRouteId]); // eslint-disable-line react-hooks/exhaustive-deps + // Resolve execution state for a node. ENDPOINT nodes (the route's "from:") + // don't appear in the processor execution tree, but should be marked as + // COMPLETED when the route executed (i.e., overlay has any entries). + const getNodeExecutionState = useCallback( + (nodeId: string | undefined, nodeType: string | undefined) => { + if (!nodeId || !executionOverlay) return undefined; + const state = executionOverlay.get(nodeId); + if (state) return state; + // Synthesize COMPLETED for ENDPOINT nodes when overlay is active + if (nodeType === 'ENDPOINT' && executionOverlay.size > 0) { + return { status: 'COMPLETED' as const, durationMs: 0, hasTraceData: false }; + } + return undefined; + }, + [executionOverlay], + ); + const handleNodeClick = useCallback( (nodeId: string) => { onNodeSelect?.(nodeId); }, [onNodeSelect], @@ -229,8 +260,10 @@ export function ProcessDiagram({ {/* Main section top-level edges (not inside compounds) */} {mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => { + const sourceHasState = executionOverlay?.has(edge.sourceId) || endpointNodeIds.has(edge.sourceId); + const targetHasState = executionOverlay?.has(edge.targetId) || endpointNodeIds.has(edge.targetId); const isTraversed = executionOverlay - ? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId)) + ? (!!sourceHasState && !!targetHasState) : undefined; return ( @@ -268,7 +301,7 @@ export function ProcessDiagram({ isHovered={toolbar.hoveredNodeId === node.id} isSelected={selectedNodeId === node.id} config={node.id ? nodeConfigs?.get(node.id) : undefined} - executionState={executionOverlay?.get(node.id ?? '')} + executionState={getNodeExecutionState(node.id, node.type)} overlayActive={overlayActive} onClick={() => node.id && handleNodeClick(node.id)} onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}