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)}