-
- {/* Exchange header card */}
-
-
-
-
-
-
- {detail.executionId}
-
-
-
- Route: navigate(`/apps/${detail.applicationName}/${detail.routeId}`)}>{detail.routeId}
- {detail.applicationName && (
- <>
- ·
- App: {detail.applicationName}
- >
- )}
- {detail.correlationId && (
- <>
- ·
- Correlation: {detail.correlationId}
- >
- )}
-
-
-
-
-
-
Duration
-
{formatDuration(detail.durationMs)}
-
-
-
Agent
-
{detail.agentId}
-
-
-
Started
-
- {detail.startTime
- ? new Date(detail.startTime).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
- : '\u2014'}
-
-
-
-
Processors
-
{countProcessors(procList)}
-
-
-
-
-
- {/* Route-level Attributes */}
- {detail.attributes && Object.keys(detail.attributes).length > 0 && (
-
- Attributes
- {Object.entries(detail.attributes).map(([key, value]) => (
-
- ))}
-
- )}
-
- {/* Correlation Chain */}
- {correlatedExchanges.length > 1 && (
-
- Correlated Exchanges
- {correlatedExchanges.map((ce) => {
- const isCurrent = ce.executionId === id
- const variant = backendStatusToVariant(ce.status)
- const statusCls =
- variant === 'success' ? styles.chainNodeSuccess
- : variant === 'error' ? styles.chainNodeError
- : variant === 'running' ? styles.chainNodeRunning
- : styles.chainNodeWarning
- return (
-
- )
- })}
- {correlationData && correlationData.total > 20 && (
- +{correlationData.total - 20} more
- )}
-
- )}
-
-
- {/* Processor Timeline Section */}
-
-
-
- Processor Timeline
- {processors.length} processors
-
-
-
-
-
-
- {timelineView === 'gantt' && (
-
- {processors.length > 0 ? (
-
setSelectedProcessorIndex(index)}
- selectedIndex={activeIndex}
- getActions={(_proc, index) => {
- const pid = processorIds[index]
- if (!pid || !detail?.applicationName) return []
- return [{
- label: tracingStore.isTraced(app, pid) ? 'Disable Tracing' : 'Enable Tracing',
- onClick: () => handleToggleTracing(pid),
- disabled: updateConfig.isPending,
- }]
- }}
- />
- ) : (
- No processor data available
- )}
-
- )}
-
-
- {timelineView === 'flow' && detail && (
-
-
-
- )}
-
- {/* Exchange-level body (start/end of route) */}
- {detail && (detail.inputBody || detail.outputBody) && (
-
-
-
-
- → Exchange Input
-
- at route entry
-
-
- {detail.inputHeaders && (
-
-
Headers
-
- {Object.entries(parseHeaders(detail.inputHeaders)).map(([key, value]) => (
-
- {key}
- {value}
-
- ))}
-
-
- )}
-
-
-
-
-
-
- ← Exchange Output
-
- at route exit
-
-
- {detail.outputHeaders && (
-
-
Headers
-
- {Object.entries(parseHeaders(detail.outputHeaders)).map(([key, value]) => (
-
- {key}
- {value}
-
- ))}
-
-
- )}
-
-
-
-
- )}
-
- {/* Processor Attributes */}
- {selectedProc && (() => {
- const procNode = flatProcNodes[activeIndex]
- return procNode?.attributes && Object.keys(procNode.attributes).length > 0 ? (
-
- Processor Attributes
- {Object.entries(procNode.attributes).map(([key, value]) => (
-
- ))}
-
- ) : null
- })()}
-
- {/* Processor Detail Panel (split IN / OUT) */}
- {selectedProc && snapshot && (
-
- {/* Message IN */}
-
-
-
- → Message IN
-
- at processor #{activeIndex + 1} entry
-
-
- {Object.keys(inputHeaders).length > 0 && (
-
-
- Headers {Object.keys(inputHeaders).length}
-
-
- {Object.entries(inputHeaders).map(([key, value]) => (
-
- {key}
- {value}
-
- ))}
-
-
- )}
-
-
-
-
- {/* Message OUT or Error */}
- {isSelectedFailed ? (
-
-
-
- × Error at Processor #{activeIndex + 1}
-
-
-
-
- {detail.errorMessage && (
-
{detail.errorMessage}
- )}
-
- Processor
- {selectedProc.name}
- Duration
- {formatDuration(selectedProc.durationMs)}
- Status
- {selectedProc.status.toUpperCase()}
-
-
-
- ) : (
-
-
-
- ← Message OUT
-
- after processor #{activeIndex + 1}
-
-
- {Object.keys(outputHeaders).length > 0 && (
-
-
- Headers {Object.keys(outputHeaders).length}
-
-
- {Object.entries(outputHeaders).map(([key, value]) => (
-
- {key}
- {value}
-
- ))}
-
-
- )}
-
-
-
- )}
-
- )}
-
- {/* Snapshot loading indicator */}
- {selectedProc && !snapshot && procList.length > 0 && (
-
- Loading exchange snapshot...
-
- )}
-
- {/* Exchange Application Log */}
- {detail && (
-
-
- Application Log
- {logEntries.length} entries
-
-
-
- setLogSearch(e.target.value)}
- aria-label="Search logs"
- />
- {logSearch && (
-
- )}
-
-
- {logLevels.size > 0 && (
-
- )}
-
- {filteredLogs.length > 0 ? (
-
- ) : (
-
- {logSearch || logLevels.size > 0 ? 'No matching log entries' : 'No logs captured for this exchange'}
-
- )}
-
- )}
-
- {/* Replay Modal */}
-
setReplayOpen(false)} title="Replay Exchange" size="lg">
-
- This will re-send the exchange to a live agent. The agent will process
- it as a new exchange. Use with caution in production environments.
-
-
-
- Target Agent
-
-
- h.key).length },
- { label: 'Body', value: 'body' },
- ]}
- active={replayTab}
- onChange={setReplayTab}
- />
-
- {replayTab === 'headers' && (
-
-
- Key
- Value
-
-
- {replayHeaders.map((h, i) => (
-
- {
- const next = [...replayHeaders]
- next[i] = { ...next[i], key: e.target.value }
- setReplayHeaders(next)
- }}
- />
- {
- const next = [...replayHeaders]
- next[i] = { ...next[i], value: e.target.value }
- setReplayHeaders(next)
- }}
- />
-
-
- ))}
-
-
- )}
-
- {replayTab === 'body' && (
-
-
- )}
-
-
-
-
-
-
-
-
- )
-}
diff --git a/ui/src/pages/Exchanges/ExchangeList.module.css b/ui/src/pages/Exchanges/ExchangeList.module.css
deleted file mode 100644
index 7dfec372..00000000
--- a/ui/src/pages/Exchanges/ExchangeList.module.css
+++ /dev/null
@@ -1,74 +0,0 @@
-.list {
- display: flex;
- flex-direction: column;
- overflow-y: auto;
- height: 100%;
- border-right: 1px solid var(--border);
- background: var(--surface);
-}
-
-.item {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- padding: 0.625rem 0.75rem;
- cursor: pointer;
- border-bottom: 1px solid var(--border-light);
- font-size: 0.8125rem;
- transition: background 0.1s;
-}
-
-.item:hover {
- background: var(--surface-hover);
-}
-
-.itemSelected {
- background: var(--surface-active);
- border-left: 3px solid var(--amber);
- padding-left: calc(0.75rem - 3px);
-}
-
-.dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- flex-shrink: 0;
-}
-
-.dotOk { background: var(--success); }
-.dotErr { background: var(--error); }
-.dotRun { background: var(--running); }
-
-.meta {
- flex: 1;
- min-width: 0;
-}
-
-.exchangeId {
- font-family: var(--font-mono);
- font-size: 0.6875rem;
- color: var(--text-muted);
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.duration {
- font-family: var(--font-mono);
- font-size: 0.75rem;
- color: var(--text-secondary);
- flex-shrink: 0;
-}
-
-.timestamp {
- font-size: 0.6875rem;
- color: var(--text-muted);
- flex-shrink: 0;
-}
-
-.empty {
- padding: 2rem;
- text-align: center;
- color: var(--text-muted);
- font-size: 0.8125rem;
-}
diff --git a/ui/src/pages/Exchanges/ExchangeList.tsx b/ui/src/pages/Exchanges/ExchangeList.tsx
deleted file mode 100644
index 0cc83d6f..00000000
--- a/ui/src/pages/Exchanges/ExchangeList.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import type { ExecutionSummary } from '../../api/types';
-import styles from './ExchangeList.module.css';
-
-interface ExchangeListProps {
- exchanges: ExecutionSummary[];
- selectedId?: string;
- onSelect: (exchange: ExecutionSummary) => void;
-}
-
-function formatDuration(ms: number): string {
- if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`;
- if (ms >= 1000) return `${(ms / 1000).toFixed(2)}s`;
- return `${ms}ms`;
-}
-
-function formatTime(iso: string): string {
- const d = new Date(iso);
- const h = String(d.getHours()).padStart(2, '0');
- const m = String(d.getMinutes()).padStart(2, '0');
- const s = String(d.getSeconds()).padStart(2, '0');
- return `${h}:${m}:${s}`;
-}
-
-function dotClass(status: string): string {
- switch (status) {
- case 'COMPLETED': return styles.dotOk;
- case 'FAILED': return styles.dotErr;
- case 'RUNNING': return styles.dotRun;
- default: return styles.dotOk;
- }
-}
-
-export function ExchangeList({ exchanges, selectedId, onSelect }: ExchangeListProps) {
- if (exchanges.length === 0) {
- return