diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java index b3defe23..ed95514c 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java @@ -97,6 +97,7 @@ public class DetailService { p.getRootCauseType(), p.getRootCauseMessage(), p.getErrorHandlerType(), p.getCircuitBreakerState(), p.getFallbackTriggered(), + p.getFilterMatched(), p.getDuplicateMessage(), hasTrace ); for (ProcessorNode child : convertProcessors(p.getChildren())) { @@ -132,6 +133,7 @@ public class DetailService { p.rootCauseType(), p.rootCauseMessage(), p.errorHandlerType(), p.circuitBreakerState(), p.fallbackTriggered(), + null, null, // filterMatched, duplicateMessage (not in flat DB records) hasTrace )); } diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java index 850c7cf7..8c7d41da 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java @@ -35,6 +35,8 @@ public final class ProcessorNode { private final String errorHandlerType; private final String circuitBreakerState; private final Boolean fallbackTriggered; + private final Boolean filterMatched; + private final Boolean duplicateMessage; private final boolean hasTraceData; private final List children; @@ -50,6 +52,7 @@ public final class ProcessorNode { String rootCauseType, String rootCauseMessage, String errorHandlerType, String circuitBreakerState, Boolean fallbackTriggered, + Boolean filterMatched, Boolean duplicateMessage, boolean hasTraceData) { this.processorId = processorId; this.processorType = processorType; @@ -73,6 +76,8 @@ public final class ProcessorNode { this.errorHandlerType = errorHandlerType; this.circuitBreakerState = circuitBreakerState; this.fallbackTriggered = fallbackTriggered; + this.filterMatched = filterMatched; + this.duplicateMessage = duplicateMessage; this.hasTraceData = hasTraceData; this.children = new ArrayList<>(); } @@ -103,6 +108,8 @@ public final class ProcessorNode { public String getErrorHandlerType() { return errorHandlerType; } public String getCircuitBreakerState() { return circuitBreakerState; } public Boolean getFallbackTriggered() { return fallbackTriggered; } + public Boolean getFilterMatched() { return filterMatched; } + public Boolean getDuplicateMessage() { return duplicateMessage; } public boolean isHasTraceData() { return hasTraceData; } public List getChildren() { return List.copyOf(children); } } diff --git a/ui/src/components/ExecutionDiagram/types.ts b/ui/src/components/ExecutionDiagram/types.ts index 47db9b42..9404b88a 100644 --- a/ui/src/components/ExecutionDiagram/types.ts +++ b/ui/src/components/ExecutionDiagram/types.ts @@ -12,6 +12,10 @@ export interface NodeExecutionState { hasTraceData?: boolean; /** Runtime-resolved endpoint URI (for TO_DYNAMIC, etc.) */ resolvedEndpointUri?: string; + /** Filter processor: true if predicate matched, false if message was rejected */ + filterMatched?: boolean; + /** Idempotent consumer: true if duplicate message detected and children skipped */ + duplicateMessage?: boolean; } export interface IterationInfo { diff --git a/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts b/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts index 5fa029d8..a734499a 100644 --- a/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts +++ b/ui/src/components/ExecutionDiagram/useExecutionOverlay.ts @@ -61,6 +61,8 @@ function buildOverlay( subRouteFailed: subRouteFailed || undefined, hasTraceData: !!proc.hasTraceData, resolvedEndpointUri: proc.resolvedEndpointUri || undefined, + filterMatched: (proc as Record).filterMatched as boolean | undefined, + duplicateMessage: (proc as Record).duplicateMessage as boolean | undefined, }); // Recurse into children diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx index e6980d27..a34505d9 100644 --- a/ui/src/components/ProcessDiagram/CompoundNode.tsx +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -66,6 +66,12 @@ export function CompoundNode({ onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave, }; + // Gate state: filter rejected or idempotent duplicate → amber container + const ownState = node.id ? executionOverlay?.get(node.id) : undefined; + const isGated = ownState?.filterMatched === false || ownState?.duplicateMessage === true; + const GATE_COLOR = '#D97706'; // amber-600 + const effectiveColor = isGated ? GATE_COLOR : color; + // _TRY_BODY / _CB_MAIN: transparent wrapper — no header, no border, just layout if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') { return ( @@ -118,7 +124,8 @@ export function CompoundNode({ ); } - // Default compound rendering (DO_TRY, EIP_CHOICE, etc.) + // Default compound rendering (DO_TRY, EIP_CHOICE, EIP_FILTER, EIP_IDEMPOTENT_CONSUMER, etc.) + const containerFill = isGated ? '#FFFBEB' : 'white'; // amber-50 tint when gated return ( {/* Container body */} @@ -128,14 +135,14 @@ export function CompoundNode({ width={w} height={h} rx={CORNER_RADIUS} - fill="white" - stroke={color} - strokeWidth={1.5} + fill={containerFill} + stroke={effectiveColor} + strokeWidth={isGated ? 2 : 1.5} /> {/* Colored header bar */} - - + + {/* Header icon (left-aligned) */}