diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json new file mode 100644 index 00000000..6cd08d82 --- /dev/null +++ b/ui/src/api/openapi.json @@ -0,0 +1,1071 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [ + { + "url": "http://192.168.50.86:30081", + "description": "Generated server url" + } + ], + "tags": [ + { + "name": "Agent SSE", + "description": "Server-Sent Events endpoint for agent communication" + }, + { + "name": "Agent Commands", + "description": "Command push endpoints for agent communication" + }, + { + "name": "Agent Management", + "description": "Agent registration and lifecycle endpoints" + }, + { + "name": "Ingestion", + "description": "Data ingestion endpoints" + }, + { + "name": "Diagrams", + "description": "Diagram rendering endpoints" + }, + { + "name": "Detail", + "description": "Execution detail and processor snapshot endpoints" + }, + { + "name": "Search", + "description": "Transaction search endpoints" + } + ], + "paths": { + "/api/v1/search/executions": { + "get": { + "tags": [ + "Search" + ], + "summary": "Search executions with basic filters", + "operationId": "searchGet", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "timeFrom", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "timeTo", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "correlationId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "text", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SearchResultExecutionSummary" + } + } + } + } + } + }, + "post": { + "tags": [ + "Search" + ], + "summary": "Advanced search with all filters", + "operationId": "searchPost", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/SearchResultExecutionSummary" + } + } + } + } + } + } + }, + "/api/v1/data/metrics": { + "post": { + "tags": [ + "Ingestion" + ], + "summary": "Ingest agent metrics", + "description": "Accepts an array of MetricsSnapshot objects", + "operationId": "ingestMetrics", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Data accepted for processing" + }, + "503": { + "description": "Buffer full, retry later" + } + } + } + }, + "/api/v1/data/executions": { + "post": { + "tags": [ + "Ingestion" + ], + "summary": "Ingest route execution data", + "description": "Accepts a single RouteExecution or an array of RouteExecutions", + "operationId": "ingestExecutions", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Data accepted for processing" + }, + "503": { + "description": "Buffer full, retry later" + } + } + } + }, + "/api/v1/data/diagrams": { + "post": { + "tags": [ + "Ingestion" + ], + "summary": "Ingest route diagram data", + "description": "Accepts a single RouteGraph or an array of RouteGraphs", + "operationId": "ingestDiagrams", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Data accepted for processing" + }, + "503": { + "description": "Buffer full, retry later" + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "tags": [ + "ui-auth-controller" + ], + "operationId": "refresh", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "tags": [ + "ui-auth-controller" + ], + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/api/v1/agents/{id}/refresh": { + "post": { + "tags": [ + "Agent Management" + ], + "summary": "Refresh access token", + "description": "Issues a new access JWT from a valid refresh token", + "operationId": "refresh_1", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "New access token issued", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Invalid or expired refresh token", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Agent not found", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/agents/{id}/heartbeat": { + "post": { + "tags": [ + "Agent Management" + ], + "summary": "Agent heartbeat ping", + "description": "Updates the agent's last heartbeat timestamp", + "operationId": "heartbeat", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Heartbeat accepted" + }, + "404": { + "description": "Agent not registered" + } + } + } + }, + "/api/v1/agents/{id}/commands": { + "post": { + "tags": [ + "Agent Commands" + ], + "summary": "Send command to a specific agent", + "description": "Sends a config-update, deep-trace, or replay command to the specified agent", + "operationId": "sendCommand", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Command accepted", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "404": { + "description": "Agent not registered", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/agents/{id}/commands/{commandId}/ack": { + "post": { + "tags": [ + "Agent Commands" + ], + "summary": "Acknowledge command receipt", + "description": "Agent acknowledges that it has received and processed a command", + "operationId": "acknowledgeCommand", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "commandId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Command acknowledged" + }, + "404": { + "description": "Command not found" + } + } + } + }, + "/api/v1/agents/register": { + "post": { + "tags": [ + "Agent Management" + ], + "summary": "Register an agent", + "description": "Registers a new agent or re-registers an existing one. Requires bootstrap token in Authorization header.", + "operationId": "register", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Agent registered successfully", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid registration payload", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "401": { + "description": "Missing or invalid bootstrap token", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/agents/groups/{group}/commands": { + "post": { + "tags": [ + "Agent Commands" + ], + "summary": "Send command to all agents in a group", + "description": "Sends a command to all LIVE agents in the specified group", + "operationId": "sendGroupCommand", + "parameters": [ + { + "name": "group", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Commands accepted", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/agents/commands": { + "post": { + "tags": [ + "Agent Commands" + ], + "summary": "Broadcast command to all live agents", + "description": "Sends a command to all agents currently in LIVE state", + "operationId": "broadcastCommand", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Commands accepted", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/executions/{executionId}": { + "get": { + "tags": [ + "Detail" + ], + "summary": "Get execution detail with nested processor tree", + "operationId": "getDetail", + "parameters": [ + { + "name": "executionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ExecutionDetail" + } + } + } + } + } + } + }, + "/api/v1/executions/{executionId}/processors/{index}/snapshot": { + "get": { + "tags": [ + "Detail" + ], + "summary": "Get exchange snapshot for a specific processor", + "operationId": "getProcessorSnapshot", + "parameters": [ + { + "name": "executionId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "index", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "/api/v1/diagrams/{contentHash}/render": { + "get": { + "tags": [ + "Diagrams" + ], + "summary": "Render a route diagram", + "description": "Returns SVG (default) or JSON layout based on Accept header", + "operationId": "renderDiagram", + "parameters": [ + { + "name": "contentHash", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Diagram rendered successfully", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + }, + "404": { + "description": "Diagram not found", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/api/v1/agents": { + "get": { + "tags": [ + "Agent Management" + ], + "summary": "List all agents", + "description": "Returns all registered agents, optionally filtered by status", + "operationId": "listAgents", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Agent list returned", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid status filter", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + }, + "/api/v1/agents/{id}/events": { + "get": { + "tags": [ + "Agent SSE" + ], + "summary": "Open SSE event stream", + "description": "Opens a Server-Sent Events stream for the specified agent. Commands (config-update, deep-trace, replay) are pushed as events. Ping keepalive comments sent every 15 seconds.", + "operationId": "events", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "Last-Event-ID", + "in": "header", + "description": "Last received event ID (no replay, acknowledged only)", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "SSE stream opened", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/SseEmitter" + } + } + } + }, + "404": { + "description": "Agent not registered", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/SseEmitter" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SearchRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "timeFrom": { + "type": "string", + "format": "date-time" + }, + "timeTo": { + "type": "string", + "format": "date-time" + }, + "durationMin": { + "type": "integer", + "format": "int64" + }, + "durationMax": { + "type": "integer", + "format": "int64" + }, + "correlationId": { + "type": "string" + }, + "text": { + "type": "string" + }, + "textInBody": { + "type": "string" + }, + "textInHeaders": { + "type": "string" + }, + "textInErrors": { + "type": "string" + }, + "offset": { + "type": "integer", + "format": "int32" + }, + "limit": { + "type": "integer", + "format": "int32" + } + } + }, + "ExecutionSummary": { + "type": "object", + "properties": { + "executionId": { + "type": "string" + }, + "routeId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "format": "int64" + }, + "correlationId": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "diagramContentHash": { + "type": "string" + } + } + }, + "SearchResultExecutionSummary": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExecutionSummary" + } + }, + "total": { + "type": "integer", + "format": "int64" + }, + "offset": { + "type": "integer", + "format": "int32" + }, + "limit": { + "type": "integer", + "format": "int32" + } + } + }, + "RefreshRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "LoginRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "ExecutionDetail": { + "type": "object", + "properties": { + "executionId": { + "type": "string" + }, + "routeId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "status": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "format": "int64" + }, + "correlationId": { + "type": "string" + }, + "exchangeId": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "errorStackTrace": { + "type": "string" + }, + "diagramContentHash": { + "type": "string" + }, + "processors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessorNode" + } + } + } + }, + "ProcessorNode": { + "type": "object", + "properties": { + "processorId": { + "type": "string" + }, + "processorType": { + "type": "string" + }, + "status": { + "type": "string" + }, + "startTime": { + "type": "string", + "format": "date-time" + }, + "endTime": { + "type": "string", + "format": "date-time" + }, + "durationMs": { + "type": "integer", + "format": "int64" + }, + "diagramNodeId": { + "type": "string" + }, + "errorMessage": { + "type": "string" + }, + "errorStackTrace": { + "type": "string" + } + } + }, + "SseEmitter": { + "type": "object", + "properties": { + "timeout": { + "type": "integer", + "format": "int64" + } + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 415e540d..6147c8b5 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -1,6 +1,6 @@ /** - * Hand-written OpenAPI types matching the cameleer3 server REST API. - * Will be replaced by openapi-typescript codegen once backend is running. + * Types matching the cameleer3 server REST API (validated against live OpenAPI spec). + * Generated from: GET /api/v1/api-docs */ export interface paths { @@ -88,7 +88,7 @@ export interface paths { responses: { 200: { content: { - 'application/json': ExchangeSnapshot; + 'application/json': ProcessorSnapshot; }; }; 404: { content: { 'application/json': { message: string } } }; @@ -137,45 +137,46 @@ export interface ExecutionSummary { executionId: string; routeId: string; agentId: string; - status: 'COMPLETED' | 'FAILED' | 'RUNNING'; + status: string; startTime: string; - duration: number; - processorCount: number; + endTime: string | null; + durationMs: number; correlationId: string | null; errorMessage: string | null; + diagramContentHash: string | null; } export interface ExecutionDetail { executionId: string; routeId: string; agentId: string; - status: 'COMPLETED' | 'FAILED' | 'RUNNING'; + status: string; startTime: string; - duration: number; + endTime: string | null; + durationMs: number; correlationId: string | null; + exchangeId: string | null; errorMessage: string | null; + errorStackTrace: string | null; + diagramContentHash: string | null; processors: ProcessorNode[]; } export interface ProcessorNode { - index: number; processorId: string; processorType: string; - uri: string | null; - status: 'COMPLETED' | 'FAILED' | 'RUNNING'; - duration: number; + status: string; + startTime: string; + endTime: string | null; + durationMs: number; + diagramNodeId: string | null; errorMessage: string | null; + errorStackTrace: string | null; children: ProcessorNode[]; } -export interface ExchangeSnapshot { - exchangeId: string; - correlationId: string | null; - bodyType: string | null; - body: string | null; - headers: Record | null; - properties: Record | null; -} +/** Processor snapshot is a flat key-value map (Map in Java) */ +export type ProcessorSnapshot = Record; export interface AgentInstance { agentId: string; diff --git a/ui/src/pages/executions/ExchangeDetail.tsx b/ui/src/pages/executions/ExchangeDetail.tsx index 33f3c6e1..833defd5 100644 --- a/ui/src/pages/executions/ExchangeDetail.tsx +++ b/ui/src/pages/executions/ExchangeDetail.tsx @@ -7,14 +7,16 @@ interface 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 const { data: snapshot } = useProcessorSnapshot(execution.executionId, 0); + const body = snapshot?.['body']; + return (

Exchange Details

-
Exchange ID
+
Execution ID
{execution.executionId}
Correlation
{execution.correlationId ?? '-'}
@@ -25,15 +27,13 @@ export function ExchangeDetail({ execution }: ExchangeDetailProps) {
Timestamp
{new Date(execution.startTime).toISOString()}
Duration
-
{execution.duration}ms
-
Processors
-
{execution.processorCount}
+
{execution.durationMs}ms
- {snapshot?.body && ( + {body && (
Input Body - {snapshot.body} + {body}
)} diff --git a/ui/src/pages/executions/ExecutionExplorer.tsx b/ui/src/pages/executions/ExecutionExplorer.tsx index 0ab6c8e3..496c6188 100644 --- a/ui/src/pages/executions/ExecutionExplorer.tsx +++ b/ui/src/pages/executions/ExecutionExplorer.tsx @@ -17,7 +17,7 @@ export function ExecutionExplorer() { // Derive stats from current search results const failedCount = results.filter((r) => r.status === 'FAILED').length; 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; const showFrom = total > 0 ? offset + 1 : 0; diff --git a/ui/src/pages/executions/ProcessorTree.tsx b/ui/src/pages/executions/ProcessorTree.tsx index aaea78c9..b9c1ff7a 100644 --- a/ui/src/pages/executions/ProcessorTree.tsx +++ b/ui/src/pages/executions/ProcessorTree.tsx @@ -35,8 +35,8 @@ export function ProcessorTree({ executionId }: { executionId: string }) { return (

Processor Execution Tree

- {data.processors.map((proc) => ( - + {data.processors.map((proc, i) => ( + ))}
); @@ -52,16 +52,15 @@ function ProcessorNodeView({ node }: { node: ProcessorNodeType }) {
{icon.label}
{node.processorType}
- {node.uri &&
{node.uri}
}
- {node.duration}ms + {node.durationMs}ms
{node.children.length > 0 && (
- {node.children.map((child) => ( - + {node.children.map((child, i) => ( + ))}
)} diff --git a/ui/src/pages/executions/ResultsTable.tsx b/ui/src/pages/executions/ResultsTable.tsx index 3423dc5b..f764f939 100644 --- a/ui/src/pages/executions/ResultsTable.tsx +++ b/ui/src/pages/executions/ResultsTable.tsx @@ -52,7 +52,6 @@ export function ResultsTable({ results, loading }: ResultsTableProps) { Route Correlation ID Duration - Processors @@ -101,13 +100,12 @@ function ResultRow({ {exec.correlationId ?? '-'} - + - {exec.processorCount} {isExpanded && ( - +