;
+ highlighted: boolean;
+ onClick: () => void;
}) {
+ const group = groupByAgent.get(exec.agentId) ?? 'default';
return (
- <>
-
- | › |
- {formatTime(exec.startTime)} |
-
-
- |
-
-
- |
- {exec.routeId} |
-
- {exec.correlationId ?? '-'}
- |
-
-
- |
-
- {isExpanded && (
-
-
-
-
-
-
-
-
-
- |
-
- )}
- >
+
+ | {formatTime(exec.startTime)} |
+
+
+ |
+
+
+ |
+
+ e.stopPropagation()}
+ >
+ {exec.routeId}
+
+ |
+
+ {exec.correlationId ?? '-'}
+ |
+
+
+ |
+
);
}
diff --git a/ui/src/pages/executions/use-search-params-sync.ts b/ui/src/pages/executions/use-search-params-sync.ts
new file mode 100644
index 00000000..ccc95b5c
--- /dev/null
+++ b/ui/src/pages/executions/use-search-params-sync.ts
@@ -0,0 +1,80 @@
+import { useEffect, useRef } from 'react';
+import { useExecutionSearch } from './use-execution-search';
+
+const DEFAULTS = {
+ status: 'COMPLETED,FAILED',
+ sortField: 'startTime',
+ sortDir: 'desc',
+ offset: '0',
+};
+
+/**
+ * Two-way sync between Zustand execution-search store and URL search params.
+ * - On mount: hydrates store from URL (if non-default values present).
+ * - On store change: serializes non-default state to URL via replaceState (no history pollution).
+ */
+export function useSearchParamsSync() {
+ const hydrated = useRef(false);
+
+ // Hydrate store from URL on mount
+ useEffect(() => {
+ const params = new URLSearchParams(window.location.search);
+ const store = useExecutionSearch.getState();
+
+ const status = params.get('status');
+ if (status) store.setStatus(status.split(','));
+
+ const text = params.get('text');
+ if (text) store.setText(text);
+
+ const routeId = params.get('routeId');
+ if (routeId) store.setRouteId(routeId);
+
+ const agentId = params.get('agentId');
+ if (agentId) store.setAgentId(agentId);
+
+ const sort = params.get('sort');
+ if (sort) {
+ const [field, dir] = sort.split(':');
+ if (field && dir) {
+ // Set sortField and sortDir directly via the store
+ useExecutionSearch.setState({
+ sortField: field as 'startTime' | 'status' | 'agentId' | 'routeId' | 'correlationId' | 'durationMs',
+ sortDir: dir as 'asc' | 'desc',
+ });
+ }
+ }
+
+ const offset = params.get('offset');
+ if (offset) store.setOffset(Number(offset));
+
+ hydrated.current = true;
+ }, []);
+
+ // Sync store → URL on changes
+ useEffect(() => {
+ const unsub = useExecutionSearch.subscribe((state) => {
+ if (!hydrated.current) return;
+
+ const params = new URLSearchParams();
+
+ const statusStr = state.status.join(',');
+ if (statusStr !== DEFAULTS.status) params.set('status', statusStr);
+
+ if (state.text) params.set('text', state.text);
+ if (state.routeId) params.set('routeId', state.routeId);
+ if (state.agentId) params.set('agentId', state.agentId);
+
+ const sortStr = `${state.sortField}:${state.sortDir}`;
+ if (sortStr !== `${DEFAULTS.sortField}:${DEFAULTS.sortDir}`) params.set('sort', sortStr);
+
+ if (state.offset > 0) params.set('offset', String(state.offset));
+
+ const qs = params.toString();
+ const newUrl = qs ? `${window.location.pathname}?${qs}` : window.location.pathname;
+ window.history.replaceState(null, '', newUrl);
+ });
+
+ return unsub;
+ }, []);
+}
diff --git a/ui/src/pages/routes/ExchangeTab.module.css b/ui/src/pages/routes/ExchangeTab.module.css
new file mode 100644
index 00000000..ce2bbe33
--- /dev/null
+++ b/ui/src/pages/routes/ExchangeTab.module.css
@@ -0,0 +1,86 @@
+.wrap {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ padding: 24px;
+ max-width: 720px;
+}
+
+.heading {
+ font-size: 11px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.8px;
+ color: var(--text-muted);
+ margin-bottom: 16px;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: 140px 1fr;
+ gap: 6px 16px;
+ font-size: 13px;
+ margin-bottom: 20px;
+}
+
+.key {
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.value {
+ font-family: var(--font-mono);
+ color: var(--text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ word-break: break-all;
+}
+
+.section {
+ margin-top: 16px;
+}
+
+.sectionLabel {
+ font-size: 10px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: var(--text-muted);
+ display: block;
+ margin-bottom: 8px;
+}
+
+.bodyPre {
+ background: var(--bg-base);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-sm);
+ padding: 12px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--text-secondary);
+ max-height: 300px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ margin: 0;
+}
+
+.errorPanel {
+ background: var(--rose-glow);
+ border: 1px solid rgba(244, 63, 94, 0.2);
+ border-radius: var(--radius-sm);
+ padding: 12px;
+ font-family: var(--font-mono);
+ font-size: 12px;
+ color: var(--rose);
+ max-height: 200px;
+ overflow: auto;
+}
+
+.loading,
+.empty {
+ color: var(--text-muted);
+ text-align: center;
+ padding: 60px 20px;
+ font-size: 14px;
+}
diff --git a/ui/src/pages/routes/ExchangeTab.tsx b/ui/src/pages/routes/ExchangeTab.tsx
new file mode 100644
index 00000000..4ac26075
--- /dev/null
+++ b/ui/src/pages/routes/ExchangeTab.tsx
@@ -0,0 +1,64 @@
+import { useExecutionDetail, useProcessorSnapshot } from '../../api/queries/executions';
+import styles from './ExchangeTab.module.css';
+
+interface ExchangeTabProps {
+ executionId: string;
+}
+
+export function ExchangeTab({ executionId }: ExchangeTabProps) {
+ const { data: execution, isLoading } = useExecutionDetail(executionId);
+ const { data: snapshot } = useProcessorSnapshot(executionId, 0);
+
+ const body = snapshot?.['body'];
+
+ if (isLoading) {
+ return Loading exchange details...
;
+ }
+
+ if (!execution) {
+ return Execution not found
;
+ }
+
+ return (
+
+
Exchange Details
+
+
+ - Execution ID
+ - {execution.executionId}
+
+ - Correlation ID
+ - {execution.correlationId ?? '-'}
+
+ - Application
+ - {execution.agentId}
+
+ - Route
+ - {execution.routeId}
+
+ - Timestamp
+ - {new Date(execution.startTime).toISOString()}
+
+ - Duration
+ - {execution.durationMs}ms
+
+ - Status
+ - {execution.status}
+
+
+ {body && (
+
+ )}
+
+ {execution.errorMessage && (
+
+
Error
+
{execution.errorMessage}
+
+ )}
+
+ );
+}
diff --git a/ui/src/pages/routes/RoutePage.tsx b/ui/src/pages/routes/RoutePage.tsx
index 999d4110..55327feb 100644
--- a/ui/src/pages/routes/RoutePage.tsx
+++ b/ui/src/pages/routes/RoutePage.tsx
@@ -7,10 +7,11 @@ import { RouteHeader } from './RouteHeader';
import { DiagramTab } from './DiagramTab';
import { PerformanceTab } from './PerformanceTab';
import { ProcessorTree } from '../executions/ProcessorTree';
+import { ExchangeTab } from './ExchangeTab';
import { ExecutionPicker } from './diagram/ExecutionPicker';
import styles from './RoutePage.module.css';
-type Tab = 'diagram' | 'performance' | 'processors';
+type Tab = 'diagram' | 'performance' | 'processors' | 'exchange';
export function RoutePage() {
const { group, routeId } = useParams<{ group: string; routeId: string }>();
@@ -53,6 +54,8 @@ export function RoutePage() {
return Missing group or routeId parameters
;
}
+ const needsExecPicker = activeTab === 'diagram' || activeTab === 'processors' || activeTab === 'exchange';
+
return (
<>
{/* Breadcrumb */}
@@ -89,22 +92,32 @@ export function RoutePage() {
>
Processor Tree
+
- {activeTab === 'diagram' && (
+ {needsExecPicker && (
-
- {execution && (
-
- {execution.status} · {execution.durationMs}ms
-
+ {activeTab === 'diagram' && (
+ <>
+
+ {execution && (
+
+ {execution.status} · {execution.durationMs}ms
+
+ )}
+ >
)}
)}
@@ -134,6 +147,16 @@ export function RoutePage() {
Select an execution to view the processor tree
)}
+
+ {activeTab === 'exchange' && execId && (
+
+ )}
+
+ {activeTab === 'exchange' && !execId && (
+
+ Select an execution to view exchange details
+
+ )}
>
);
}
diff --git a/ui/src/router.tsx b/ui/src/router.tsx
index faeeeb80..fd2a601a 100644
--- a/ui/src/router.tsx
+++ b/ui/src/router.tsx
@@ -6,6 +6,7 @@ import { OidcCallback } from './auth/OidcCallback';
import { ExecutionExplorer } from './pages/executions/ExecutionExplorer';
import { OidcAdminPage } from './pages/admin/OidcAdminPage';
import { RoutePage } from './pages/routes/RoutePage';
+import { ApplicationsPage } from './pages/apps/ApplicationsPage';
export const router = createBrowserRouter([
{
@@ -24,6 +25,7 @@ export const router = createBrowserRouter([
children: [
{ index: true, element: },
{ path: 'executions', element: },
+ { path: 'apps', element: },
{ path: 'apps/:group/routes/:routeId', element: },
{ path: 'admin/oidc', element: },
],