Files
cameleer-server/docs/superpowers/plans/2026-03-27-execution-overlay.md
hsiegeln 3a41e1f1d3 docs: add execution overlay implementation plan (sub-project 2)
12 tasks covering backend prerequisites (iteration fields, snapshot-by-id
endpoint), ProcessDiagram overlay props, node/edge visual states, compound
iteration stepper, detail panel with 7 tabs, ExecutionDiagram wrapper,
and ExchangeDetail page integration.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:25:47 +01:00

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 cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql Add iteration columns to processor_executions
Modify cameleer3-server-core/.../storage/ExecutionStore.java Extend ProcessorRecord with iteration fields
Modify cameleer3-server-app/.../storage/PostgresExecutionStore.java Update SQL queries for new columns
Modify cameleer3-server-core/.../ingestion/IngestionService.java Store iteration fields during ingestion
Modify cameleer3-server-core/.../detail/ProcessorNode.java Add iteration fields to detail model
Modify cameleer3-server-core/.../detail/DetailService.java Pass iteration fields through tree builder
Modify cameleer3-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 cameleer3-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 cameleer3-common and a new SNAPSHOT or release published before Task 1 Step 4 can work. Check cameleer3/cameleer3-common/src/main/java/com/cameleer3/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: cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/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 cameleer3-server-app/src/main/resources/db/migration/V8__processor_iteration_fields.sql \
  cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java \
  cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java \
  cameleer3-server-core/src/main/java/com/cameleer3/server/core/ingestion/IngestionService.java \
  cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/ProcessorNode.java \
  cameleer3-server-core/src/main/java/com/cameleer3/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: cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java

  • Modify: cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java

  • Modify: cameleer3-server-app/src/main/java/com/cameleer3/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 cameleer3-server-core/src/main/java/com/cameleer3/server/core/storage/ExecutionStore.java \
  cameleer3-server-app/src/main/java/com/cameleer3/server/app/storage/PostgresExecutionStore.java \
  cameleer3-server-core/src/main/java/com/cameleer3/server/core/detail/DetailService.java \
  cameleer3-server-app/src/main/java/com/cameleer3/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 cameleer3-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:

  1. Add loopIndex, loopSize, splitIndex, splitSize, multicastIndex (all integer, nullable) to the ProcessorNode schema
  2. 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: #3D7C47 at 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 #3D7C47 at bottom-right (e.g., "5ms")

Failed state:

  • Card background fill: #FDF2F0 (red tint)
  • Border stroke: #C0392B at 2px
  • Error badge: red circle (16px, #C0392B) with white ! at top-right
  • Duration text: red #C0392B at 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?: boolean prop. When true and no executionState, 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}>&lsaquo;</button>
      <span>{iterationInfo.current + 1} / {iterationInfo.total}</span>
      <button onClick={() => onIterationChange?.(node.id!, iterationInfo.current + 1)}
              disabled={iterationInfo.current >= iterationInfo.total - 1}>&rsaquo;</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 a NodeExecutionState with status and durationMs

  • 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 in iterationState for its parent compound

  • Set hasTraceData: true if 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:

  1. A processor header bar showing selected processor name, status badge, processor ID, duration — or exchange-level info when nothing selected
  2. A tab bar with 7 tabs: Info, Headers, Input, Output, Error, Config, Timeline
  3. 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.processors tree 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:

  1. Fetches ExecutionDetail via useExecutionDetail(executionId) if not pre-fetched
  2. Loads diagram by content hash via useDiagramLayout(detail.diagramContentHash, direction) — this ensures the diagram version matches the execution, not the latest version
  3. Initializes useIterationState(detail.processors)
  4. Computes useExecutionOverlay(detail.processors, iterationState)
  5. Manages selectedProcessorId state
  6. Finds the selected ProcessorNode in the tree by walking detail.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

  1. mvn clean compile -DskipTests passes (backend changes)
  2. cd ui && npx tsc -p tsconfig.app.json --noEmit passes (frontend types)
  3. ExecutionDiagram renders on ExchangeDetail page for a known failed exchange
  4. Completed nodes show green tint + checkmark + duration badge
  5. Failed nodes show red tint + ! badge + red duration
  6. Skipped nodes are dimmed to 35% opacity
  7. Edges between executed nodes turn green; edges to skipped nodes are dashed gray
  8. Loop/split compounds show iteration stepper; stepping updates child overlay
  9. Nested loops step independently (outer loop at iteration 2, inner split at branch 1)
  10. CHOICE compounds highlight taken branch, dim untaken branches
  11. Clicking a node shows its data in the detail panel
  12. 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
  13. "Jump to Error" navigates to and selects the failed processor
  14. Error tab grayed out for non-failed processors
  15. Config tab shows placeholder
  16. Resizable splitter between diagram and detail panel works