Fix UI types to match actual backend API
All checks were successful
CI / build (push) Successful in 1m0s
CI / docker (push) Successful in 45s
CI / deploy (push) Successful in 29s

Validated against live OpenAPI spec at /api/v1/api-docs. Fixes:
- duration → durationMs (all models)
- Remove processorCount (not in ExecutionSummary)
- Remove ProcessorNode.index and .uri (not in backend)
- ProcessorSnapshot is Record<string,string>, not structured object
- Add missing fields: endTime, diagramContentHash, exchangeId, etc.
- Save openapi.json from live server

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-13 14:23:56 +01:00
parent 1dfe53abee
commit 6f415cb017
6 changed files with 1107 additions and 38 deletions

1071
ui/src/api/openapi.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
/** /**
* Hand-written OpenAPI types matching the cameleer3 server REST API. * Types matching the cameleer3 server REST API (validated against live OpenAPI spec).
* Will be replaced by openapi-typescript codegen once backend is running. * Generated from: GET /api/v1/api-docs
*/ */
export interface paths { export interface paths {
@@ -88,7 +88,7 @@ export interface paths {
responses: { responses: {
200: { 200: {
content: { content: {
'application/json': ExchangeSnapshot; 'application/json': ProcessorSnapshot;
}; };
}; };
404: { content: { 'application/json': { message: string } } }; 404: { content: { 'application/json': { message: string } } };
@@ -137,45 +137,46 @@ export interface ExecutionSummary {
executionId: string; executionId: string;
routeId: string; routeId: string;
agentId: string; agentId: string;
status: 'COMPLETED' | 'FAILED' | 'RUNNING'; status: string;
startTime: string; startTime: string;
duration: number; endTime: string | null;
processorCount: number; durationMs: number;
correlationId: string | null; correlationId: string | null;
errorMessage: string | null; errorMessage: string | null;
diagramContentHash: string | null;
} }
export interface ExecutionDetail { export interface ExecutionDetail {
executionId: string; executionId: string;
routeId: string; routeId: string;
agentId: string; agentId: string;
status: 'COMPLETED' | 'FAILED' | 'RUNNING'; status: string;
startTime: string; startTime: string;
duration: number; endTime: string | null;
durationMs: number;
correlationId: string | null; correlationId: string | null;
exchangeId: string | null;
errorMessage: string | null; errorMessage: string | null;
errorStackTrace: string | null;
diagramContentHash: string | null;
processors: ProcessorNode[]; processors: ProcessorNode[];
} }
export interface ProcessorNode { export interface ProcessorNode {
index: number;
processorId: string; processorId: string;
processorType: string; processorType: string;
uri: string | null; status: string;
status: 'COMPLETED' | 'FAILED' | 'RUNNING'; startTime: string;
duration: number; endTime: string | null;
durationMs: number;
diagramNodeId: string | null;
errorMessage: string | null; errorMessage: string | null;
errorStackTrace: string | null;
children: ProcessorNode[]; children: ProcessorNode[];
} }
export interface ExchangeSnapshot { /** Processor snapshot is a flat key-value map (Map<String, String> in Java) */
exchangeId: string; export type ProcessorSnapshot = Record<string, string>;
correlationId: string | null;
bodyType: string | null;
body: string | null;
headers: Record<string, string> | null;
properties: Record<string, string> | null;
}
export interface AgentInstance { export interface AgentInstance {
agentId: string; agentId: string;

View File

@@ -7,14 +7,16 @@ interface ExchangeDetailProps {
} }
export function ExchangeDetail({ execution }: ExchangeDetailProps) { export function ExchangeDetail({ execution }: ExchangeDetailProps) {
// Fetch the first processor's snapshot (index 0) for body preview // Fetch the first processor's snapshot (index 0) — returns Record<string, string>
const { data: snapshot } = useProcessorSnapshot(execution.executionId, 0); const { data: snapshot } = useProcessorSnapshot(execution.executionId, 0);
const body = snapshot?.['body'];
return ( return (
<div className={styles.sidebar}> <div className={styles.sidebar}>
<h4 className={styles.title}>Exchange Details</h4> <h4 className={styles.title}>Exchange Details</h4>
<dl className={styles.kv}> <dl className={styles.kv}>
<dt className={styles.kvKey}>Exchange ID</dt> <dt className={styles.kvKey}>Execution ID</dt>
<dd className={styles.kvValue}>{execution.executionId}</dd> <dd className={styles.kvValue}>{execution.executionId}</dd>
<dt className={styles.kvKey}>Correlation</dt> <dt className={styles.kvKey}>Correlation</dt>
<dd className={styles.kvValue}>{execution.correlationId ?? '-'}</dd> <dd className={styles.kvValue}>{execution.correlationId ?? '-'}</dd>
@@ -25,15 +27,13 @@ export function ExchangeDetail({ execution }: ExchangeDetailProps) {
<dt className={styles.kvKey}>Timestamp</dt> <dt className={styles.kvKey}>Timestamp</dt>
<dd className={styles.kvValue}>{new Date(execution.startTime).toISOString()}</dd> <dd className={styles.kvValue}>{new Date(execution.startTime).toISOString()}</dd>
<dt className={styles.kvKey}>Duration</dt> <dt className={styles.kvKey}>Duration</dt>
<dd className={styles.kvValue}>{execution.duration}ms</dd> <dd className={styles.kvValue}>{execution.durationMs}ms</dd>
<dt className={styles.kvKey}>Processors</dt>
<dd className={styles.kvValue}>{execution.processorCount}</dd>
</dl> </dl>
{snapshot?.body && ( {body && (
<div className={styles.bodyPreview}> <div className={styles.bodyPreview}>
<span className={styles.bodyLabel}>Input Body</span> <span className={styles.bodyLabel}>Input Body</span>
{snapshot.body} {body}
</div> </div>
)} )}

View File

@@ -17,7 +17,7 @@ export function ExecutionExplorer() {
// Derive stats from current search results // Derive stats from current search results
const failedCount = results.filter((r) => r.status === 'FAILED').length; const failedCount = results.filter((r) => r.status === 'FAILED').length;
const avgDuration = results.length > 0 const avgDuration = results.length > 0
? Math.round(results.reduce((sum, r) => sum + r.duration, 0) / results.length) ? Math.round(results.reduce((sum, r) => sum + r.durationMs, 0) / results.length)
: 0; : 0;
const showFrom = total > 0 ? offset + 1 : 0; const showFrom = total > 0 ? offset + 1 : 0;

View File

@@ -35,8 +35,8 @@ export function ProcessorTree({ executionId }: { executionId: string }) {
return ( return (
<div className={styles.tree}> <div className={styles.tree}>
<h4 className={styles.title}>Processor Execution Tree</h4> <h4 className={styles.title}>Processor Execution Tree</h4>
{data.processors.map((proc) => ( {data.processors.map((proc, i) => (
<ProcessorNodeView key={proc.index} node={proc} /> <ProcessorNodeView key={proc.processorId ?? i} node={proc} />
))} ))}
</div> </div>
); );
@@ -52,16 +52,15 @@ function ProcessorNodeView({ node }: { node: ProcessorNodeType }) {
<div className={`${styles.procIcon} ${icon.className}`}>{icon.label}</div> <div className={`${styles.procIcon} ${icon.className}`}>{icon.label}</div>
<div className={styles.procInfo}> <div className={styles.procInfo}>
<div className={styles.procType}>{node.processorType}</div> <div className={styles.procType}>{node.processorType}</div>
{node.uri && <div className={styles.procUri}>{node.uri}</div>}
</div> </div>
<div className={styles.procTiming}> <div className={styles.procTiming}>
<span className={styles.procDuration}>{node.duration}ms</span> <span className={styles.procDuration}>{node.durationMs}ms</span>
</div> </div>
</div> </div>
{node.children.length > 0 && ( {node.children.length > 0 && (
<div className={styles.nested}> <div className={styles.nested}>
{node.children.map((child) => ( {node.children.map((child, i) => (
<ProcessorNodeView key={child.index} node={child} /> <ProcessorNodeView key={child.processorId ?? i} node={child} />
))} ))}
</div> </div>
)} )}

View File

@@ -52,7 +52,6 @@ export function ResultsTable({ results, loading }: ResultsTableProps) {
<th className={styles.th}>Route</th> <th className={styles.th}>Route</th>
<th className={styles.th}>Correlation ID</th> <th className={styles.th}>Correlation ID</th>
<th className={styles.th}>Duration</th> <th className={styles.th}>Duration</th>
<th className={styles.th}>Processors</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -101,13 +100,12 @@ function ResultRow({
{exec.correlationId ?? '-'} {exec.correlationId ?? '-'}
</td> </td>
<td className={styles.td}> <td className={styles.td}>
<DurationBar duration={exec.duration} /> <DurationBar duration={exec.durationMs} />
</td> </td>
<td className={`${styles.td} mono text-muted`}>{exec.processorCount}</td>
</tr> </tr>
{isExpanded && ( {isExpanded && (
<tr className={styles.detailRowVisible}> <tr className={styles.detailRowVisible}>
<td className={styles.detailCell} colSpan={8}> <td className={styles.detailCell} colSpan={7}>
<div className={styles.detailContent}> <div className={styles.detailContent}>
<ProcessorTree executionId={exec.executionId} /> <ProcessorTree executionId={exec.executionId} />
<ExchangeDetail execution={exec} /> <ExchangeDetail execution={exec} />