Files
cameleer-server/docs/superpowers/plans/2026-03-27-execution-overlay.md

1122 lines
44 KiB
Markdown
Raw Normal View History

# 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**
```sql
-- 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`:
```java
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`:
```java
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:
```java
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()`:
```java
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**
```bash
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**
```java
Optional<ProcessorRecord> findProcessorById(String executionId, String processorId);
```
- [ ] **Step 2: Implement in PostgresExecutionStore**
```java
@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**
```java
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**
```java
@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**
```bash
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:
```bash
# 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`:
```typescript
// 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**
```bash
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`:
```typescript
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`:
```typescript
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:
```typescript
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`:
```typescript
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**
```bash
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**
```bash
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.
```typescript
// 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:
```xml
<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**
```bash
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:
```typescript
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):
```tsx
{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**
```css
.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**
```bash
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`:
```typescript
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**
```bash
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.
```typescript
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**
```typescript
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**
```bash
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**
```typescript
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**
```bash
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**
```typescript
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:
```tsx
<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**
```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**
```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**
```typescript
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**
```bash
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**
```typescript
import { ExecutionDiagram } from '../../components/ExecutionDiagram';
```
- [ ] **Step 2: Replace the Flow view section**
Replace the `RouteFlow` rendering in the "flow" view branch with:
```tsx
{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**
```css
.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**
```bash
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
11. 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
12. "Jump to Error" navigates to and selects the failed processor
13. Error tab grayed out for non-failed processors
14. Config tab shows placeholder
15. Resizable splitter between diagram and detail panel works