feat: amber container for filter/idempotent gate state + red pulse on failed badge
When a filter processor rejects a message (filterMatched=false) or an idempotent consumer detects a duplicate (duplicateMessage=true), the compound container turns amber (header, border, body tint). Also adds red pulsing rings on the failed processor badge (same SMIL pattern as the teal hasTraceData pulse). Backend: ProcessorNode gains filterMatched/duplicateMessage fields, threaded from ProcessorExecution JSON path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,7 @@ public class DetailService {
|
|||||||
p.getRootCauseType(), p.getRootCauseMessage(),
|
p.getRootCauseType(), p.getRootCauseMessage(),
|
||||||
p.getErrorHandlerType(), p.getCircuitBreakerState(),
|
p.getErrorHandlerType(), p.getCircuitBreakerState(),
|
||||||
p.getFallbackTriggered(),
|
p.getFallbackTriggered(),
|
||||||
|
p.getFilterMatched(), p.getDuplicateMessage(),
|
||||||
hasTrace
|
hasTrace
|
||||||
);
|
);
|
||||||
for (ProcessorNode child : convertProcessors(p.getChildren())) {
|
for (ProcessorNode child : convertProcessors(p.getChildren())) {
|
||||||
@@ -132,6 +133,7 @@ public class DetailService {
|
|||||||
p.rootCauseType(), p.rootCauseMessage(),
|
p.rootCauseType(), p.rootCauseMessage(),
|
||||||
p.errorHandlerType(), p.circuitBreakerState(),
|
p.errorHandlerType(), p.circuitBreakerState(),
|
||||||
p.fallbackTriggered(),
|
p.fallbackTriggered(),
|
||||||
|
null, null, // filterMatched, duplicateMessage (not in flat DB records)
|
||||||
hasTrace
|
hasTrace
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ public final class ProcessorNode {
|
|||||||
private final String errorHandlerType;
|
private final String errorHandlerType;
|
||||||
private final String circuitBreakerState;
|
private final String circuitBreakerState;
|
||||||
private final Boolean fallbackTriggered;
|
private final Boolean fallbackTriggered;
|
||||||
|
private final Boolean filterMatched;
|
||||||
|
private final Boolean duplicateMessage;
|
||||||
private final boolean hasTraceData;
|
private final boolean hasTraceData;
|
||||||
private final List<ProcessorNode> children;
|
private final List<ProcessorNode> children;
|
||||||
|
|
||||||
@@ -50,6 +52,7 @@ public final class ProcessorNode {
|
|||||||
String rootCauseType, String rootCauseMessage,
|
String rootCauseType, String rootCauseMessage,
|
||||||
String errorHandlerType, String circuitBreakerState,
|
String errorHandlerType, String circuitBreakerState,
|
||||||
Boolean fallbackTriggered,
|
Boolean fallbackTriggered,
|
||||||
|
Boolean filterMatched, Boolean duplicateMessage,
|
||||||
boolean hasTraceData) {
|
boolean hasTraceData) {
|
||||||
this.processorId = processorId;
|
this.processorId = processorId;
|
||||||
this.processorType = processorType;
|
this.processorType = processorType;
|
||||||
@@ -73,6 +76,8 @@ public final class ProcessorNode {
|
|||||||
this.errorHandlerType = errorHandlerType;
|
this.errorHandlerType = errorHandlerType;
|
||||||
this.circuitBreakerState = circuitBreakerState;
|
this.circuitBreakerState = circuitBreakerState;
|
||||||
this.fallbackTriggered = fallbackTriggered;
|
this.fallbackTriggered = fallbackTriggered;
|
||||||
|
this.filterMatched = filterMatched;
|
||||||
|
this.duplicateMessage = duplicateMessage;
|
||||||
this.hasTraceData = hasTraceData;
|
this.hasTraceData = hasTraceData;
|
||||||
this.children = new ArrayList<>();
|
this.children = new ArrayList<>();
|
||||||
}
|
}
|
||||||
@@ -103,6 +108,8 @@ public final class ProcessorNode {
|
|||||||
public String getErrorHandlerType() { return errorHandlerType; }
|
public String getErrorHandlerType() { return errorHandlerType; }
|
||||||
public String getCircuitBreakerState() { return circuitBreakerState; }
|
public String getCircuitBreakerState() { return circuitBreakerState; }
|
||||||
public Boolean getFallbackTriggered() { return fallbackTriggered; }
|
public Boolean getFallbackTriggered() { return fallbackTriggered; }
|
||||||
|
public Boolean getFilterMatched() { return filterMatched; }
|
||||||
|
public Boolean getDuplicateMessage() { return duplicateMessage; }
|
||||||
public boolean isHasTraceData() { return hasTraceData; }
|
public boolean isHasTraceData() { return hasTraceData; }
|
||||||
public List<ProcessorNode> getChildren() { return List.copyOf(children); }
|
public List<ProcessorNode> getChildren() { return List.copyOf(children); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ export interface NodeExecutionState {
|
|||||||
hasTraceData?: boolean;
|
hasTraceData?: boolean;
|
||||||
/** Runtime-resolved endpoint URI (for TO_DYNAMIC, etc.) */
|
/** Runtime-resolved endpoint URI (for TO_DYNAMIC, etc.) */
|
||||||
resolvedEndpointUri?: string;
|
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 {
|
export interface IterationInfo {
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ function buildOverlay(
|
|||||||
subRouteFailed: subRouteFailed || undefined,
|
subRouteFailed: subRouteFailed || undefined,
|
||||||
hasTraceData: !!proc.hasTraceData,
|
hasTraceData: !!proc.hasTraceData,
|
||||||
resolvedEndpointUri: proc.resolvedEndpointUri || undefined,
|
resolvedEndpointUri: proc.resolvedEndpointUri || undefined,
|
||||||
|
filterMatched: (proc as Record<string, unknown>).filterMatched as boolean | undefined,
|
||||||
|
duplicateMessage: (proc as Record<string, unknown>).duplicateMessage as boolean | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Recurse into children
|
// Recurse into children
|
||||||
|
|||||||
@@ -66,6 +66,12 @@ export function CompoundNode({
|
|||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
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
|
// _TRY_BODY / _CB_MAIN: transparent wrapper — no header, no border, just layout
|
||||||
if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') {
|
if (node.type === '_TRY_BODY' || node.type === '_CB_MAIN') {
|
||||||
return (
|
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 (
|
return (
|
||||||
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
<g data-node-id={node.id} transform={`translate(${x}, ${y})`}>
|
||||||
{/* Container body */}
|
{/* Container body */}
|
||||||
@@ -128,14 +135,14 @@ export function CompoundNode({
|
|||||||
width={w}
|
width={w}
|
||||||
height={h}
|
height={h}
|
||||||
rx={CORNER_RADIUS}
|
rx={CORNER_RADIUS}
|
||||||
fill="white"
|
fill={containerFill}
|
||||||
stroke={color}
|
stroke={effectiveColor}
|
||||||
strokeWidth={1.5}
|
strokeWidth={isGated ? 2 : 1.5}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Colored header bar */}
|
{/* Colored header bar */}
|
||||||
<rect x={0} y={0} width={w} height={HEADER_HEIGHT} rx={CORNER_RADIUS} fill={color} />
|
<rect x={0} y={0} width={w} height={HEADER_HEIGHT} rx={CORNER_RADIUS} fill={effectiveColor} />
|
||||||
<rect x={CORNER_RADIUS} y={CORNER_RADIUS} width={w - CORNER_RADIUS * 2} height={HEADER_HEIGHT - CORNER_RADIUS} fill={color} />
|
<rect x={CORNER_RADIUS} y={CORNER_RADIUS} width={w - CORNER_RADIUS * 2} height={HEADER_HEIGHT - CORNER_RADIUS} fill={effectiveColor} />
|
||||||
|
|
||||||
{/* Header icon (left-aligned) */}
|
{/* Header icon (left-aligned) */}
|
||||||
<g transform={`translate(6, ${HEADER_HEIGHT / 2 - 5}) scale(0.417)`}>
|
<g transform={`translate(6, ${HEADER_HEIGHT / 2 - 5}) scale(0.417)`}>
|
||||||
|
|||||||
Reference in New Issue
Block a user