diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java index 44adb0d5..8e16fc79 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcAuthController.java @@ -16,6 +16,7 @@ import org.springframework.web.bind.annotation.RestController; import java.net.URI; import java.time.Instant; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -60,11 +61,15 @@ public class OidcAuthController { try { OidcConfig oidc = config.get(); - return ResponseEntity.ok(Map.of( - "issuer", oidc.issuerUri(), - "clientId", oidc.clientId(), - "authorizationEndpoint", tokenExchanger.getAuthorizationEndpoint() - )); + Map response = new LinkedHashMap<>(); + response.put("issuer", oidc.issuerUri()); + response.put("clientId", oidc.clientId()); + response.put("authorizationEndpoint", tokenExchanger.getAuthorizationEndpoint()); + String endSessionEndpoint = tokenExchanger.getEndSessionEndpoint(); + if (endSessionEndpoint != null) { + response.put("endSessionEndpoint", endSessionEndpoint); + } + return ResponseEntity.ok(response); } catch (Exception e) { log.error("Failed to retrieve OIDC provider metadata: {}", e.getMessage()); return ResponseEntity.internalServerError() diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java index f84995ca..3e99c298 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/security/OidcTokenExchanger.java @@ -114,6 +114,15 @@ public class OidcTokenExchanger { return getProviderMetadata(config.issuerUri()).getAuthorizationEndpointURI().toString(); } + /** + * Returns the provider's end-session (logout) endpoint, or {@code null} if not advertised. + */ + public String getEndSessionEndpoint() throws Exception { + OidcConfig config = getConfig(); + URI uri = getProviderMetadata(config.issuerUri()).getEndSessionEndpointURI(); + return uri != null ? uri.toString() : null; + } + /** * Invalidates cached provider metadata and JWKS processor. * Call after OIDC configuration is updated in the database. diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index 17f3c836..201f2f21 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1 +1,1639 @@ -{"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":"User Admin","description":"User management (ADMIN only)"},{"name":"Agent Management","description":"Agent registration and lifecycle endpoints"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"OIDC Config Admin","description":"OIDC provider configuration (ADMIN only)"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Search","description":"Transaction search endpoints"}],"paths":{"/api/v1/admin/users/{userId}/roles":{"put":{"tags":["User Admin"],"summary":"Update user roles","operationId":"updateRoles","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RolesRequest"}}},"required":true},"responses":{"200":{"description":"Roles updated"},"404":{"description":"User not found"}}}},"/api/v1/admin/oidc":{"get":{"tags":["OIDC Config Admin"],"summary":"Get OIDC configuration","operationId":"getConfig","responses":{"200":{"description":"Current OIDC configuration (client_secret masked)","content":{"*/*":{"schema":{"type":"object"}}}}}},"put":{"tags":["OIDC Config Admin"],"summary":"Save OIDC configuration","operationId":"saveConfig","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OidcConfigRequest"}}},"required":true},"responses":{"200":{"description":"Configuration saved","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["OIDC Config Admin"],"summary":"Delete OIDC configuration","operationId":"deleteConfig","responses":{"204":{"description":"Configuration deleted"}}}},"/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":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"processorType","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/oidc/callback":{"post":{"tags":["oidc-auth-controller"],"operationId":"callback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallbackRequest"}}},"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/admin/oidc/test":{"post":{"tags":["OIDC Config Admin"],"summary":"Test OIDC provider connectivity","operationId":"testConnection","responses":{"200":{"description":"Provider reachable","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Provider unreachable or misconfigured","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/api/v1/search/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count)","operationId":"stats","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ExecutionStats"}}}}}}},"/api/v1/search/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}},"/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/auth/oidc/config":{"get":{"tags":["oidc-auth-controller"],"operationId":"getConfig_1","responses":{"200":{"description":"OK","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"}}}}}}},"/api/v1/admin/users":{"get":{"tags":["User Admin"],"summary":"List all users","operationId":"listUsers","responses":{"200":{"description":"User list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserInfo"}}}}}}}},"/api/v1/admin/users/{userId}":{"get":{"tags":["User Admin"],"summary":"Get user by ID","operationId":"getUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserInfo"}}}}}},"delete":{"tags":["User Admin"],"summary":"Delete user","operationId":"deleteUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"User deleted"}}}}},"components":{"schemas":{"RolesRequest":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"}}}},"OidcConfigRequest":{"type":"object","properties":{"enabled":{"type":"boolean"},"issuerUri":{"type":"string"},"clientId":{"type":"string"},"clientSecret":{"type":"string"},"rolesClaim":{"type":"string"},"defaultRoles":{"type":"array","items":{"type":"string"}},"autoSignup":{"type":"boolean"}}},"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"},"routeId":{"type":"string"},"agentId":{"type":"string"},"processorType":{"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"}}},"CallbackRequest":{"type":"object","properties":{"code":{"type":"string"},"redirectUri":{"type":"string"}}},"LoginRequest":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"ExecutionStats":{"type":"object","properties":{"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"},"p99LatencyMs":{"type":"integer","format":"int64"},"activeCount":{"type":"integer","format":"int64"},"totalToday":{"type":"integer","format":"int64"},"prevTotalCount":{"type":"integer","format":"int64"},"prevFailedCount":{"type":"integer","format":"int64"},"prevAvgDurationMs":{"type":"integer","format":"int64"},"prevP99LatencyMs":{"type":"integer","format":"int64"}}},"StatsTimeseries":{"type":"object","properties":{"buckets":{"type":"array","items":{"$ref":"#/components/schemas/TimeseriesBucket"}}}},"TimeseriesBucket":{"type":"object","properties":{"time":{"type":"string","format":"date-time"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"},"p99DurationMs":{"type":"integer","format":"int64"},"activeCount":{"type":"integer","format":"int64"}}},"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"}}},"UserInfo":{"type":"object","properties":{"userId":{"type":"string"},"provider":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"roles":{"type":"array","items":{"type":"string"}},"createdAt":{"type":"string","format":"date-time"}}}}}} \ No newline at end of file +{ + "openapi": "3.1.0", + "info": { + "title": "OpenAPI definition", + "version": "v0" + }, + "servers": [], + "tags": [ + { + "name": "Agent SSE", + "description": "Server-Sent Events endpoint for agent communication" + }, + { + "name": "Agent Commands", + "description": "Command push endpoints for agent communication" + }, + { + "name": "User Admin", + "description": "User management (ADMIN only)" + }, + { + "name": "Agent Management", + "description": "Agent registration and lifecycle endpoints" + }, + { + "name": "Ingestion", + "description": "Data ingestion endpoints" + }, + { + "name": "OIDC Config Admin", + "description": "OIDC provider configuration (ADMIN only)" + }, + { + "name": "Diagrams", + "description": "Diagram rendering endpoints" + }, + { + "name": "Detail", + "description": "Execution detail and processor snapshot endpoints" + }, + { + "name": "Search", + "description": "Transaction search endpoints" + } + ], + "paths": { + "/admin/users/{userId}/roles": { + "put": { + "tags": [ + "User Admin" + ], + "summary": "Update user roles", + "operationId": "updateRoles", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RolesRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Roles updated" + }, + "404": { + "description": "User not found" + } + } + } + }, + "/admin/oidc": { + "get": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Get OIDC configuration", + "operationId": "getConfig", + "responses": { + "200": { + "description": "Current OIDC configuration (client_secret masked)", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "put": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Save OIDC configuration", + "operationId": "saveConfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OidcConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Configuration saved", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "description": "Invalid configuration", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + }, + "delete": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Delete OIDC configuration", + "operationId": "deleteConfig", + "responses": { + "204": { + "description": "Configuration deleted" + } + } + } + }, + "/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": "routeId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "agentId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "processorType", + "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" + } + } + } + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/auth/oidc/callback": { + "post": { + "tags": [ + "oidc-auth-controller" + ], + "operationId": "callback", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/admin/oidc/test": { + "post": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Test OIDC provider connectivity", + "operationId": "testConnection", + "responses": { + "200": { + "description": "Provider reachable", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + }, + "400": { + "description": "Provider unreachable or misconfigured", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/search/stats": { + "get": { + "tags": [ + "Search" + ], + "summary": "Aggregate execution stats (P99 latency, active count)", + "operationId": "stats", + "parameters": [ + { + "name": "from", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ExecutionStats" + } + } + } + } + } + } + }, + "/search/stats/timeseries": { + "get": { + "tags": [ + "Search" + ], + "summary": "Bucketed time-series stats over a time window", + "operationId": "timeseries", + "parameters": [ + { + "name": "from", + "in": "query", + "required": true, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "buckets", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 24 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/StatsTimeseries" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/auth/oidc/config": { + "get": { + "tags": [ + "oidc-auth-controller" + ], + "operationId": "getConfig_1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/admin/users": { + "get": { + "tags": [ + "User Admin" + ], + "summary": "List all users", + "operationId": "listUsers", + "responses": { + "200": { + "description": "User list returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserInfo" + } + } + } + } + } + } + } + }, + "/admin/users/{userId}": { + "get": { + "tags": [ + "User Admin" + ], + "summary": "Get user by ID", + "operationId": "getUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserInfo" + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserInfo" + } + } + } + } + } + }, + "delete": { + "tags": [ + "User Admin" + ], + "summary": "Delete user", + "operationId": "deleteUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User deleted" + } + } + } + } + }, + "components": { + "schemas": { + "RolesRequest": { + "type": "object", + "properties": { + "roles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "OidcConfigRequest": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "issuerUri": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecret": { + "type": "string" + }, + "rolesClaim": { + "type": "string" + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "autoSignup": { + "type": "boolean" + } + } + }, + "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" + }, + "routeId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "processorType": { + "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" + } + } + }, + "CallbackRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "redirectUri": { + "type": "string" + } + } + }, + "LoginRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "ExecutionStats": { + "type": "object", + "properties": { + "totalCount": { + "type": "integer", + "format": "int64" + }, + "failedCount": { + "type": "integer", + "format": "int64" + }, + "avgDurationMs": { + "type": "integer", + "format": "int64" + }, + "p99LatencyMs": { + "type": "integer", + "format": "int64" + }, + "activeCount": { + "type": "integer", + "format": "int64" + }, + "totalToday": { + "type": "integer", + "format": "int64" + }, + "prevTotalCount": { + "type": "integer", + "format": "int64" + }, + "prevFailedCount": { + "type": "integer", + "format": "int64" + }, + "prevAvgDurationMs": { + "type": "integer", + "format": "int64" + }, + "prevP99LatencyMs": { + "type": "integer", + "format": "int64" + } + } + }, + "StatsTimeseries": { + "type": "object", + "properties": { + "buckets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeseriesBucket" + } + } + } + }, + "TimeseriesBucket": { + "type": "object", + "properties": { + "time": { + "type": "string", + "format": "date-time" + }, + "totalCount": { + "type": "integer", + "format": "int64" + }, + "failedCount": { + "type": "integer", + "format": "int64" + }, + "avgDurationMs": { + "type": "integer", + "format": "int64" + }, + "p99DurationMs": { + "type": "integer", + "format": "int64" + }, + "activeCount": { + "type": "integer", + "format": "int64" + } + } + }, + "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" + } + } + }, + "UserInfo": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "email": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + } + } + } + } + } +} diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts index d54aa5af..e761a26f 100644 --- a/ui/src/api/queries/executions.ts +++ b/ui/src/api/queries/executions.ts @@ -1,6 +1,12 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; -import type { SearchRequest } from '../schema'; +import type { + SearchRequest, + ExecutionStats, + ExecutionSummary, + StatsTimeseries, + ExecutionDetail, +} from '../schema-types'; export function useExecutionStats(timeFrom: string | undefined, timeTo: string | undefined) { return useQuery({ @@ -15,7 +21,7 @@ export function useExecutionStats(timeFrom: string | undefined, timeTo: string | }, }); if (error) throw new Error('Failed to load stats'); - return data!; + return data as unknown as ExecutionStats; }, enabled: !!timeFrom, placeholderData: (prev) => prev, @@ -31,7 +37,7 @@ export function useSearchExecutions(filters: SearchRequest, live = false) { body: filters, }); if (error) throw new Error('Search failed'); - return data!; + return data as unknown as { data: ExecutionSummary[]; total: number; offset: number; limit: number }; }, placeholderData: (prev) => prev, refetchInterval: live ? 5_000 : false, @@ -52,7 +58,7 @@ export function useStatsTimeseries(timeFrom: string | undefined, timeTo: string }, }); if (error) throw new Error('Failed to load timeseries'); - return data!; + return data as unknown as StatsTimeseries; }, enabled: !!timeFrom, placeholderData: (prev) => prev, @@ -68,7 +74,7 @@ export function useExecutionDetail(executionId: string | null) { params: { path: { executionId: executionId! } }, }); if (error) throw new Error('Failed to load execution detail'); - return data!; + return data as unknown as ExecutionDetail; }, enabled: !!executionId, }); @@ -90,7 +96,7 @@ export function useProcessorSnapshot( }, ); if (error) throw new Error('Failed to load snapshot'); - return data!; + return data as unknown as Record; }, enabled: !!executionId && index !== null, }); diff --git a/ui/src/api/schema-types.ts b/ui/src/api/schema-types.ts new file mode 100644 index 00000000..327de35d --- /dev/null +++ b/ui/src/api/schema-types.ts @@ -0,0 +1,31 @@ +import type { components } from './schema'; + +type Require = { + [K in keyof T]-?: T[K] extends (infer U)[] + ? Require[] + : T[K] extends object | undefined + ? Require> + : NonNullable; +}; + +export type ExecutionSummary = Require; +export type SearchRequest = components['schemas']['SearchRequest']; +export type ExecutionDetail = Require; +export type ExecutionStats = Require; +export type StatsTimeseries = Require; +export type TimeseriesBucket = Require; +export type UserInfo = Require; + +export type ProcessorNode = Require & { + children?: ProcessorNode[]; +}; + +export interface AgentInstance { + id: string; + applicationName: string; + group: string; + status: string; + routeIds: string[]; + registeredAt: string; + lastHeartbeat: string; +} diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index edb9fea5..ffbc3bd2 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -4,7 +4,7 @@ */ export interface paths { - "/api/v1/admin/users/{userId}/roles": { + "/admin/users/{userId}/roles": { parameters: { query?: never; header?: never; @@ -21,7 +21,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/oidc": { + "/admin/oidc": { parameters: { query?: never; header?: never; @@ -40,7 +40,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/search/executions": { + "/search/executions": { parameters: { query?: never; header?: never; @@ -58,7 +58,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/data/metrics": { + "/data/metrics": { parameters: { query?: never; header?: never; @@ -78,7 +78,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/data/executions": { + "/data/executions": { parameters: { query?: never; header?: never; @@ -98,7 +98,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/data/diagrams": { + "/data/diagrams": { parameters: { query?: never; header?: never; @@ -118,7 +118,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/auth/refresh": { + "/auth/refresh": { parameters: { query?: never; header?: never; @@ -134,7 +134,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/auth/oidc/callback": { + "/auth/oidc/callback": { parameters: { query?: never; header?: never; @@ -150,7 +150,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/auth/login": { + "/auth/login": { parameters: { query?: never; header?: never; @@ -166,7 +166,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/{id}/refresh": { + "/agents/{id}/refresh": { parameters: { query?: never; header?: never; @@ -186,7 +186,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/{id}/heartbeat": { + "/agents/{id}/heartbeat": { parameters: { query?: never; header?: never; @@ -206,7 +206,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/{id}/commands": { + "/agents/{id}/commands": { parameters: { query?: never; header?: never; @@ -226,7 +226,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/{id}/commands/{commandId}/ack": { + "/agents/{id}/commands/{commandId}/ack": { parameters: { query?: never; header?: never; @@ -246,7 +246,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/register": { + "/agents/register": { parameters: { query?: never; header?: never; @@ -266,7 +266,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/groups/{group}/commands": { + "/agents/groups/{group}/commands": { parameters: { query?: never; header?: never; @@ -286,7 +286,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/commands": { + "/agents/commands": { parameters: { query?: never; header?: never; @@ -306,7 +306,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/oidc/test": { + "/admin/oidc/test": { parameters: { query?: never; header?: never; @@ -323,7 +323,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/search/stats": { + "/search/stats": { parameters: { query?: never; header?: never; @@ -340,7 +340,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/search/stats/timeseries": { + "/search/stats/timeseries": { parameters: { query?: never; header?: never; @@ -357,7 +357,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/executions/{executionId}": { + "/executions/{executionId}": { parameters: { query?: never; header?: never; @@ -374,7 +374,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/executions/{executionId}/processors/{index}/snapshot": { + "/executions/{executionId}/processors/{index}/snapshot": { parameters: { query?: never; header?: never; @@ -391,7 +391,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/diagrams/{contentHash}/render": { + "/diagrams/{contentHash}/render": { parameters: { query?: never; header?: never; @@ -411,7 +411,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/auth/oidc/config": { + "/auth/oidc/config": { parameters: { query?: never; header?: never; @@ -427,7 +427,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents": { + "/agents": { parameters: { query?: never; header?: never; @@ -447,7 +447,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/agents/{id}/events": { + "/agents/{id}/events": { parameters: { query?: never; header?: never; @@ -467,7 +467,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/users": { + "/admin/users": { parameters: { query?: never; header?: never; @@ -484,7 +484,7 @@ export interface paths { patch?: never; trace?: never; }; - "/api/v1/admin/users/{userId}": { + "/admin/users/{userId}": { parameters: { query?: never; header?: never; diff --git a/ui/src/auth/LoginPage.tsx b/ui/src/auth/LoginPage.tsx index 1c064c22..c45b240d 100644 --- a/ui/src/auth/LoginPage.tsx +++ b/ui/src/auth/LoginPage.tsx @@ -22,6 +22,9 @@ export function LoginPage() { .then((data) => { if (data?.authorizationEndpoint && data?.clientId) { setOidc({ clientId: data.clientId, authorizationEndpoint: data.authorizationEndpoint }); + if (data.endSessionEndpoint) { + localStorage.setItem('cameleer-oidc-end-session', data.endSessionEndpoint); + } } }) .catch(() => {}); diff --git a/ui/src/auth/auth-store.ts b/ui/src/auth/auth-store.ts index 294914b4..84cef41b 100644 --- a/ui/src/auth/auth-store.ts +++ b/ui/src/auth/auth-store.ts @@ -68,6 +68,7 @@ export const useAuthStore = create((set, get) => ({ throw new Error(body.message || 'Invalid credentials'); } const { accessToken, refreshToken } = await res.json(); + localStorage.removeItem('cameleer-oidc-end-session'); persistTokens(accessToken, refreshToken, username); set({ accessToken, @@ -143,7 +144,9 @@ export const useAuthStore = create((set, get) => ({ }, logout: () => { + const endSessionEndpoint = localStorage.getItem('cameleer-oidc-end-session'); clearTokens(); + localStorage.removeItem('cameleer-oidc-end-session'); set({ accessToken: null, refreshToken: null, @@ -152,5 +155,12 @@ export const useAuthStore = create((set, get) => ({ isAuthenticated: false, error: null, }); + if (endSessionEndpoint) { + const postLogoutRedirect = `${window.location.origin}/login`; + const params = new URLSearchParams({ + post_logout_redirect_uri: postLogoutRedirect, + }); + window.location.href = `${endSessionEndpoint}?${params}`; + } }, })); diff --git a/ui/src/components/command-palette/ResultItem.tsx b/ui/src/components/command-palette/ResultItem.tsx index 27c92447..94280043 100644 --- a/ui/src/components/command-palette/ResultItem.tsx +++ b/ui/src/components/command-palette/ResultItem.tsx @@ -1,4 +1,4 @@ -import type { ExecutionSummary, AgentInstance } from '../../api/schema'; +import type { ExecutionSummary, AgentInstance } from '../../api/schema-types'; import type { PaletteResult, RouteInfo } from './use-palette-search'; import { highlightMatch, formatRelativeTime } from './utils'; import { AppBadge } from '../shared/AppBadge'; @@ -93,7 +93,7 @@ function ApplicationResult({ data, query }: { data: AgentInstance; query: string
- {data.state} + {data.status}
group: {data.group} diff --git a/ui/src/components/command-palette/use-palette-search.ts b/ui/src/components/command-palette/use-palette-search.ts index 3bf062ec..c997b92f 100644 --- a/ui/src/components/command-palette/use-palette-search.ts +++ b/ui/src/components/command-palette/use-palette-search.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../../api/client'; -import type { ExecutionSummary, AgentInstance } from '../../api/schema'; +import type { ExecutionSummary, AgentInstance } from '../../api/schema-types'; import { useCommandPalette, type PaletteScope } from './use-command-palette'; import { useDebouncedValue } from './utils'; @@ -51,7 +51,7 @@ export function usePaletteSearch() { }, }); if (error) throw new Error('Search failed'); - return data!; + return data as unknown as { data: ExecutionSummary[]; total: number }; }, enabled: isOpen && isExecutionScope(scope), placeholderData: (prev) => prev, @@ -64,7 +64,7 @@ export function usePaletteSearch() { params: { query: {} }, }); if (error) throw new Error('Failed to load agents'); - return data!; + return data as unknown as AgentInstance[]; }, enabled: isOpen && (isApplicationScope(scope) || isRouteScope(scope)), staleTime: 30_000, diff --git a/ui/src/pages/executions/ExchangeDetail.tsx b/ui/src/pages/executions/ExchangeDetail.tsx index 833defd5..c7281c3c 100644 --- a/ui/src/pages/executions/ExchangeDetail.tsx +++ b/ui/src/pages/executions/ExchangeDetail.tsx @@ -1,5 +1,5 @@ import { useProcessorSnapshot } from '../../api/queries/executions'; -import type { ExecutionSummary } from '../../api/schema'; +import type { ExecutionSummary } from '../../api/schema-types'; import styles from './ExchangeDetail.module.css'; interface ExchangeDetailProps { diff --git a/ui/src/pages/executions/ProcessorTree.tsx b/ui/src/pages/executions/ProcessorTree.tsx index b9c1ff7a..bc962f18 100644 --- a/ui/src/pages/executions/ProcessorTree.tsx +++ b/ui/src/pages/executions/ProcessorTree.tsx @@ -1,5 +1,5 @@ import { useExecutionDetail } from '../../api/queries/executions'; -import type { ProcessorNode as ProcessorNodeType } from '../../api/schema'; +import type { ProcessorNode as ProcessorNodeType } from '../../api/schema-types'; import styles from './ProcessorTree.module.css'; const ICON_MAP: Record = { @@ -35,7 +35,7 @@ export function ProcessorTree({ executionId }: { executionId: string }) { return (

Processor Execution Tree

- {data.processors.map((proc, i) => ( + {(data.processors as ProcessorNodeType[])?.map((proc, i) => ( ))}
@@ -57,7 +57,7 @@ function ProcessorNodeView({ node }: { node: ProcessorNodeType }) { {node.durationMs}ms
- {node.children.length > 0 && ( + {node.children && node.children.length > 0 && (
{node.children.map((child, i) => ( diff --git a/ui/src/pages/executions/ResultsTable.tsx b/ui/src/pages/executions/ResultsTable.tsx index acaa3ec3..8171c7b6 100644 --- a/ui/src/pages/executions/ResultsTable.tsx +++ b/ui/src/pages/executions/ResultsTable.tsx @@ -1,5 +1,5 @@ import { useState, useMemo } from 'react'; -import type { ExecutionSummary } from '../../api/schema'; +import type { ExecutionSummary } from '../../api/schema-types'; import { StatusPill } from '../../components/shared/StatusPill'; import { DurationBar } from '../../components/shared/DurationBar'; import { AppBadge } from '../../components/shared/AppBadge'; diff --git a/ui/src/pages/executions/SearchFilters.tsx b/ui/src/pages/executions/SearchFilters.tsx index c992b7b4..ec45bd0e 100644 --- a/ui/src/pages/executions/SearchFilters.tsx +++ b/ui/src/pages/executions/SearchFilters.tsx @@ -7,7 +7,7 @@ import { ScopeTabs } from '../../components/command-palette/ScopeTabs'; import { ResultsList } from '../../components/command-palette/ResultsList'; import { PaletteFooter } from '../../components/command-palette/PaletteFooter'; import { FilterChip } from '../../components/shared/FilterChip'; -import type { ExecutionSummary, AgentInstance } from '../../api/schema'; +import type { ExecutionSummary, AgentInstance } from '../../api/schema-types'; import styles from './SearchFilters.module.css'; export function SearchFilters() { diff --git a/ui/src/pages/executions/use-execution-search.ts b/ui/src/pages/executions/use-execution-search.ts index 50614ccb..95e23d13 100644 --- a/ui/src/pages/executions/use-execution-search.ts +++ b/ui/src/pages/executions/use-execution-search.ts @@ -1,5 +1,5 @@ import { create } from 'zustand'; -import type { SearchRequest } from '../../api/schema'; +import type { SearchRequest } from '../../api/schema-types'; function todayMidnight(): string { const d = new Date(); @@ -94,8 +94,8 @@ export const useExecutionSearch = create((set, get) => ({ status: statusStr ?? undefined, timeFrom: s.timeFrom ? new Date(s.timeFrom).toISOString() : undefined, timeTo: s.timeTo ? new Date(s.timeTo).toISOString() : undefined, - durationMin: s.durationMin, - durationMax: s.durationMax, + durationMin: s.durationMin ?? undefined, + durationMax: s.durationMax ?? undefined, text: s.text || undefined, routeId: s.routeId || undefined, agentId: s.agentId || undefined,