Rename Java packages from com.cameleer3 to com.cameleer, module directories from cameleer3-* to cameleer-*, and all references throughout workflows, Dockerfiles, docs, migrations, and pom.xml. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
44 KiB
Execution Overlay & Debugger — Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Overlay real execution data onto the ProcessDiagram component, turning it into an after-the-fact debugger with node status visualization, iteration stepping, and a tabbed detail panel.
Architecture: An ExecutionDiagram wrapper component composes the existing ProcessDiagram with execution data from useExecutionDetail. ProcessDiagram gains optional overlay props that tint nodes, color edges, and add iteration steppers to compound nodes. A resizable bottom panel shows per-processor details across 7 tabs. Backend prerequisites add iteration fields to the storage layer and a snapshot-by-processorId endpoint.
Tech Stack: Java 17 / Spring Boot (backend), React / TypeScript / CSS Modules (frontend), PostgreSQL (storage), React Query (data fetching)
Design spec: docs/superpowers/specs/2026-03-27-execution-overlay-design.md
File Structure
Backend Changes
| Action | File | Responsibility |
|---|---|---|
| Create | cameleer-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql |
Add iteration columns to processor_executions |
| Modify | cameleer-server-core/.../storage/ExecutionStore.java |
Extend ProcessorRecord with iteration fields |
| Modify | cameleer-server-app/.../storage/PostgresExecutionStore.java |
Update SQL queries for new columns |
| Modify | cameleer-server-core/.../ingestion/IngestionService.java |
Store iteration fields during ingestion |
| Modify | cameleer-server-core/.../detail/ProcessorNode.java |
Add iteration fields to detail model |
| Modify | cameleer-server-core/.../detail/DetailService.java |
Pass iteration fields through tree builder |
| Modify | cameleer-server-app/.../controller/DetailController.java |
Add snapshot-by-processorId endpoint |
Frontend Changes
| Action | File | Responsibility |
|---|---|---|
| Modify | ui/src/api/openapi.json |
Regenerate with new fields/endpoints |
| Modify | ui/src/api/schema.d.ts |
Regenerate TypeScript types |
| Modify | ui/src/api/queries/executions.ts |
Add useProcessorSnapshotById hook |
| Modify | ui/src/components/ProcessDiagram/types.ts |
Add overlay props + NodeExecutionState |
| Modify | ui/src/components/ProcessDiagram/DiagramNode.tsx |
Execution overlay visuals (tint, badges, duration) |
| Modify | ui/src/components/ProcessDiagram/DiagramEdge.tsx |
Traversed/not-traversed edge styling |
| Modify | ui/src/components/ProcessDiagram/CompoundNode.tsx |
Iteration stepper in compound header |
| Modify | ui/src/components/ProcessDiagram/ErrorSection.tsx |
Pass overlay props to child nodes |
| Modify | ui/src/components/ProcessDiagram/ProcessDiagram.tsx |
Pass overlay props through to children |
| Modify | ui/src/components/ProcessDiagram/ProcessDiagram.module.css |
Overlay-related styles |
| Create | ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx |
Root wrapper: exchange bar + diagram + detail panel |
| Create | ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css |
Layout styles (splitter, exchange bar, panel) |
| Create | ui/src/components/ExecutionDiagram/useExecutionOverlay.ts |
Maps execution data → node overlay state |
| Create | ui/src/components/ExecutionDiagram/useIterationState.ts |
Per-compound iteration tracking |
| Create | ui/src/components/ExecutionDiagram/DetailPanel.tsx |
Bottom panel: tabs container + processor header |
| Create | ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx |
Processor metadata + attributes |
| Create | ui/src/components/ExecutionDiagram/tabs/HeadersTab.tsx |
Input/output headers side-by-side |
| Create | ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx |
Formatted message body (shared by Input/Output) |
| Create | ui/src/components/ExecutionDiagram/tabs/ErrorTab.tsx |
Exception details + stack trace |
| Create | ui/src/components/ExecutionDiagram/tabs/ConfigTab.tsx |
Placeholder (TODO: agent data) |
| Create | ui/src/components/ExecutionDiagram/tabs/TimelineTab.tsx |
Gantt-style processor duration chart |
| Create | ui/src/components/ExecutionDiagram/types.ts |
Overlay-specific types |
| Create | ui/src/components/ExecutionDiagram/index.ts |
Public exports |
| Modify | ui/src/pages/ExchangeDetail/ExchangeDetail.tsx |
Replace RouteFlow with ExecutionDiagram |
Prerequisites
Before starting, verify that the cameleer-common dependency (in the agent repo) exposes iteration getters on ProcessorExecution: getLoopIndex(), getLoopSize(), getSplitIndex(), getSplitSize(), getMulticastIndex(). If these methods do not exist in the current published version, they must be added to cameleer-common and a new SNAPSHOT or release published before Task 1 Step 4 can work. Check cameleer/cameleer-common/src/main/java/com/cameleer/common/model/ProcessorExecution.java.
Note on migration versioning: The next migration is V8. If other work merges before this plan executes, bump the version number accordingly.
Tasks
Task 1: Add iteration fields to backend storage
Add loop_index, loop_size, split_index, split_size, multicast_index columns to the database and thread them through the storage → ingestion → detail pipeline.
Files:
-
Create:
cameleer-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql -
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java -
Modify:
cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresExecutionStore.java -
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java -
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ProcessorNode.java -
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java -
Step 1: Create Flyway migration V8
-- V8__processor_iteration_fields.sql
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_index INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS loop_size INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_index INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS split_size INTEGER;
ALTER TABLE processor_executions ADD COLUMN IF NOT EXISTS multicast_index INTEGER;
- Step 2: Extend ProcessorRecord in ExecutionStore.java
Add five new fields to the ProcessorRecord record. Current signature has 18 fields; add after attributes:
record ProcessorRecord(
String executionId, String processorId, String processorType,
String applicationName, String routeId,
int depth, String parentProcessorId, String status,
Instant startTime, Instant endTime, Long durationMs,
String errorMessage, String errorStacktrace,
String inputBody, String outputBody, String inputHeaders, String outputHeaders,
String attributes,
// NEW iteration fields:
Integer loopIndex, Integer loopSize,
Integer splitIndex, Integer splitSize,
Integer multicastIndex
) {}
- Step 3: Update PostgresExecutionStore SQL and row mapper
In upsertProcessors(): add the 5 new columns to the INSERT and ON CONFLICT SET clauses. In the ProcessorRecord RowMapper: read the new columns using rs.getObject("loop_index") != null ? rs.getInt("loop_index") : null pattern. In findProcessors(): add the new columns to the SELECT list.
- Step 4: Update IngestionService.flattenProcessors()
In the flattenProcessors method, extract iteration fields from ProcessorExecution and pass them to ProcessorRecord:
flat.add(new ProcessorRecord(
executionId, p.getProcessorId(), p.getProcessorType(),
applicationName, routeId,
depth, parentProcessorId,
p.getStatus() != null ? p.getStatus().name() : "RUNNING",
p.getStartTime() != null ? p.getStartTime() : execStartTime,
p.getEndTime(),
p.getDurationMs(),
p.getErrorMessage(), p.getErrorStackTrace(),
truncateBody(p.getInputBody()), truncateBody(p.getOutputBody()),
toJson(p.getInputHeaders()), toJson(p.getOutputHeaders()),
toJson(p.getAttributes()),
p.getLoopIndex(), p.getLoopSize(),
p.getSplitIndex(), p.getSplitSize(),
p.getMulticastIndex()
));
- Step 5: Add iteration fields to ProcessorNode.java
Add 5 private fields, update constructor, add getters:
private final Integer loopIndex;
private final Integer loopSize;
private final Integer splitIndex;
private final Integer splitSize;
private final Integer multicastIndex;
Constructor adds these after attributes. Add corresponding getters.
- Step 6: Update DetailService.buildTree() to pass iteration fields
In the node creation inside buildTree():
ProcessorNode node = new ProcessorNode(
p.processorId(), p.processorType(), p.status(),
p.startTime(), p.endTime(),
p.durationMs() != null ? p.durationMs() : 0L,
p.errorMessage(), p.errorStacktrace(),
parseAttributes(p.attributes()),
p.loopIndex(), p.loopSize(),
p.splitIndex(), p.splitSize(),
p.multicastIndex()
);
- Step 7: Verify backend compiles
Run: mvn clean compile -DskipTests
Expected: BUILD SUCCESS
- Step 8: Commit
git add cameleer-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql \
cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java \
cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresExecutionStore.java \
cameleer-server-core/src/main/java/com/cameleer/server/core/ingestion/IngestionService.java \
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/ProcessorNode.java \
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java
git commit -m "feat: add iteration fields to processor storage and detail model"
Task 2: Add snapshot-by-processorId endpoint
Add a REST endpoint that fetches processor snapshot data by processorId instead of positional index.
Files:
-
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java -
Modify:
cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresExecutionStore.java -
Modify:
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java -
Modify:
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DetailController.java -
Step 1: Add findProcessorById to ExecutionStore interface
Optional<ProcessorRecord> findProcessorById(String executionId, String processorId);
- Step 2: Implement in PostgresExecutionStore
@Override
public Optional<ProcessorRecord> findProcessorById(String executionId, String processorId) {
String sql = "SELECT * FROM processor_executions WHERE execution_id = ? AND processor_id = ? LIMIT 1";
List<ProcessorRecord> results = jdbc.query(sql, processorRowMapper, executionId, processorId);
return results.isEmpty() ? Optional.empty() : Optional.of(results.get(0));
}
- Step 3: Add getProcessorSnapshot method to DetailService
public Optional<Map<String, String>> getProcessorSnapshot(String executionId, String processorId) {
return executionStore.findProcessorById(executionId, processorId)
.map(p -> {
Map<String, String> snapshot = new LinkedHashMap<>();
if (p.inputBody() != null) snapshot.put("inputBody", p.inputBody());
if (p.outputBody() != null) snapshot.put("outputBody", p.outputBody());
if (p.inputHeaders() != null) snapshot.put("inputHeaders", p.inputHeaders());
if (p.outputHeaders() != null) snapshot.put("outputHeaders", p.outputHeaders());
return snapshot;
});
}
- Step 4: Add endpoint to DetailController
@GetMapping("/{executionId}/processors/by-id/{processorId}/snapshot")
public ResponseEntity<Map<String, String>> processorSnapshotById(
@PathVariable String executionId,
@PathVariable String processorId) {
return detailService.getProcessorSnapshot(executionId, processorId)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
- Step 5: Verify backend compiles
Run: mvn clean compile -DskipTests
Expected: BUILD SUCCESS
- Step 6: Commit
git add cameleer-server-core/src/main/java/com/cameleer/server/core/storage/ExecutionStore.java \
cameleer-server-app/src/main/java/com/cameleer/server/app/storage/PostgresExecutionStore.java \
cameleer-server-core/src/main/java/com/cameleer/server/core/detail/DetailService.java \
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DetailController.java
git commit -m "feat: add snapshot-by-processorId endpoint"
Task 3: Regenerate OpenAPI spec and TypeScript types
Update openapi.json with the new fields and endpoint, then regenerate schema.d.ts.
Files:
-
Modify:
ui/src/api/openapi.json -
Modify:
ui/src/api/schema.d.ts -
Step 1: Start the server and regenerate openapi.json
Run the server locally, then fetch the OpenAPI spec:
# Build and run
cd cameleer-server-app && mvn spring-boot:run -DskipTests &
# Wait for startup, then fetch
curl -s http://localhost:8080/v3/api-docs | python3 -m json.tool > ui/src/api/openapi.json
If the server cannot start (no database), manually add the following to openapi.json:
- Add
loopIndex,loopSize,splitIndex,splitSize,multicastIndex(allinteger, nullable) to theProcessorNodeschema - Add the new endpoint path
/api/v1/executions/{executionId}/processors/by-id/{processorId}/snapshot
- Step 2: Regenerate schema.d.ts
Run the project's type generation script, or manually add the new fields to the ProcessorNode interface in ui/src/api/schema.d.ts:
// Add to ProcessorNode in schema.d.ts
loopIndex?: number;
loopSize?: number;
splitIndex?: number;
splitSize?: number;
multicastIndex?: number;
Note: The current schema.d.ts has a stale diagramNodeId field on ProcessorNode — this was dropped in V6 migration and never existed in the Java model. Remove it during regeneration to avoid confusion. The processorId field IS the diagram node ID.
- Step 3: Verify frontend types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
Expected: No errors
- Step 4: Commit
git add ui/src/api/openapi.json ui/src/api/schema.d.ts
git commit -m "chore: regenerate openapi spec with iteration fields and snapshot-by-id endpoint"
Task 4: Add overlay types, extend ProcessDiagramProps, and add diagramLayout prop
Define the execution overlay type system and extend ProcessDiagram to accept optional overlay props. Critically, add a diagramLayout prop so ExecutionDiagram can load the diagram by diagramContentHash and pass the pre-fetched layout — ensuring the diagram version matches the execution, not the latest.
Files:
-
Create:
ui/src/components/ExecutionDiagram/types.ts -
Modify:
ui/src/components/ProcessDiagram/types.ts -
Modify:
ui/src/components/ProcessDiagram/useDiagramData.ts -
Modify:
ui/src/components/ProcessDiagram/ProcessDiagram.tsx -
Step 1: Create ExecutionDiagram types
Create ui/src/components/ExecutionDiagram/types.ts:
import type { components } from '../../api/schema';
export type ExecutionDetail = components['schemas']['ExecutionDetail'];
export type ProcessorNode = components['schemas']['ProcessorNode'];
export interface NodeExecutionState {
status: 'COMPLETED' | 'FAILED';
durationMs: number;
/** True if this node's target sub-route failed (DIRECT/SEDA) */
subRouteFailed?: boolean;
/** True if trace data is available for this processor */
hasTraceData?: boolean;
}
export interface IterationInfo {
/** Current iteration index (0-based) */
current: number;
/** Total number of iterations */
total: number;
/** Type of iteration (determines label) */
type: 'loop' | 'split' | 'multicast';
}
export type DetailTab = 'info' | 'headers' | 'input' | 'output' | 'error' | 'config' | 'timeline';
Note: The spec defines iterationState as Map<string, number>. This plan uses Map<string, IterationInfo> instead, which carries total and type in addition to current — needed by the CompoundNode stepper to render "3/5" and label the type. This is a deliberate improvement over the spec.
- Step 2: Extend ProcessDiagramProps with overlay and diagramLayout props
Add to ui/src/components/ProcessDiagram/types.ts:
import type { DiagramLayout } from '../../api/queries/diagrams';
import type { NodeExecutionState, IterationInfo } from '../ExecutionDiagram/types';
// Add to ProcessDiagramProps:
/** Pre-fetched diagram layout (bypasses internal fetch by application/routeId) */
diagramLayout?: DiagramLayout;
/** Execution overlay: maps diagram node ID → execution state */
executionOverlay?: Map<string, NodeExecutionState>;
/** Per-compound iteration info: maps compound node ID → iteration info */
iterationState?: Map<string, IterationInfo>;
/** Called when user changes iteration on a compound stepper */
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
- Step 3: Update useDiagramData to accept pre-fetched layout
Modify ui/src/components/ProcessDiagram/useDiagramData.ts to accept an optional DiagramLayout parameter. When provided, skip the useDiagramByRoute fetch and process the pre-fetched layout directly:
export function useDiagramData(
application: string, routeId: string, direction: 'LR' | 'TB',
preloadedLayout?: DiagramLayout, // NEW
) {
const { data: fetchedLayout, isLoading, error } = useDiagramByRoute(
preloadedLayout ? undefined : application, // disable fetch when preloaded
preloadedLayout ? undefined : routeId,
direction,
);
const layout = preloadedLayout ?? fetchedLayout;
// ... rest of section/edge separation logic uses `layout`
}
- Step 4: Update ProcessDiagram to pass diagramLayout through
In ProcessDiagram.tsx, pass the new diagramLayout prop to useDiagramData:
const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData(
application, currentRouteId, direction, diagramLayout,
);
- Step 3: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
Expected: No errors
- Step 4: Commit
git add ui/src/components/ExecutionDiagram/types.ts \
ui/src/components/ProcessDiagram/types.ts
git commit -m "feat: add execution overlay types and ProcessDiagram overlay props"
Task 5: DiagramNode overlay visual states
Update DiagramNode to render execution status: green tint + checkmark for completed, red tint + ! for failed, dimmed for skipped, duration badge.
Files:
- Modify:
ui/src/components/ProcessDiagram/DiagramNode.tsx - Modify:
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
Reference: Design spec Section 3 — Node Visual States
- Step 1: Add overlay prop to DiagramNode
Add executionState?: NodeExecutionState to the DiagramNodeProps interface. Import NodeExecutionState from ExecutionDiagram/types.
- Step 2: Implement overlay visuals
When executionState is present:
Completed state:
- Card background fill:
#F0F9F1(green tint) instead of white - Border stroke:
#3D7C47at 1.5px + left accent bar 4px green - Checkmark badge: green circle (16px,
#3D7C47) with white✓at top-right corner (x: node.width - 8, y: -8) - Duration text: green
#3D7C47at bottom-right (e.g., "5ms")
Failed state:
- Card background fill:
#FDF2F0(red tint) - Border stroke:
#C0392Bat 2px - Error badge: red circle (16px,
#C0392B) with white!at top-right - Duration text: red
#C0392Bat bottom-right - Label text color:
#C0392B
Sub-route failure (subRouteFailed flag):
- Same as failed, plus a drill-down arrow icon (↳) at bottom-left
No execution state and overlay is active (skipped):
-
Set group opacity to 0.35
-
This requires knowing whether overlay mode is active. Add an
overlayActive?: booleanprop. When true and noexecutionState, dim the node. -
Step 3: Add CSS for overlay elements
Add to ProcessDiagram.module.css: styles for duration badge text positioning and any overlay-specific hover states.
- Step 4: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 5: Commit
git add ui/src/components/ProcessDiagram/DiagramNode.tsx \
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
git commit -m "feat: DiagramNode execution overlay visuals (tint, badges, duration)"
Task 6: DiagramEdge traversed/not-traversed states
Update DiagramEdge to show green solid edges for traversed paths and dashed gray for not-traversed.
Files:
- Modify:
ui/src/components/ProcessDiagram/DiagramEdge.tsx - Modify:
ui/src/components/ProcessDiagram/ProcessDiagram.tsx
Reference: Design spec Section 3 — Edge States
- Step 1: Add traversal prop to DiagramEdge
Add traversed?: boolean | undefined to DiagramEdgeProps. When undefined (no overlay), render as current default. When true, render green solid. When false, render gray dashed.
// Traversed: green solid
stroke={traversed === true ? '#3D7C47' : traversed === false ? '#9CA3AF' : '#9CA3AF'}
strokeWidth={traversed === true ? 1.5 : 1}
strokeDasharray={traversed === false ? '4,3' : undefined}
markerEnd={traversed === false ? undefined : 'url(#arrowhead)'}
- Step 2: Add a green arrowhead marker
In ProcessDiagram.tsx <defs>, add a second marker with green fill:
<marker id="arrowhead-green" markerWidth="8" markerHeight="6" refX="7" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" fill="#3D7C47" />
</marker>
Use url(#arrowhead-green) for traversed edges.
- Step 3: Compute edge traversal in ProcessDiagram
In ProcessDiagram.tsx, when executionOverlay is present, determine if an edge is traversed by checking if both its sourceId and targetId have entries in the overlay map. Pass this as traversed prop to each DiagramEdge.
- Step 4: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 5: Commit
git add ui/src/components/ProcessDiagram/DiagramEdge.tsx \
ui/src/components/ProcessDiagram/ProcessDiagram.tsx
git commit -m "feat: DiagramEdge traversed/not-traversed overlay states"
Task 7: CompoundNode iteration stepper
Add an iteration stepper widget to compound node headers (LOOP, SPLIT, MULTICAST) when overlay iteration data is present.
Files:
- Modify:
ui/src/components/ProcessDiagram/CompoundNode.tsx - Modify:
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
Reference: Design spec Section 4 — Per-Compound Iteration Stepper
- Step 1: Add overlay props to CompoundNode
Add to CompoundNodeProps:
executionOverlay?: Map<string, NodeExecutionState>;
overlayActive?: boolean;
iterationState?: Map<string, IterationInfo>;
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
- Step 2: Render iteration stepper in compound header
When iterationState has an entry for this compound node's ID, render a stepper widget inside the colored header bar (right-aligned):
{iterationInfo && (
<foreignObject x={headerWidth - 80} y={1} width={75} height={20}>
<div className={styles.iterationStepper}>
<button onClick={() => onIterationChange?.(node.id!, iterationInfo.current - 1)}
disabled={iterationInfo.current <= 0}>‹</button>
<span>{iterationInfo.current + 1} / {iterationInfo.total}</span>
<button onClick={() => onIterationChange?.(node.id!, iterationInfo.current + 1)}
disabled={iterationInfo.current >= iterationInfo.total - 1}>›</button>
</div>
</foreignObject>
)}
- Step 3: Add CSS for iteration stepper
.iterationStepper {
display: flex;
align-items: center;
gap: 2px;
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
padding: 1px 3px;
font-size: 10px;
color: white;
}
.iterationStepper button {
width: 16px;
height: 16px;
border: none;
background: rgba(255, 255, 255, 0.2);
color: white;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.iterationStepper button:disabled {
opacity: 0.3;
cursor: default;
}
- Step 4: Pass overlay props through from ProcessDiagram
In ProcessDiagram.tsx, pass executionOverlay, overlayActive (= !!executionOverlay), iterationState, and onIterationChange down to CompoundNode instances and ErrorSection instances. Both components render child nodes that need overlay state.
- Step 5: Update ErrorSection to accept and forward overlay props
ErrorSection.tsx internally renders DiagramNode and CompoundNode. Add executionOverlay, overlayActive, iterationState, and onIterationChange to its props interface, and pass them through to its child node renderers. Without this, error/completion handler section nodes will render without overlay styling.
- Step 6: Apply overlay dimming to compound children
When overlay is active: compound children without execution state should be dimmed (opacity 0.35). Pass overlayActive and executionOverlay down to child DiagramNode and recursive CompoundNode calls.
Compound node overlay behavior: Compound nodes (LOOP, SPLIT, CHOICE) appear as processors in the execution tree but should NOT get status badges. The useExecutionOverlay hook (Task 9) should either skip compound-type processor IDs or mark them with a flag. Compound nodes derive their visual from their children — if all children are green, the compound looks normal; if a child failed, the child shows red.
- Step 7: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 8: Commit
git add ui/src/components/ProcessDiagram/CompoundNode.tsx \
ui/src/components/ProcessDiagram/ErrorSection.tsx \
ui/src/components/ProcessDiagram/ProcessDiagram.tsx \
ui/src/components/ProcessDiagram/ProcessDiagram.module.css
git commit -m "feat: CompoundNode iteration stepper and overlay prop threading"
Task 8: useProcessorSnapshotById hook
Add a frontend hook for the new snapshot-by-processorId endpoint.
Files:
-
Modify:
ui/src/api/queries/executions.ts -
Step 1: Add the hook
Important: This project uses openapi-fetch with typed api.GET() calls (uppercase, path template parameters), not axios-style api.get(). Follow the existing pattern in executions.ts:
export function useProcessorSnapshotById(
executionId: string | null,
processorId: string | null,
) {
return useQuery({
queryKey: ['processor-snapshot-by-id', executionId, processorId],
queryFn: async () => {
const { data, error } = await api.GET(
'/executions/{executionId}/processors/by-id/{processorId}/snapshot',
{ params: { path: { executionId: executionId!, processorId: processorId! } } },
);
if (error) throw error;
return data as Record<string, string>;
},
enabled: !!executionId && !!processorId,
});
}
Note: This endpoint path must exist in openapi.json (added in Task 3) for the typed path to work. If the openapi spec was manually updated rather than regenerated, ensure the path template matches exactly.
- Step 2: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 3: Commit
git add ui/src/api/queries/executions.ts
git commit -m "feat: add useProcessorSnapshotById hook"
Task 9: useExecutionOverlay and useIterationState hooks
Create the hooks that map execution data to diagram overlay state and manage per-compound iteration.
Files:
-
Create:
ui/src/components/ExecutionDiagram/useExecutionOverlay.ts -
Create:
ui/src/components/ExecutionDiagram/useIterationState.ts -
Step 1: Create useExecutionOverlay
This hook takes an ExecutionDetail and the current iteration state, and produces a Map<string, NodeExecutionState> mapping processor IDs to overlay states.
import { useMemo } from 'react';
import type { NodeExecutionState, IterationInfo } from './types';
import type { components } from '../../api/schema';
type ProcessorNode = components['schemas']['ProcessorNode'];
export function useExecutionOverlay(
processors: ProcessorNode[] | undefined,
iterationState: Map<string, IterationInfo>,
): Map<string, NodeExecutionState> {
return useMemo(() => {
if (!processors) return new Map();
const overlay = new Map<string, NodeExecutionState>();
buildOverlay(processors, overlay, iterationState);
return overlay;
}, [processors, iterationState]);
}
The buildOverlay function recursively walks the ProcessorNode tree:
-
For each processor with a
processorId, create aNodeExecutionStatewithstatusanddurationMs -
If the processor is a DIRECT/SEDA type and has status FAILED, set
subRouteFailed: true(the target sub-route failed) -
For processors with
loopIndex/splitIndex: only include the processor if it matches the current iteration selected initerationStatefor its parent compound -
Set
hasTraceData: trueif the processor has non-null fields (as a heuristic, or always true since snapshot can be fetched lazily) -
Step 2: Create useIterationState
import { useCallback, useState } from 'react';
import type { IterationInfo } from './types';
import type { components } from '../../api/schema';
type ProcessorNode = components['schemas']['ProcessorNode'];
export function useIterationState(processors: ProcessorNode[] | undefined) {
const [state, setState] = useState<Map<string, IterationInfo>>(new Map());
// Initialize iteration info from processor tree when processors change
// Walk the tree, find compounds with children that have loopSize/splitSize
// Set default iteration to 0 for each compound
const setIteration = useCallback((compoundId: string, index: number) => {
setState(prev => {
const next = new Map(prev);
const info = next.get(compoundId);
if (info && index >= 0 && index < info.total) {
next.set(compoundId, { ...info, current: index });
}
return next;
});
}, []);
return { iterationState: state, setIteration };
}
Key logic: Walk the ProcessorNode tree looking for processors that are children of the same parent and have different loopIndex or splitIndex values. Group by parent, compute total from loopSize/splitSize, initialize current: 0.
- Step 3: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 4: Commit
git add ui/src/components/ExecutionDiagram/useExecutionOverlay.ts \
ui/src/components/ExecutionDiagram/useIterationState.ts
git commit -m "feat: useExecutionOverlay and useIterationState hooks"
Task 10: DetailPanel with tab infrastructure
Create the bottom panel component with tab bar, processor header, and tab content area.
Files:
- Create:
ui/src/components/ExecutionDiagram/DetailPanel.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/InfoTab.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/HeadersTab.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/BodyTab.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/ErrorTab.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/ConfigTab.tsx - Create:
ui/src/components/ExecutionDiagram/tabs/TimelineTab.tsx - Modify:
ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
Reference: Design spec Section 6 — Detail Panel
- Step 1: Create DetailPanel shell
interface DetailPanelProps {
/** Currently selected processor (or null for exchange-level view) */
selectedProcessor: ProcessorNode | null;
/** The full execution detail (for exchange-level tabs and timeline) */
executionDetail: ExecutionDetail;
/** Execution ID for fetching snapshots */
executionId: string;
/** Callback to select a processor (from timeline clicks) */
onSelectProcessor: (processorId: string) => void;
}
The panel renders:
- A processor header bar showing selected processor name, status badge, processor ID, duration — or exchange-level info when nothing selected
- A tab bar with 7 tabs: Info, Headers, Input, Output, Error, Config, Timeline
- Active tab content area (scrollable)
Track activeTab state internally. When selectedProcessor changes, keep the current tab unless it was Error (auto-switch to Info for non-failed processors).
- Step 2: Create InfoTab
Grid layout showing:
- Processor: ID, Type, Status, Start/End time, Duration, Endpoint URI
- Attributes: tap-extracted attributes rendered as pill badges
- When no processor selected: show exchange-level metadata (executionId, correlationId, route, app, total duration, engine level, route attributes)
Data source: ProcessorNode fields directly (no API call needed).
- Step 3: Create HeadersTab
Side-by-side layout:
- Left: Input headers parsed from JSON string → key/value table
- Right: Output headers parsed from JSON string → key/value table
- Compare and highlight new/changed headers in green
Data source: useProcessorSnapshotById(executionId, processorId) → inputHeaders, outputHeaders. When no processor selected, use executionDetail.inputHeaders / executionDetail.outputHeaders.
- Step 4: Create BodyTab (shared by Input and Output)
Formatted body display:
- Auto-detect format (try JSON parse → formatted, otherwise plain text)
- Dark-themed code block with monospace font
- Copy button
- Byte size indicator
Props: { body: string | undefined; label: string }. The Input tab passes snapshot.inputBody, the Output tab passes snapshot.outputBody.
- Step 5: Create ErrorTab
When processor has error:
- Exception type (errorType from attributes if available, otherwise derive from errorMessage)
- Error message
- Root cause (if available in attributes)
- Stack trace in monospace pre block
When no error: show grayed-out "No error on this processor" message.
Data source: ProcessorNode.errorMessage, ProcessorNode.errorStackTrace.
- Step 6: Create ConfigTab (placeholder)
Show a centered message: "Processor configuration data is not yet available." Styled with muted text.
- Step 7: Create TimelineTab
Gantt-style horizontal bar chart:
-
Flatten
ExecutionDetail.processorstree into execution-order list -
One row per executed processor
-
Bar width:
(processor.durationMs / execution.durationMs) * 100% -
Bar offset:
((processor.startTime - execution.startTime) / execution.durationMs) * 100% -
Green bars for COMPLETED, red for FAILED
-
Click handler:
onSelectProcessor(processorId) -
Highlight the currently selected processor
-
Step 8: Add CSS for detail panel
Add to ExecutionDiagram.module.css:
-
.detailPanel— flex column layout -
.processorHeader— flex row with processor info -
.tabBar— flex row of tab buttons -
.tabButton,.tabButtonActive— styled tabs -
.tabContent— scrollable content area -
Tab-specific styles (
.infoGrid,.headersTable,.codeBlock,.ganttRow, etc.) -
Step 9: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 10: Commit
git add ui/src/components/ExecutionDiagram/DetailPanel.tsx \
ui/src/components/ExecutionDiagram/tabs/ \
ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
git commit -m "feat: DetailPanel with 7 tabs (Info, Headers, Input, Output, Error, Config, Timeline)"
Task 11: ExecutionDiagram wrapper component
Create the root wrapper that composes ProcessDiagram with execution overlay data, the exchange summary bar, resizable splitter, and the detail panel.
Files:
- Create:
ui/src/components/ExecutionDiagram/ExecutionDiagram.tsx - Create:
ui/src/components/ExecutionDiagram/index.ts - Modify:
ui/src/components/ExecutionDiagram/ExecutionDiagram.module.css
Reference: Design spec Sections 1, 5, 7
- Step 1: Create ExecutionDiagram component
interface ExecutionDiagramProps {
executionId: string;
executionDetail?: ExecutionDetail;
direction?: 'LR' | 'TB';
knownRouteIds?: Set<string>;
onNodeAction?: (nodeId: string, action: NodeAction) => void;
nodeConfigs?: Map<string, NodeConfig>;
className?: string;
}
The component:
- Fetches
ExecutionDetailviauseExecutionDetail(executionId)if not pre-fetched - Loads diagram by content hash via
useDiagramLayout(detail.diagramContentHash, direction)— this ensures the diagram version matches the execution, not the latest version - Initializes
useIterationState(detail.processors) - Computes
useExecutionOverlay(detail.processors, iterationState) - Manages
selectedProcessorIdstate - Finds the selected
ProcessorNodein the tree by walkingdetail.processors
RUNNING execution handling: If detail.status === 'RUNNING', the overlay shows completed processors with green/red and unreached processors as dimmed. No special RUNNING visual is needed — the overlay hook only creates entries for processors that have executed.
Layout:
<div className={styles.executionDiagram}>
{/* Exchange summary bar */}
<div className={styles.exchangeBar}>
<ExchangeId /> <StatusBadge /> <RouteInfo /> <Duration />
{detail.status === 'FAILED' && <JumpToErrorButton />}
</div>
{/* Diagram area */}
<div className={styles.diagramArea} style={{ flex: `1 1 ${splitPercent}%` }}>
<ProcessDiagram
application={detail.applicationName}
routeId={detail.routeId}
direction={direction}
diagramLayout={diagramLayout}
selectedNodeId={selectedProcessorId}
onNodeSelect={setSelectedProcessorId}
onNodeAction={onNodeAction}
nodeConfigs={nodeConfigs}
knownRouteIds={knownRouteIds}
executionOverlay={overlay}
iterationState={iterationState}
onIterationChange={setIteration}
/>
</div>
{/* Resizable splitter */}
<div className={styles.splitter} onPointerDown={startResize} />
{/* Detail panel */}
<div className={styles.detailArea} style={{ flex: `0 0 ${100 - splitPercent}%` }}>
<DetailPanel
selectedProcessor={findProcessorInTree(detail.processors, selectedProcessorId)}
executionDetail={detail}
executionId={executionId}
onSelectProcessor={setSelectedProcessorId}
/>
</div>
</div>
- Step 2: Implement resizable splitter
Track splitPercent state (default 60). On pointer down on splitter, capture pointer, update percentage on pointer move based on container height. Minimum panel height ~120px.
- Step 3: Implement "Jump to Error"
Find first FAILED ProcessorNode in tree. Set it as selected. If it's a DIRECT/SEDA node, trigger drill-down into the sub-route via ProcessDiagram's double-click mechanism. Auto-switch detail panel to Error tab.
- Step 4: Add exchange summary bar CSS
.exchangeBar {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 14px;
background: var(--bg-surface, #FFFFFF);
border-bottom: 1px solid var(--border, #E4DFD8);
font-size: 12px;
color: var(--text-secondary, #5C5347);
flex-shrink: 0;
}
- Step 5: Add layout CSS
.executionDiagram {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
min-height: 400px;
}
.diagramArea {
overflow: hidden;
position: relative;
}
.splitter {
height: 4px;
background: var(--border, #E4DFD8);
cursor: row-resize;
flex-shrink: 0;
}
.detailArea {
overflow: hidden;
min-height: 120px;
}
- Step 6: Create index.ts
export { ExecutionDiagram } from './ExecutionDiagram';
export type { ExecutionDiagramProps } from './ExecutionDiagram';
- Step 7: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 8: Commit
git add ui/src/components/ExecutionDiagram/
git commit -m "feat: ExecutionDiagram wrapper with exchange bar, splitter, and detail panel"
Task 12: Integrate into ExchangeDetail page
Replace the existing RouteFlow-based "Flow" view in ExchangeDetail with the new ExecutionDiagram component.
Files:
- Modify:
ui/src/pages/ExchangeDetail/ExchangeDetail.tsx - Modify:
ui/src/pages/ExchangeDetail/ExchangeDetail.module.css
Reference: Design spec Section 9; existing Flow view at lines ~486-507 of ExchangeDetail.tsx
- Step 1: Import ExecutionDiagram
import { ExecutionDiagram } from '../../components/ExecutionDiagram';
- Step 2: Replace the Flow view section
Replace the RouteFlow rendering in the "flow" view branch with:
{timelineView === 'flow' && (
<div className={styles.executionDiagramContainer}>
<ExecutionDiagram
executionId={id!}
executionDetail={detail}
knownRouteIds={knownRouteIds}
onNodeAction={handleNodeAction}
nodeConfigs={nodeConfigs}
/>
</div>
)}
Build knownRouteIds from the route catalog (same pattern as DevDiagram page). Build nodeConfigs from existing tracedMap.
- Step 3: Remove or simplify redundant sections
The ExchangeDetail page currently has its own processor snapshot display (Message IN/OUT panels). With the detail panel inside ExecutionDiagram, these become redundant when in Flow view. Keep them for the Gantt view, but hide when Flow view is active.
The exchange header card at the top of the page stays — it provides context. The ExecutionDiagram's exchange bar provides a compact summary inside the diagram area.
- Step 4: Add CSS for container
.executionDiagramContainer {
height: 600px;
border: 1px solid var(--border, #E4DFD8);
border-radius: var(--radius-md, 8px);
overflow: hidden;
}
- Step 5: Verify types compile
Run: cd ui && npx tsc -p tsconfig.app.json --noEmit
- Step 6: Manual test
Navigate to an exchange detail page for a known execution. Toggle to Flow view. Verify:
-
Diagram renders with execution overlay
-
Completed nodes are green with checkmarks
-
Failed nodes are red with ! badge
-
Skipped nodes are dimmed
-
Clicking a node updates the detail panel
-
Tabs work (Info, Headers, Input, Output, Error, Timeline)
-
"Jump to Error" navigates to the failed processor
-
Step 7: Commit
git add ui/src/pages/ExchangeDetail/ExchangeDetail.tsx \
ui/src/pages/ExchangeDetail/ExchangeDetail.module.css
git commit -m "feat: integrate ExecutionDiagram into ExchangeDetail flow view"
Verification
mvn clean compile -DskipTestspasses (backend changes)cd ui && npx tsc -p tsconfig.app.json --noEmitpasses (frontend types)- ExecutionDiagram renders on ExchangeDetail page for a known failed exchange
- Completed nodes show green tint + checkmark + duration badge
- Failed nodes show red tint + ! badge + red duration
- Skipped nodes are dimmed to 35% opacity
- Edges between executed nodes turn green; edges to skipped nodes are dashed gray
- Loop/split compounds show iteration stepper; stepping updates child overlay
- Nested loops step independently (outer loop at iteration 2, inner split at branch 1)
- CHOICE compounds highlight taken branch, dim untaken branches
- Clicking a node shows its data in the detail panel
- Detail panel tabs: Info shows metadata + attributes, Headers shows side-by-side, Input/Output show formatted body, Error shows exception + stack trace, Timeline shows Gantt chart
- "Jump to Error" navigates to and selects the failed processor
- Error tab grayed out for non-failed processors
- Config tab shows placeholder
- Resizable splitter between diagram and detail panel works