From ac32396a5748b196f3ee883e9d115b735d97570e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:55:29 +0100 Subject: [PATCH] feat: add interactive ProcessDiagram SVG component (sub-project 1/3) New interactive route diagram component with SVG rendering using server-computed ELK layout coordinates. TIBCO BW5-inspired top-bar card node style with zoom/pan, hover toolbars, config badges, and error handler sections below the main flow. Backend: add direction query parameter (LR/TB) to diagram render endpoints, defaulting to left-to-right layout. Frontend: 14-file ProcessDiagram component in ui/src/components/ with DiagramNode, CompoundNode, DiagramEdge, ConfigBadge, NodeToolbar, ErrorSection, ZoomControls, and supporting hooks. Dev test page at /dev/diagram for validation. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../controller/DiagramRenderController.java | 8 +- .../app/diagram/ElkDiagramRenderer.java | 14 +- .../server/core/diagram/DiagramRenderer.java | 10 + ...3-27-interactive-process-diagram-design.md | 335 + ui/src/api/openapi.json | 5403 ++++++++++++++++- ui/src/api/queries/diagrams.ts | 35 +- ui/src/api/schema.d.ts | 7 +- .../ProcessDiagram/CompoundNode.tsx | 82 + .../components/ProcessDiagram/ConfigBadge.tsx | 48 + .../components/ProcessDiagram/DiagramEdge.tsx | 49 + .../components/ProcessDiagram/DiagramNode.tsx | 93 + .../ProcessDiagram/ErrorSection.tsx | 93 + .../components/ProcessDiagram/NodeToolbar.tsx | 118 + .../ProcessDiagram/ProcessDiagram.module.css | 79 + .../ProcessDiagram/ProcessDiagram.tsx | 221 + .../ProcessDiagram/ZoomControls.tsx | 19 + ui/src/components/ProcessDiagram/index.ts | 2 + .../components/ProcessDiagram/node-colors.ts | 93 + ui/src/components/ProcessDiagram/types.ts | 27 + .../ProcessDiagram/useDiagramData.ts | 115 + .../components/ProcessDiagram/useZoomPan.ts | 171 + ui/src/pages/DevDiagram/DevDiagram.module.css | 131 + ui/src/pages/DevDiagram/DevDiagram.tsx | 127 + ui/src/router.tsx | 2 + 24 files changed, 7264 insertions(+), 18 deletions(-) create mode 100644 docs/superpowers/specs/2026-03-27-interactive-process-diagram-design.md create mode 100644 ui/src/components/ProcessDiagram/CompoundNode.tsx create mode 100644 ui/src/components/ProcessDiagram/ConfigBadge.tsx create mode 100644 ui/src/components/ProcessDiagram/DiagramEdge.tsx create mode 100644 ui/src/components/ProcessDiagram/DiagramNode.tsx create mode 100644 ui/src/components/ProcessDiagram/ErrorSection.tsx create mode 100644 ui/src/components/ProcessDiagram/NodeToolbar.tsx create mode 100644 ui/src/components/ProcessDiagram/ProcessDiagram.module.css create mode 100644 ui/src/components/ProcessDiagram/ProcessDiagram.tsx create mode 100644 ui/src/components/ProcessDiagram/ZoomControls.tsx create mode 100644 ui/src/components/ProcessDiagram/index.ts create mode 100644 ui/src/components/ProcessDiagram/node-colors.ts create mode 100644 ui/src/components/ProcessDiagram/types.ts create mode 100644 ui/src/components/ProcessDiagram/useDiagramData.ts create mode 100644 ui/src/components/ProcessDiagram/useZoomPan.ts create mode 100644 ui/src/pages/DevDiagram/DevDiagram.module.css create mode 100644 ui/src/pages/DevDiagram/DevDiagram.tsx diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DiagramRenderController.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DiagramRenderController.java index 41aeda19..4e74ec28 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DiagramRenderController.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/controller/DiagramRenderController.java @@ -62,6 +62,7 @@ public class DiagramRenderController { @ApiResponse(responseCode = "404", description = "Diagram not found") public ResponseEntity renderDiagram( @PathVariable String contentHash, + @RequestParam(defaultValue = "LR") String direction, HttpServletRequest request) { Optional graphOpt = diagramStore.findByContentHash(contentHash); @@ -76,7 +77,7 @@ public class DiagramRenderController { // without also accepting everything (*/*). This means "application/json" // must appear and wildcards must not dominate the preference. if (accept != null && isJsonPreferred(accept)) { - DiagramLayout layout = diagramRenderer.layoutJson(graph); + DiagramLayout layout = diagramRenderer.layoutJson(graph, direction); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(layout); @@ -96,7 +97,8 @@ public class DiagramRenderController { @ApiResponse(responseCode = "404", description = "No diagram found for the given application and route") public ResponseEntity findByApplicationAndRoute( @RequestParam String application, - @RequestParam String routeId) { + @RequestParam String routeId, + @RequestParam(defaultValue = "LR") String direction) { List agentIds = registryService.findByApplication(application).stream() .map(AgentInfo::id) .toList(); @@ -115,7 +117,7 @@ public class DiagramRenderController { return ResponseEntity.notFound().build(); } - DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get()); + DiagramLayout layout = diagramRenderer.layoutJson(graphOpt.get(), direction); return ResponseEntity.ok(layout); } diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index 127f956f..b4a40615 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -112,7 +112,7 @@ public class ElkDiagramRenderer implements DiagramRenderer { @Override public String renderSvg(RouteGraph graph) { - LayoutResult result = computeLayout(graph); + LayoutResult result = computeLayout(graph, Direction.DOWN); DiagramLayout layout = result.layout; int svgWidth = (int) Math.ceil(layout.width()) + 2 * PADDING; @@ -153,21 +153,27 @@ public class ElkDiagramRenderer implements DiagramRenderer { @Override public DiagramLayout layoutJson(RouteGraph graph) { - return computeLayout(graph).layout; + return computeLayout(graph, Direction.RIGHT).layout; + } + + @Override + public DiagramLayout layoutJson(RouteGraph graph, String direction) { + Direction dir = "TB".equalsIgnoreCase(direction) ? Direction.DOWN : Direction.RIGHT; + return computeLayout(graph, dir).layout; } // ---------------------------------------------------------------- // Layout computation // ---------------------------------------------------------------- - private LayoutResult computeLayout(RouteGraph graph) { + private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) { ElkGraphFactory factory = ElkGraphFactory.eINSTANCE; // Create root node ElkNode rootNode = factory.createElkNode(); rootNode.setIdentifier("root"); rootNode.setProperty(CoreOptions.ALGORITHM, "org.eclipse.elk.layered"); - rootNode.setProperty(CoreOptions.DIRECTION, Direction.DOWN); + rootNode.setProperty(CoreOptions.DIRECTION, rootDirection); rootNode.setProperty(CoreOptions.SPACING_NODE_NODE, NODE_SPACING); rootNode.setProperty(CoreOptions.SPACING_EDGE_NODE, EDGE_SPACING); rootNode.setProperty(CoreOptions.HIERARCHY_HANDLING, HierarchyHandling.INCLUDE_CHILDREN); diff --git a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/DiagramRenderer.java b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/DiagramRenderer.java index f8027464..540e39b7 100644 --- a/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/DiagramRenderer.java +++ b/cameleer3-server-core/src/main/java/com/cameleer3/server/core/diagram/DiagramRenderer.java @@ -19,4 +19,14 @@ public interface DiagramRenderer { * Compute a positioned JSON layout for the route graph. */ DiagramLayout layoutJson(RouteGraph graph); + + /** + * Compute a positioned JSON layout with a specific flow direction. + * + * @param graph the route graph + * @param direction "LR" for left-to-right, "TB" for top-to-bottom + */ + default DiagramLayout layoutJson(RouteGraph graph, String direction) { + return layoutJson(graph); + } } diff --git a/docs/superpowers/specs/2026-03-27-interactive-process-diagram-design.md b/docs/superpowers/specs/2026-03-27-interactive-process-diagram-design.md new file mode 100644 index 00000000..f833ff49 --- /dev/null +++ b/docs/superpowers/specs/2026-03-27-interactive-process-diagram-design.md @@ -0,0 +1,335 @@ +# Interactive Process Diagram — Design Spec + +**Sub-project:** 1 of 3 (Component → Execution Overlay → Page Integration) +**Scope:** Interactive SVG diagram component with zoom/pan, node interactions, config badges, and a configurable layout direction. Does NOT include execution overlay or page replacement — those are sub-projects 2 and 3. + +--- + +## Problem + +The current RouteFlow component renders Camel routes as a flat vertical list of nodes. It cannot show compound structures (choice branches, split fan-out, try-catch nesting), does not support zoom/pan, and has no interactive controls beyond click-to-select. Routes with 10+ processors become hard to follow, and the relationship between processors is not visually clear. + +## Goal + +Build an interactive process diagram component styled after MuleSoft / TIBCO BusinessWorks 5, rendering Camel routes as left-to-right flow diagrams using server-computed ELK layout coordinates. The component supports zoom/pan, node hover toolbars for tracing/tap configuration, config badge indicators, and a collapsible detail side-panel. + +--- + +## Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Rendering | SVG + custom React | Full control over styling, no heavy deps. Server owns layout. | +| Node style | Top-Bar Cards | TIBCO BW5-inspired white cards with colored top accent bar. Professional, clean. | +| Flow direction | Left-to-right (default) | Matches MuleSoft/BW5 conventions. Query param for flexibility. | +| Component location | `ui/src/components/ProcessDiagram/` | Tightly coupled to Cameleer data model, no design-system abstraction needed. | +| Interactions | Hover floating toolbar + click-to-select | Discoverable, no right-click dependency. | +| Error handlers | Below main flow | Clear visual separation, labeled divider. | +| Selection behavior | Side panel with config info; execution data only with overlay | Keeps base diagram focused on topology. | + +--- + +## 1. Backend: Layout Direction Parameter + +### Change + +Add optional `direction` query parameter to diagram render endpoints. + +### Files + +- `cameleer3-server-app/.../diagram/ElkDiagramRenderer.java` — accept direction param, map to ELK `Direction.RIGHT` (LR) or `Direction.DOWN` (TB) +- `cameleer3-server-core/.../diagram/DiagramRenderer.java` — update interface to accept direction +- `cameleer3-server-app/.../controller/DiagramRenderController.java` — add `@RequestParam(defaultValue = "LR") String direction` to render endpoints +- `ui/src/api/queries/diagrams.ts` — pass `direction` query param to API calls; also update `DiagramLayout` edge type to match backend `PositionedEdge` serialization: `{ sourceId, targetId, label?, points: number[][] }` (currently defines `{ from?, to? }` which is missing `points` and `label`) + +### Behavior + +- `GET /diagrams/{contentHash}/render?direction=LR` → left-to-right layout (default) +- `GET /diagrams/{contentHash}/render?direction=TB` → top-to-bottom layout +- `GET /diagrams?application=X&routeId=Y&direction=LR` → same for by-route endpoint + +### Compound Node Direction + +The direction parameter applies to the **root** layout only. Compound nodes (CHOICE, SPLIT, TRY_CATCH, etc.) keep their internal layout direction as **top-to-bottom** regardless of the root direction. This matches how MuleSoft/BW5 render branching patterns: the main flow goes left-to-right, but branches within a choice or split fan out vertically inside their container. + +--- + +## 2. Frontend: ProcessDiagram Component + +### File Structure + +``` +ui/src/components/ProcessDiagram/ +├── ProcessDiagram.tsx # Root: SVG container, zoom/pan, section layout +├── ProcessDiagram.module.css # Styles using design system tokens +├── DiagramNode.tsx # Individual node: top-bar card rendering +├── DiagramEdge.tsx # Edge: cubic Bezier path with arrowhead +├── CompoundNode.tsx # Container for compound types (choice, split) +├── NodeToolbar.tsx # Floating action toolbar on hover +├── ConfigBadge.tsx # Indicator badges (TRACE, TAP) on nodes +├── ErrorSection.tsx # Visual separator + error handler flow section +├── ZoomControls.tsx # HTML overlay: zoom in/out/fit buttons +├── useZoomPan.ts # Hook: viewBox transform, wheel zoom, drag pan +├── useDiagramData.ts # Hook: fetch + separate layout into sections +├── node-colors.ts # NodeType → design system color token mapping +├── types.ts # Shared TypeScript interfaces +└── index.ts # Public exports +``` + +### Props API + +```typescript +interface ProcessDiagramProps { + application: string; + routeId: string; + direction?: 'LR' | 'TB'; // default 'LR' + selectedNodeId?: string; // controlled selection + onNodeSelect?: (nodeId: string) => void; + onNodeAction?: (nodeId: string, action: NodeAction) => void; + nodeConfigs?: Map; // active taps/tracing per processor + className?: string; +} + +type NodeAction = 'inspect' | 'toggle-trace' | 'configure-tap' | 'copy-id'; + +interface NodeConfig { + traceEnabled?: boolean; + tapExpression?: string; +} + +// ExecutionOverlay types will be added in sub-project 2 when needed. +// No forward-declared types here to avoid drift. +``` + +### SVG Structure + +``` +
+ // zoom = viewBox transform + // arrowhead markers, filters + ... + + // pan offset transform + + + + // rendered first (behind nodes) + // cubic bezier from ELK waypoints + + + // ELK-computed position + + + + + // CompoundNode: dashed border container + + + + + + + + onException: java.lang.Exception + // divider + ... + ... + + + + +
...
// HTML overlay, bottom-right +
+``` + +--- + +## 3. Node Visual States + +### Base States + +| State | Visual | +|-------|--------| +| Normal | White card, `--border` (#E4DFD8), colored top bar per type | +| Hovered | Warm tint background (`--bg-hover` / #F5F0EA), stronger border, floating toolbar appears above | +| Selected | Amber selection ring (2.5px solid `--amber`), side panel opens | + +### Config Badges + +Small colored pill badges positioned at the top-right corner of the node card, always visible: +- **TRACE** — teal (`--running`) pill, shown when tracing is enabled +- **TAP** — purple (`--purple`) pill, shown when a tap expression is configured + +### Execution Overlay States (sub-project 2 — node must support these props) + +| State | Visual | +|-------|--------| +| Executed (OK) | Green left border or subtle green tint | +| Failed (caused error handler) | Red border (2px `--error`), red marker icon | +| Not executed | Dimmed (reduced opacity) | +| Has trace data | Small "data available" indicator icon | +| No trace data | No indicator (or grayed-out data icon) | + +### Node Type Colors + +| Category | Token | Hex | Types | +|----------|-------|-----|-------| +| Endpoints | `--running` | #1A7F8E teal | ENDPOINT | +| Processors | `--amber` | #C6820E | PROCESSOR, BEAN, LOG, SET_HEADER, SET_BODY, TRANSFORM, MARSHAL, UNMARSHAL | +| Targets | `--success` | #3D7C47 green | TO, TO_DYNAMIC, DIRECT, SEDA | +| EIP Patterns | `--purple` | #7C3AED | EIP_CHOICE, EIP_WHEN, EIP_OTHERWISE, EIP_SPLIT, EIP_MULTICAST, EIP_LOOP, EIP_AGGREGATE, EIP_FILTER, etc. | +| Error Handling | `--error` | #C0392B red | ERROR_HANDLER, ON_EXCEPTION, TRY_CATCH, DO_TRY, DO_CATCH, DO_FINALLY | +| Cross-Route | (hardcoded) | #06B6D4 cyan | EIP_WIRE_TAP, EIP_ENRICH, EIP_POLL_ENRICH | + +Note: This frontend color mapping intentionally differs from the backend `ElkDiagramRenderer` SVG colors (which use blue for endpoints, green for processors). The frontend uses design system tokens for consistency with the rest of the UI. The backend SVG renderer is not changed. + +### Compound Node Rendering + +Compound types (CHOICE, SPLIT, TRY_CATCH, LOOP, etc.) render as: +- Full-width colored header bar with white text label (type name) +- White body area with subtle border matching the type color +- Children rendered inside at their ELK-relative positions +- Children have their own hover/select/badge behavior + +--- + +## 4. Interactions + +### Hover Floating Toolbar + +On mouse enter over a node, a dark floating toolbar appears above the node (centered). Uses `` for HTML accessibility. + +| Icon | Action | Callback | +|------|--------|----------| +| Search | Inspect | `onNodeAction(id, 'inspect')` — selects node, opens side panel | +| T | Toggle Trace | `onNodeAction(id, 'toggle-trace')` — enables/disables tracing | +| Pencil | Configure Tap | `onNodeAction(id, 'configure-tap')` — opens tap config | +| ... | More | `onNodeAction(id, 'copy-id')` — copies processor ID | + +Toolbar hides on mouse leave after a short delay (150ms) to prevent flicker when moving between node and toolbar. + +### Click-to-Select + +Click on a node → calls `onNodeSelect(nodeId)`. Parent controls `selectedNodeId` prop. Selected node shows amber ring. + +### Zoom & Pan + +**`useZoomPan` hook manages:** +- Mouse wheel → zoom centered on cursor +- Click+drag on background → pan +- Pinch gesture → zoom (trackpad/touch) +- State: `{ scale, translateX, translateY }` +- Applied to SVG `viewBox` attribute + +**`ZoomControls` component:** +- Three buttons: `+` (zoom in), `−` (zoom out), fit-to-view icon +- Positioned as HTML overlay at bottom-right of diagram container +- Fit-to-view calculates viewBox to show entire diagram with 40px padding + +**Zoom limits:** 25% to 400%. + +### Keyboard Navigation + +**Required:** + +| Key | Action | +|-----|--------| +| Escape | Deselect / close panel | +| +/- | Zoom in/out | +| 0 | Fit to view | + +**Stretch (implement if time permits):** + +| Key | Action | +|-----|--------| +| Arrow keys | Move selection between connected nodes | +| Tab | Cycle through nodes in flow order | +| Enter | Open detail panel for selected node | + +--- + +## 5. Error Handler Sections + +Error handler compounds (ON_EXCEPTION, ERROR_HANDLER) render as separate sections below the main flow: + +1. **Divider:** Horizontal line with label text (e.g., "onException: java.lang.Exception") +2. **Gap:** 40px vertical gap between main section and error section +3. **Layout:** Error section gets its own ELK-computed layout (compound node children already have relative coordinates) +4. **Styling:** Same node rendering as main section, but the section background has a subtle red tint +5. **Multiple handlers:** Each ON_EXCEPTION becomes its own section, stacked vertically + +The `useDiagramData` hook separates top-level compound error nodes from regular nodes, computing the Y offset for each error section based on accumulated heights. + +--- + +## 6. Data Flow + +``` +useDiagramByRoute(app, routeId) + → contentHash + → useDiagramLayout(contentHash, direction) + → DiagramLayout { nodes[], edges[], width, height } + +useDiagramData hook: + 1. Separate nodes into mainNodes[] and errorSections[] + (reuses logic from buildFlowSegments: error-handler compounds with children → error sections) + 2. Filter edges: mainEdges (between main nodes), errorEdges (within each error section) + 3. Compute total SVG dimensions: max(mainWidth, errorWidths) × (mainHeight + gap + errorHeights) + 4. Return { mainNodes, mainEdges, errorSections, totalWidth, totalHeight } +``` + +The existing `diagram-mapping.ts` `buildFlowSegments` function handles the separation logic. The new `useDiagramData` hook adapts this for SVG coordinate-based rendering instead of RouteFlow's FlowSegment format. + +--- + +## 7. Side Panel (Detail Panel) + +When a node is selected, a collapsible side panel slides in from the right of the diagram container. + +**Base mode (no execution overlay):** +- Processor ID +- Processor type +- Endpoint URI (if applicable) +- Active configuration: tracing status, tap expression +- Node metadata from the diagram + +**With execution overlay (sub-project 2):** +- Execution status + duration +- Input/output body (if trace data captured) +- Input/output headers +- Error message + stack trace (if failed) +- Loop iteration selector (if inside a loop) + +For sub-project 1, the side panel shows config info only. The component accepts an `onNodeSelect` callback — the parent page controls what appears in the panel. + +The side panel is NOT part of the ProcessDiagram component itself. It is rendered by the parent page and controlled via the `selectedNodeId` / `onNodeSelect` props. This keeps the diagram component focused on visualization. + +**Dev test page (`/dev/diagram`):** In sub-project 1, the test page renders the ProcessDiagram with a simple stub side panel that shows the selected node's ID, type, label, and any `nodeConfigs` entry. This validates the selection interaction without needing full page integration. + +--- + +## 8. Non-Goals (Sub-project 2 & 3) + +These are explicitly out of scope for sub-project 1: + +- **Execution overlay rendering** — animated flow, per-node status/duration, dimming non-executed nodes +- **Loop/split iteration stepping** — "debugger" UI with iteration tabs +- **Page integration** — replacing RouteFlow on RouteDetail, ExchangeDetail, Dashboard +- **Minimap** — small overview for large diagrams (stretch goal, not v1) +- **Drag to rearrange** — nodes are server-positioned, not user-movable + +--- + +## Verification + +1. **Backend:** `mvn clean verify -DskipITs` passes after direction param addition +2. **Frontend types:** `npx tsc -p tsconfig.app.json --noEmit` passes +3. **Manual test:** Create a temporary test page or Storybook-like route (`/dev/diagram`) that renders the ProcessDiagram component with a known route +4. **Zoom/pan:** Mouse wheel zooms, drag pans, fit-to-view works +5. **Node interaction:** Hover shows toolbar, click selects with amber ring +6. **Config badges:** Pass mock `nodeConfigs` and verify TRACE/TAP pills render +7. **Error sections:** Route with ON_EXCEPTION renders error handler below main flow +8. **Compound nodes:** Route with CHOICE renders children inside dashed container +9. **Keyboard (required):** Escape deselects, +/- zooms, 0 fits to view +10. **Direction:** `?direction=TB` renders top-to-bottom layout diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index 2f1f050e..3fe82c87 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1 +1,5402 @@ -{"openapi":"3.1.0","info":{"title":"Cameleer3 Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Agent Events","description":"Agent lifecycle event log"},{"name":"Database Admin","description":"Database monitoring and management (ADMIN only)"},{"name":"Threshold Admin","description":"Monitoring threshold configuration (ADMIN only)"},{"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":"Authentication","description":"Login and token refresh endpoints"},{"name":"Role Admin","description":"Role management (ADMIN only)"},{"name":"RBAC Stats","description":"RBAC statistics (ADMIN only)"},{"name":"OIDC Config Admin","description":"OIDC provider configuration (ADMIN only)"},{"name":"Route Metrics","description":"Route performance metrics"},{"name":"Search","description":"Transaction search endpoints"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Application Logs","description":"Query application logs stored in OpenSearch"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"OpenSearch Admin","description":"OpenSearch monitoring and management (ADMIN only)"},{"name":"Application Config","description":"Per-application observability configuration"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Route Catalog","description":"Route catalog and discovery"}],"paths":{"/config/{application}":{"get":{"tags":["Application Config"],"summary":"Get application config","description":"Returns the current configuration for an application. Returns defaults if none stored.","operationId":"getConfig","parameters":[{"name":"application","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config","description":"Saves config and pushes CONFIG_UPDATE to all LIVE agents of this application","operationId":"updateConfig","parameters":[{"name":"application","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplicationConfig"}}},"required":true},"responses":{"200":{"description":"Config saved and pushed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}},"/admin/users/{userId}":{"get":{"tags":["User Admin"],"summary":"Get user by ID with RBAC detail","operationId":"getUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"404":{"description":"User not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}},"put":{"tags":["User Admin"],"summary":"Update user display name or email","operationId":"updateUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserRequest"}}},"required":true},"responses":{"200":{"description":"User updated"},"404":{"description":"User not found"}}},"delete":{"tags":["User Admin"],"summary":"Delete user","operationId":"deleteUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"User deleted"}}}},"/admin/thresholds":{"get":{"tags":["Threshold Admin"],"summary":"Get current threshold configuration","operationId":"getThresholds","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ThresholdConfig"}}}}}},"put":{"tags":["Threshold Admin"],"summary":"Update threshold configuration","operationId":"updateThresholds","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThresholdConfigRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ThresholdConfig"}}}}}}},"/admin/roles/{id}":{"get":{"tags":["Role Admin"],"summary":"Get role by ID with effective principals","operationId":"getRole","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Role found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDetail"}}}},"404":{"description":"Role not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RoleDetail"}}}}}},"put":{"tags":["Role Admin"],"summary":"Update a custom role","operationId":"updateRole","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role updated"},"403":{"description":"Cannot modify system role"},"404":{"description":"Role not found"}}},"delete":{"tags":["Role Admin"],"summary":"Delete a custom role","operationId":"deleteRole","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Role deleted"},"403":{"description":"Cannot delete system role"},"404":{"description":"Role not found"}}}},"/admin/oidc":{"get":{"tags":["OIDC Config Admin"],"summary":"Get OIDC configuration","operationId":"getConfig_1","responses":{"200":{"description":"Current OIDC configuration (client_secret masked)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OidcAdminConfigResponse"}}}}}},"put":{"tags":["OIDC Config Admin"],"summary":"Save OIDC configuration","operationId":"saveConfig","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OidcAdminConfigRequest"}}},"required":true},"responses":{"200":{"description":"Configuration saved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OidcAdminConfigResponse"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}},"delete":{"tags":["OIDC Config Admin"],"summary":"Delete OIDC configuration","operationId":"deleteConfig","responses":{"204":{"description":"Configuration deleted"}}}},"/admin/groups/{id}":{"get":{"tags":["Group Admin"],"summary":"Get group by ID with effective roles","operationId":"getGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Group found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GroupDetail"}}}},"404":{"description":"Group not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/GroupDetail"}}}}}},"put":{"tags":["Group Admin"],"summary":"Update group name or parent","operationId":"updateGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroupRequest"}}},"required":true},"responses":{"200":{"description":"Group updated"},"404":{"description":"Group not found"},"409":{"description":"Cycle detected in group hierarchy"}}},"delete":{"tags":["Group Admin"],"summary":"Delete group","operationId":"deleteGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Group deleted"},"404":{"description":"Group not found"}}}},"/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":"application","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}},{"name":"sortField","in":"query","required":false,"schema":{"type":"string"}},{"name":"sortDir","in":"query","required":false,"schema":{"type":"string"}}],"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"},"400":{"description":"Invalid payload"},"503":{"description":"Buffer full, retry later"}}}},"/data/logs":{"post":{"tags":["Ingestion"],"summary":"Ingest application log entries","description":"Accepts a batch of log entries from an agent. Entries are indexed in OpenSearch.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogBatch"}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/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"}}}},"/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"}}}},"/config/{application}/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent","operationId":"testExpression","parameters":[{"name":"application","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestExpressionRequest"}}},"required":true},"responses":{"200":{"description":"Expression evaluated successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"404":{"description":"No live agent available for this application","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/auth/refresh":{"post":{"tags":["Authentication"],"summary":"Refresh access token","operationId":"refresh","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshRequest"}}},"required":true},"responses":{"200":{"description":"Token refreshed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}},"401":{"description":"Invalid refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/auth/oidc/callback":{"post":{"tags":["Authentication"],"summary":"Exchange OIDC authorization code for JWTs","operationId":"callback","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CallbackRequest"}}},"required":true},"responses":{"200":{"description":"Authentication successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}},"401":{"description":"OIDC authentication failed","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"403":{"description":"Account not provisioned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"404":{"description":"OIDC not configured or disabled","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/auth/login":{"post":{"tags":["Authentication"],"summary":"Login with local credentials","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Login successful","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}},"401":{"description":"Invalid credentials","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/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":{"$ref":"#/components/schemas/AgentRefreshRequest"}}},"required":true},"responses":{"200":{"description":"New access token issued","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentRefreshResponse"}}}},"401":{"description":"Invalid or expired refresh token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentRefreshResponse"}}}},"404":{"description":"Agent not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentRefreshResponse"}}}}}}},"/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 command to the specified agent via SSE","operationId":"sendCommand","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"202":{"description":"Command accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandSingleResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandSingleResponse"}}}},"404":{"description":"Agent not registered","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandSingleResponse"}}}}}}},"/agents/{id}/commands/{commandId}/ack":{"post":{"tags":["Agent Commands"],"summary":"Acknowledge command receipt","description":"Agent acknowledges that it has received and processed a command, with result status and message","operationId":"acknowledgeCommand","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}},{"name":"commandId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandAckRequest"}}}},"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":{"$ref":"#/components/schemas/AgentRegistrationRequest"}}},"required":true},"responses":{"200":{"description":"Agent registered successfully","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentRegistrationResponse"}}}},"400":{"description":"Invalid registration payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}},"401":{"description":"Missing or invalid bootstrap token","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentRegistrationResponse"}}}}}}},"/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":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"202":{"description":"Commands accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandBroadcastResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandBroadcastResponse"}}}}}}},"/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":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"202":{"description":"Commands accepted","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandBroadcastResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandBroadcastResponse"}}}}}}},"/admin/users":{"get":{"tags":["User Admin"],"summary":"List all users with RBAC detail","operationId":"listUsers","responses":{"200":{"description":"User list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"post":{"tags":["User Admin"],"summary":"Create a local user","operationId":"createUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserRequest"}}},"required":true},"responses":{"200":{"description":"User created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/admin/users/{userId}/roles/{roleId}":{"post":{"tags":["User Admin"],"summary":"Assign a role to a user","operationId":"assignRoleToUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}},{"name":"roleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Role assigned"},"404":{"description":"User or role not found"}}},"delete":{"tags":["User Admin"],"summary":"Remove a role from a user","operationId":"removeRoleFromUser","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}},{"name":"roleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Role removed"}}}},"/admin/users/{userId}/password":{"post":{"tags":["User Admin"],"summary":"Reset user password","operationId":"resetPassword","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetPasswordRequest"}}},"required":true},"responses":{"204":{"description":"Password reset"}}}},"/admin/users/{userId}/groups/{groupId}":{"post":{"tags":["User Admin"],"summary":"Add a user to a group","operationId":"addUserToGroup","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"User added to group"}}},"delete":{"tags":["User Admin"],"summary":"Remove a user from a group","operationId":"removeUserFromGroup","parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}},{"name":"groupId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"User removed from group"}}}},"/admin/roles":{"get":{"tags":["Role Admin"],"summary":"List all roles (system and custom)","operationId":"listRoles","responses":{"200":{"description":"Role list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDetail"}}}}}}},"post":{"tags":["Role Admin"],"summary":"Create a custom role","operationId":"createRole","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleRequest"}}},"required":true},"responses":{"200":{"description":"Role created","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string","format":"uuid"}}}}}}}},"/admin/oidc/test":{"post":{"tags":["OIDC Config Admin"],"summary":"Test OIDC provider connectivity","operationId":"testConnection","responses":{"200":{"description":"Provider reachable","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OidcTestResult"}}}},"400":{"description":"Provider unreachable or misconfigured","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/admin/groups":{"get":{"tags":["Group Admin"],"summary":"List all groups with hierarchy and effective roles","operationId":"listGroups","responses":{"200":{"description":"Group list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/GroupDetail"}}}}}}},"post":{"tags":["Group Admin"],"summary":"Create a new group","operationId":"createGroup","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroupRequest"}}},"required":true},"responses":{"200":{"description":"Group created","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string","format":"uuid"}}}}}}}},"/admin/groups/{id}/roles/{roleId}":{"post":{"tags":["Group Admin"],"summary":"Assign a role to a group","operationId":"assignRoleToGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"roleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Role assigned to group"},"404":{"description":"Group not found"}}},"delete":{"tags":["Group Admin"],"summary":"Remove a role from a group","operationId":"removeRoleFromGroup","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"roleId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"204":{"description":"Role removed from group"},"404":{"description":"Group not found"}}}},"/admin/database/queries/{pid}/kill":{"post":{"tags":["Database Admin"],"summary":"Terminate a query by PID","operationId":"killQuery","parameters":[{"name":"pid","in":"path","required":true,"schema":{"type":"integer","format":"int32"}}],"responses":{"200":{"description":"OK"}}}},"/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"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"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}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}},"/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics","description":"Returns aggregated performance metrics per route for the given time window","operationId":"getMetrics","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"appId","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Metrics returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RouteMetrics"}}}}}}}},"/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"routeId","in":"query","required":true,"schema":{"type":"string"}},{"name":"appId","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"Metrics returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorMetrics"}}}}}}}},"/routes/catalog":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog","description":"Returns all applications with their routes, agents, and health status","operationId":"getCatalog","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppCatalogEntry"}}}}}}}},"/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries","description":"Returns log entries for a given application, optionally filtered by agent, level, time range, and text query","operationId":"searchLogs","parameters":[{"name":"application","in":"query","required":true,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":200}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntryResponse"}}}}}}}},"/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":"Execution detail found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ExecutionDetail"}}}},"404":{"description":"Execution not found","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":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/diagrams":{"get":{"tags":["Diagrams"],"summary":"Find diagram by application and route ID","description":"Resolves application to agent IDs and finds the latest diagram for the route","operationId":"findByApplicationAndRoute","parameters":[{"name":"application","in":"query","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found for the given application and route","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/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":{"image/svg+xml":{"schema":{"type":"string"}},"application/json":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"Diagram not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/config":{"get":{"tags":["Application Config"],"summary":"List all application configs","description":"Returns stored configurations for all applications","operationId":"listConfigs","responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/auth/oidc/config":{"get":{"tags":["Authentication"],"summary":"Get OIDC config for SPA login flow","operationId":"getConfig_2","responses":{"200":{"description":"OIDC configuration","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OidcPublicConfigResponse"}}}},"404":{"description":"OIDC not configured or disabled","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OidcPublicConfigResponse"}}}},"500":{"description":"Failed to retrieve OIDC provider metadata","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/agents":{"get":{"tags":["Agent Management"],"summary":"List all agents","description":"Returns all registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentInstanceResponse"}}}}},"400":{"description":"Invalid status filter","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}}}}}},"/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"}}}}}}},"/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"agentId","in":"path","required":true,"schema":{"type":"string"}},{"name":"names","in":"query","required":true,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"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":60}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/agents/events-log":{"get":{"tags":["Agent Events"],"summary":"Query agent events","description":"Returns agent lifecycle events, optionally filtered by app and/or agent ID","operationId":"getEvents","parameters":[{"name":"appId","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Events returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}}}}}}}},"/admin/rbac/stats":{"get":{"tags":["RBAC Stats"],"summary":"Get RBAC statistics for the dashboard","operationId":"getStats","responses":{"200":{"description":"RBAC stats returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RbacStats"}}}}}}},"/admin/opensearch/status":{"get":{"tags":["OpenSearch Admin"],"summary":"Get OpenSearch cluster status and version","operationId":"getStatus","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OpenSearchStatusResponse"}}}}}}},"/admin/opensearch/pipeline":{"get":{"tags":["OpenSearch Admin"],"summary":"Get indexing pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PipelineStatsResponse"}}}}}}},"/admin/opensearch/performance":{"get":{"tags":["OpenSearch Admin"],"summary":"Get OpenSearch performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/PerformanceResponse"}}}}}}},"/admin/opensearch/indices":{"get":{"tags":["OpenSearch Admin"],"summary":"Get OpenSearch indices with pagination","operationId":"getIndices","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":20}},{"name":"search","in":"query","required":false,"schema":{"type":"string","default":""}},{"name":"prefix","in":"query","required":false,"schema":{"type":"string","default":"executions"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndicesPageResponse"}}}}}}},"/admin/database/tables":{"get":{"tags":["Database Admin"],"summary":"Get table sizes and row counts","operationId":"getTables","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TableSizeResponse"}}}}}}}},"/admin/database/status":{"get":{"tags":["Database Admin"],"summary":"Get database connection status and version","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DatabaseStatusResponse"}}}}}}},"/admin/database/queries":{"get":{"tags":["Database Admin"],"summary":"Get active queries","operationId":"getQueries","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ActiveQueryResponse"}}}}}}}},"/admin/database/pool":{"get":{"tags":["Database Admin"],"summary":"Get HikariCP connection pool stats","operationId":"getPool","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ConnectionPoolResponse"}}}}}}},"/admin/database/metrics-pipeline":{"get":{"tags":["Database Admin"],"summary":"Get metrics ingestion pipeline diagnostics","operationId":"getMetricsPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}}}}}},"/admin/audit":{"get":{"tags":["Audit Log"],"summary":"Search audit log entries with pagination","operationId":"getAuditLog","parameters":[{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"category","in":"query","required":false,"schema":{"type":"string"}},{"name":"search","in":"query","required":false,"schema":{"type":"string"}},{"name":"from","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"timestamp"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"desc"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":0}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":25}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuditLogPageResponse"}}}}}}},"/admin/opensearch/indices/{name}":{"delete":{"tags":["OpenSearch Admin"],"summary":"Delete an OpenSearch index","operationId":"deleteIndex","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}}},"components":{"schemas":{"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"version":{"type":"integer","format":"int32"},"updatedAt":{"type":"string","format":"date-time"},"engineLevel":{"type":"string"},"payloadCaptureMode":{"type":"string"},"metricsEnabled":{"type":"boolean"},"samplingRate":{"type":"number","format":"double"},"tracedProcessors":{"type":"object","additionalProperties":{"type":"string"}},"logForwardingLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"}}},"TapDefinition":{"type":"object","properties":{"tapId":{"type":"string"},"processorId":{"type":"string"},"target":{"type":"string"},"expression":{"type":"string"},"language":{"type":"string"},"attributeName":{"type":"string"},"attributeType":{"type":"string"},"enabled":{"type":"boolean"},"version":{"type":"integer","format":"int32"}}},"UpdateUserRequest":{"type":"object","properties":{"displayName":{"type":"string"},"email":{"type":"string"}}},"DatabaseThresholdsRequest":{"type":"object","description":"Database monitoring thresholds","properties":{"connectionPoolWarning":{"type":"integer","format":"int32","description":"Connection pool usage warning threshold (percentage)","maximum":100,"minimum":0},"connectionPoolCritical":{"type":"integer","format":"int32","description":"Connection pool usage critical threshold (percentage)","maximum":100,"minimum":0},"queryDurationWarning":{"type":"number","format":"double","description":"Query duration warning threshold (seconds)"},"queryDurationCritical":{"type":"number","format":"double","description":"Query duration critical threshold (seconds)"}}},"OpenSearchThresholdsRequest":{"type":"object","description":"OpenSearch monitoring thresholds","properties":{"clusterHealthWarning":{"type":"string","description":"Cluster health warning threshold (GREEN, YELLOW, RED)","minLength":1},"clusterHealthCritical":{"type":"string","description":"Cluster health critical threshold (GREEN, YELLOW, RED)","minLength":1},"queueDepthWarning":{"type":"integer","format":"int32","description":"Queue depth warning threshold","minimum":0},"queueDepthCritical":{"type":"integer","format":"int32","description":"Queue depth critical threshold","minimum":0},"jvmHeapWarning":{"type":"integer","format":"int32","description":"JVM heap usage warning threshold (percentage)","maximum":100,"minimum":0},"jvmHeapCritical":{"type":"integer","format":"int32","description":"JVM heap usage critical threshold (percentage)","maximum":100,"minimum":0},"failedDocsWarning":{"type":"integer","format":"int32","description":"Failed document count warning threshold","minimum":0},"failedDocsCritical":{"type":"integer","format":"int32","description":"Failed document count critical threshold","minimum":0}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"},"opensearch":{"$ref":"#/components/schemas/OpenSearchThresholdsRequest"}},"required":["database","opensearch"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"OpenSearchThresholds":{"type":"object","properties":{"clusterHealthWarning":{"type":"string"},"clusterHealthCritical":{"type":"string"},"queueDepthWarning":{"type":"integer","format":"int32"},"queueDepthCritical":{"type":"integer","format":"int32"},"jvmHeapWarning":{"type":"integer","format":"int32"},"jvmHeapCritical":{"type":"integer","format":"int32"},"failedDocsWarning":{"type":"integer","format":"int32"},"failedDocsCritical":{"type":"integer","format":"int32"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"},"opensearch":{"$ref":"#/components/schemas/OpenSearchThresholds"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OidcAdminConfigRequest":{"type":"object","description":"OIDC configuration update request","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"},"displayNameClaim":{"type":"string"}}},"ErrorResponse":{"type":"object","description":"Error response","properties":{"message":{"type":"string"}},"required":["message"]},"OidcAdminConfigResponse":{"type":"object","description":"OIDC configuration for admin management","properties":{"configured":{"type":"boolean"},"enabled":{"type":"boolean"},"issuerUri":{"type":"string"},"clientId":{"type":"string"},"clientSecretSet":{"type":"boolean"},"rolesClaim":{"type":"string"},"defaultRoles":{"type":"array","items":{"type":"string"}},"autoSignup":{"type":"boolean"},"displayNameClaim":{"type":"string"}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"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"},"application":{"type":"string"},"agentIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"applicationName":{"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"},"highlight":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}},"required":["agentId","applicationName","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","highlight","routeId","startTime","status"]},"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"}},"required":["data","limit","offset","total"]},"LogBatch":{"type":"object","properties":{"entries":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"LogEntry":{"type":"object","properties":{"timestamp":{"type":"string","format":"date-time"},"level":{"type":"string"},"loggerName":{"type":"string"},"message":{"type":"string"},"threadName":{"type":"string"},"stackTrace":{"type":"string"},"mdc":{"type":"object","additionalProperties":{"type":"string"}}}},"TestExpressionRequest":{"type":"object","properties":{"expression":{"type":"string"},"language":{"type":"string"},"body":{"type":"string"},"target":{"type":"string"}}},"TestExpressionResponse":{"type":"object","properties":{"result":{"type":"string"},"error":{"type":"string"}}},"RefreshRequest":{"type":"object","properties":{"refreshToken":{"type":"string"}}},"AuthTokenResponse":{"type":"object","description":"JWT token pair","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"},"displayName":{"type":"string"},"idToken":{"type":"string","description":"OIDC id_token for end-session logout (only present after OIDC login)"}},"required":["accessToken","displayName","refreshToken"]},"CallbackRequest":{"type":"object","properties":{"code":{"type":"string"},"redirectUri":{"type":"string"}}},"LoginRequest":{"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AgentRefreshRequest":{"type":"object","description":"Agent token refresh request","properties":{"refreshToken":{"type":"string"}},"required":["refreshToken"]},"AgentRefreshResponse":{"type":"object","description":"Refreshed access and refresh tokens","properties":{"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","refreshToken"]},"CommandRequest":{"type":"object","description":"Command to send to agent(s)","properties":{"type":{"type":"string","description":"Command type: config-update, deep-trace, or replay"},"payload":{"type":"object","description":"Command payload JSON"}},"required":["type"]},"CommandSingleResponse":{"type":"object","description":"Result of sending a command to a single agent","properties":{"commandId":{"type":"string"},"status":{"type":"string"}},"required":["commandId","status"]},"CommandAckRequest":{"type":"object","properties":{"status":{"type":"string"},"message":{"type":"string"},"data":{"type":"string"}}},"AgentRegistrationRequest":{"type":"object","description":"Agent registration payload","properties":{"agentId":{"type":"string"},"name":{"type":"string"},"application":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["agentId","name"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"agentId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","agentId","refreshToken","serverPublicKey","sseEndpoint"]},"CommandBroadcastResponse":{"type":"object","description":"Result of broadcasting a command to multiple agents","properties":{"commandIds":{"type":"array","items":{"type":"string"}},"targetCount":{"type":"integer","format":"int32"}},"required":["commandIds"]},"CreateUserRequest":{"type":"object","properties":{"username":{"type":"string"},"displayName":{"type":"string"},"email":{"type":"string"},"password":{"type":"string"}}},"GroupSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}}},"RoleSummary":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"system":{"type":"boolean"},"source":{"type":"string"}}},"UserDetail":{"type":"object","properties":{"userId":{"type":"string"},"provider":{"type":"string"},"email":{"type":"string"},"displayName":{"type":"string"},"createdAt":{"type":"string","format":"date-time"},"directRoles":{"type":"array","items":{"$ref":"#/components/schemas/RoleSummary"}},"directGroups":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}},"effectiveRoles":{"type":"array","items":{"$ref":"#/components/schemas/RoleSummary"}},"effectiveGroups":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"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"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","totalCount","totalToday"]},"StatsTimeseries":{"type":"object","properties":{"buckets":{"type":"array","items":{"$ref":"#/components/schemas/TimeseriesBucket"}}},"required":["buckets"]},"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"}},"required":["activeCount","avgDurationMs","failedCount","p99DurationMs","time","totalCount"]},"RouteMetrics":{"type":"object","description":"Aggregated route performance metrics","properties":{"routeId":{"type":"string"},"appId":{"type":"string"},"exchangeCount":{"type":"integer","format":"int64"},"successRate":{"type":"number","format":"double"},"avgDurationMs":{"type":"number","format":"double"},"p99DurationMs":{"type":"number","format":"double"},"errorRate":{"type":"number","format":"double"},"throughputPerSec":{"type":"number","format":"double"},"sparkline":{"type":"array","items":{"type":"number","format":"double"}}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","sparkline","successRate","throughputPerSec"]},"ProcessorMetrics":{"type":"object","properties":{"processorId":{"type":"string"},"processorType":{"type":"string"},"routeId":{"type":"string"},"appId":{"type":"string"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"number","format":"double"},"p99DurationMs":{"type":"number","format":"double"},"errorRate":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","failedCount","p99DurationMs","processorId","processorType","routeId","totalCount"]},"AgentSummary":{"type":"object","description":"Summary of an agent instance for sidebar display","properties":{"id":{"type":"string"},"name":{"type":"string"},"status":{"type":"string"},"tps":{"type":"number","format":"double"}},"required":["id","name","status","tps"]},"AppCatalogEntry":{"type":"object","description":"Application catalog entry with routes and agents","properties":{"appId":{"type":"string"},"routes":{"type":"array","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","items":{"$ref":"#/components/schemas/AgentSummary"}},"agentCount":{"type":"integer","format":"int32"},"health":{"type":"string"},"exchangeCount":{"type":"integer","format":"int64"}},"required":["agentCount","agents","appId","exchangeCount","health","routes"]},"RouteSummary":{"type":"object","description":"Summary of a route within an application","properties":{"routeId":{"type":"string"},"exchangeCount":{"type":"integer","format":"int64"},"lastSeen":{"type":"string","format":"date-time"}},"required":["exchangeCount","lastSeen","routeId"]},"LogEntryResponse":{"type":"object","description":"Application log entry from OpenSearch","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG)"},"loggerName":{"type":"string","description":"Logger name"},"message":{"type":"string","description":"Log message"},"threadName":{"type":"string","description":"Thread name"},"stackTrace":{"type":"string","description":"Stack trace (if present)"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"applicationName":{"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"}},"inputBody":{"type":"string"},"outputBody":{"type":"string"},"inputHeaders":{"type":"string"},"outputHeaders":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}},"required":["agentId","applicationName","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","errorStackTrace","exchangeId","executionId","inputBody","inputHeaders","outputBody","outputHeaders","processors","routeId","startTime","status"]},"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"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","diagramNodeId","durationMs","endTime","errorMessage","errorStackTrace","processorId","processorType","startTime","status"]},"DiagramLayout":{"type":"object","properties":{"width":{"type":"number","format":"double"},"height":{"type":"number","format":"double"},"nodes":{"type":"array","items":{"$ref":"#/components/schemas/PositionedNode"}},"edges":{"type":"array","items":{"$ref":"#/components/schemas/PositionedEdge"}}}},"PositionedEdge":{"type":"object","properties":{"sourceId":{"type":"string"},"targetId":{"type":"string"},"label":{"type":"string"},"points":{"type":"array","items":{"type":"array","items":{"type":"number","format":"double"}}}}},"PositionedNode":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"type":{"type":"string"},"x":{"type":"number","format":"double"},"y":{"type":"number","format":"double"},"width":{"type":"number","format":"double"},"height":{"type":"number","format":"double"}}},"OidcPublicConfigResponse":{"type":"object","description":"OIDC configuration for SPA login flow","properties":{"issuer":{"type":"string"},"clientId":{"type":"string"},"authorizationEndpoint":{"type":"string"},"endSessionEndpoint":{"type":"string","description":"Present if the provider supports RP-initiated logout"}},"required":["authorizationEndpoint","clientId","issuer"]},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"id":{"type":"string"},"name":{"type":"string"},"application":{"type":"string"},"status":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"registeredAt":{"type":"string","format":"date-time"},"lastHeartbeat":{"type":"string","format":"date-time"},"version":{"type":"string"},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"tps":{"type":"number","format":"double"},"errorRate":{"type":"number","format":"double"},"activeRoutes":{"type":"integer","format":"int32"},"totalRoutes":{"type":"integer","format":"int32"},"uptimeSeconds":{"type":"integer","format":"int64"}},"required":["activeRoutes","application","capabilities","errorRate","id","lastHeartbeat","name","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"AgentMetricsResponse":{"type":"object","properties":{"metrics":{"type":"object","additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/MetricBucket"}}}},"required":["metrics"]},"MetricBucket":{"type":"object","properties":{"time":{"type":"string","format":"date-time"},"value":{"type":"number","format":"double"}},"required":["time","value"]},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"agentId":{"type":"string"},"appId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["agentId","appId","detail","eventType","id","timestamp"]},"RoleDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"},"system":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time"},"assignedGroups":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}},"directUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserSummary"}},"effectivePrincipals":{"type":"array","items":{"$ref":"#/components/schemas/UserSummary"}}}},"UserSummary":{"type":"object","properties":{"userId":{"type":"string"},"displayName":{"type":"string"},"provider":{"type":"string"}}},"RbacStats":{"type":"object","properties":{"userCount":{"type":"integer","format":"int32"},"activeUserCount":{"type":"integer","format":"int32"},"groupCount":{"type":"integer","format":"int32"},"maxGroupDepth":{"type":"integer","format":"int32"},"roleCount":{"type":"integer","format":"int32"}}},"OpenSearchStatusResponse":{"type":"object","description":"OpenSearch cluster status","properties":{"reachable":{"type":"boolean","description":"Whether the cluster is reachable"},"clusterHealth":{"type":"string","description":"Cluster health status (GREEN, YELLOW, RED)"},"version":{"type":"string","description":"OpenSearch version"},"nodeCount":{"type":"integer","format":"int32","description":"Number of nodes in the cluster"},"host":{"type":"string","description":"OpenSearch host"}}},"PipelineStatsResponse":{"type":"object","description":"Search indexing pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32","description":"Current queue depth"},"maxQueueSize":{"type":"integer","format":"int32","description":"Maximum queue size"},"failedCount":{"type":"integer","format":"int64","description":"Number of failed indexing operations"},"indexedCount":{"type":"integer","format":"int64","description":"Number of successfully indexed documents"},"debounceMs":{"type":"integer","format":"int64","description":"Debounce interval in milliseconds"},"indexingRate":{"type":"number","format":"double","description":"Current indexing rate (docs/sec)"},"lastIndexedAt":{"type":"string","format":"date-time","description":"Timestamp of last indexed document"}}},"PerformanceResponse":{"type":"object","description":"OpenSearch performance metrics","properties":{"queryCacheHitRate":{"type":"number","format":"double","description":"Query cache hit rate (0.0-1.0)"},"requestCacheHitRate":{"type":"number","format":"double","description":"Request cache hit rate (0.0-1.0)"},"searchLatencyMs":{"type":"number","format":"double","description":"Average search latency in milliseconds"},"indexingLatencyMs":{"type":"number","format":"double","description":"Average indexing latency in milliseconds"},"jvmHeapUsedBytes":{"type":"integer","format":"int64","description":"JVM heap used in bytes"},"jvmHeapMaxBytes":{"type":"integer","format":"int64","description":"JVM heap max in bytes"}}},"IndexInfoResponse":{"type":"object","description":"OpenSearch index information","properties":{"name":{"type":"string","description":"Index name"},"docCount":{"type":"integer","format":"int64","description":"Document count"},"size":{"type":"string","description":"Human-readable index size"},"sizeBytes":{"type":"integer","format":"int64","description":"Index size in bytes"},"health":{"type":"string","description":"Index health status"},"primaryShards":{"type":"integer","format":"int32","description":"Number of primary shards"},"replicaShards":{"type":"integer","format":"int32","description":"Number of replica shards"}}},"IndicesPageResponse":{"type":"object","description":"Paginated list of OpenSearch indices","properties":{"indices":{"type":"array","description":"Index list for current page","items":{"$ref":"#/components/schemas/IndexInfoResponse"}},"totalIndices":{"type":"integer","format":"int64","description":"Total number of indices"},"totalDocs":{"type":"integer","format":"int64","description":"Total document count across all indices"},"totalSize":{"type":"string","description":"Human-readable total size"},"page":{"type":"integer","format":"int32","description":"Current page number (0-based)"},"pageSize":{"type":"integer","format":"int32","description":"Page size"},"totalPages":{"type":"integer","format":"int32","description":"Total number of pages"}}},"GroupDetail":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"},"directRoles":{"type":"array","items":{"$ref":"#/components/schemas/RoleSummary"}},"effectiveRoles":{"type":"array","items":{"$ref":"#/components/schemas/RoleSummary"}},"members":{"type":"array","items":{"$ref":"#/components/schemas/UserSummary"}},"childGroups":{"type":"array","items":{"$ref":"#/components/schemas/GroupSummary"}}}},"TableSizeResponse":{"type":"object","description":"Table size and row count information","properties":{"tableName":{"type":"string","description":"Table name"},"rowCount":{"type":"integer","format":"int64","description":"Approximate row count"},"dataSize":{"type":"string","description":"Human-readable data size"},"indexSize":{"type":"string","description":"Human-readable index size"},"dataSizeBytes":{"type":"integer","format":"int64","description":"Data size in bytes"},"indexSizeBytes":{"type":"integer","format":"int64","description":"Index size in bytes"}}},"DatabaseStatusResponse":{"type":"object","description":"Database connection and version status","properties":{"connected":{"type":"boolean","description":"Whether the database is reachable"},"version":{"type":"string","description":"PostgreSQL version string"},"host":{"type":"string","description":"Database host"},"schema":{"type":"string","description":"Current schema search path"},"timescaleDb":{"type":"boolean","description":"Whether TimescaleDB extension is available"}}},"ActiveQueryResponse":{"type":"object","description":"Currently running database query","properties":{"pid":{"type":"integer","format":"int32","description":"Backend process ID"},"durationSeconds":{"type":"number","format":"double","description":"Query duration in seconds"},"state":{"type":"string","description":"Backend state (active, idle, etc.)"},"query":{"type":"string","description":"SQL query text"}}},"ConnectionPoolResponse":{"type":"object","description":"HikariCP connection pool statistics","properties":{"activeConnections":{"type":"integer","format":"int32","description":"Number of currently active connections"},"idleConnections":{"type":"integer","format":"int32","description":"Number of idle connections"},"pendingThreads":{"type":"integer","format":"int32","description":"Number of threads waiting for a connection"},"maxWaitMs":{"type":"integer","format":"int64","description":"Maximum wait time in milliseconds"},"maxPoolSize":{"type":"integer","format":"int32","description":"Maximum pool size"}}},"AuditLogPageResponse":{"type":"object","description":"Paginated audit log entries","properties":{"items":{"type":"array","description":"Audit log entries","items":{"$ref":"#/components/schemas/AuditRecord"}},"totalCount":{"type":"integer","format":"int64","description":"Total number of matching entries"},"page":{"type":"integer","format":"int32","description":"Current page number (0-based)"},"pageSize":{"type":"integer","format":"int32","description":"Page size"},"totalPages":{"type":"integer","format":"int32","description":"Total number of pages"}}},"AuditRecord":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"timestamp":{"type":"string","format":"date-time"},"username":{"type":"string"},"action":{"type":"string"},"category":{"type":"string","enum":["INFRA","AUTH","USER_MGMT","CONFIG","RBAC","AGENT"]},"target":{"type":"string"},"detail":{"type":"object","additionalProperties":{"type":"object"}},"result":{"type":"string","enum":["SUCCESS","FAILURE"]},"ipAddress":{"type":"string"},"userAgent":{"type":"string"}}}},"securitySchemes":{"bearer":{"type":"http","scheme":"bearer","bearerFormat":"JWT"}}}} \ No newline at end of file +{ + "openapi": "3.1.0", + "info": { + "title": "Cameleer3 Server API", + "version": "1.0" + }, + "servers": [ + { + "url": "/api/v1", + "description": "Relative" + } + ], + "security": [ + { + "bearer": [] + } + ], + "tags": [ + { + "name": "Agent Events", + "description": "Agent lifecycle event log" + }, + { + "name": "Database Admin", + "description": "Database monitoring and management (ADMIN only)" + }, + { + "name": "Threshold Admin", + "description": "Monitoring threshold configuration (ADMIN only)" + }, + { + "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": "Authentication", + "description": "Login and token refresh endpoints" + }, + { + "name": "Role Admin", + "description": "Role management (ADMIN only)" + }, + { + "name": "RBAC Stats", + "description": "RBAC statistics (ADMIN only)" + }, + { + "name": "OIDC Config Admin", + "description": "OIDC provider configuration (ADMIN only)" + }, + { + "name": "Route Metrics", + "description": "Route performance metrics" + }, + { + "name": "Search", + "description": "Transaction search endpoints" + }, + { + "name": "Agent SSE", + "description": "Server-Sent Events endpoint for agent communication" + }, + { + "name": "Ingestion", + "description": "Data ingestion endpoints" + }, + { + "name": "Audit Log", + "description": "Audit log viewer (ADMIN only)" + }, + { + "name": "Application Logs", + "description": "Query application logs stored in OpenSearch" + }, + { + "name": "Group Admin", + "description": "Group management (ADMIN only)" + }, + { + "name": "Diagrams", + "description": "Diagram rendering endpoints" + }, + { + "name": "OpenSearch Admin", + "description": "OpenSearch monitoring and management (ADMIN only)" + }, + { + "name": "Application Config", + "description": "Per-application observability configuration" + }, + { + "name": "Detail", + "description": "Execution detail and processor snapshot endpoints" + }, + { + "name": "Route Catalog", + "description": "Route catalog and discovery" + } + ], + "paths": { + "/config/{application}": { + "get": { + "tags": [ + "Application Config" + ], + "summary": "Get application config", + "description": "Returns the current configuration for an application. Returns defaults if none stored.", + "operationId": "getConfig", + "parameters": [ + { + "name": "application", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Config returned", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApplicationConfig" + } + } + } + } + } + }, + "put": { + "tags": [ + "Application Config" + ], + "summary": "Update application config", + "description": "Saves config and pushes CONFIG_UPDATE to all LIVE agents of this application", + "operationId": "updateConfig", + "parameters": [ + { + "name": "application", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApplicationConfig" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Config saved and pushed", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ApplicationConfig" + } + } + } + } + } + } + }, + "/admin/users/{userId}": { + "get": { + "tags": [ + "User Admin" + ], + "summary": "Get user by ID with RBAC detail", + "operationId": "getUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "User found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDetail" + } + } + } + }, + "404": { + "description": "User not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDetail" + } + } + } + } + } + }, + "put": { + "tags": [ + "User Admin" + ], + "summary": "Update user display name or email", + "operationId": "updateUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User updated" + }, + "404": { + "description": "User not found" + } + } + }, + "delete": { + "tags": [ + "User Admin" + ], + "summary": "Delete user", + "operationId": "deleteUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "User deleted" + } + } + } + }, + "/admin/thresholds": { + "get": { + "tags": [ + "Threshold Admin" + ], + "summary": "Get current threshold configuration", + "operationId": "getThresholds", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ThresholdConfig" + } + } + } + } + } + }, + "put": { + "tags": [ + "Threshold Admin" + ], + "summary": "Update threshold configuration", + "operationId": "updateThresholds", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ThresholdConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ThresholdConfig" + } + } + } + } + } + } + }, + "/admin/roles/{id}": { + "get": { + "tags": [ + "Role Admin" + ], + "summary": "Get role by ID with effective principals", + "operationId": "getRole", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Role found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RoleDetail" + } + } + } + }, + "404": { + "description": "Role not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RoleDetail" + } + } + } + } + } + }, + "put": { + "tags": [ + "Role Admin" + ], + "summary": "Update a custom role", + "operationId": "updateRole", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Role updated" + }, + "403": { + "description": "Cannot modify system role" + }, + "404": { + "description": "Role not found" + } + } + }, + "delete": { + "tags": [ + "Role Admin" + ], + "summary": "Delete a custom role", + "operationId": "deleteRole", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Role deleted" + }, + "403": { + "description": "Cannot delete system role" + }, + "404": { + "description": "Role not found" + } + } + } + }, + "/admin/oidc": { + "get": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Get OIDC configuration", + "operationId": "getConfig_1", + "responses": { + "200": { + "description": "Current OIDC configuration (client_secret masked)", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OidcAdminConfigResponse" + } + } + } + } + } + }, + "put": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Save OIDC configuration", + "operationId": "saveConfig", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OidcAdminConfigRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Configuration saved", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OidcAdminConfigResponse" + } + } + } + }, + "400": { + "description": "Invalid configuration", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + }, + "delete": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Delete OIDC configuration", + "operationId": "deleteConfig", + "responses": { + "204": { + "description": "Configuration deleted" + } + } + } + }, + "/admin/groups/{id}": { + "get": { + "tags": [ + "Group Admin" + ], + "summary": "Get group by ID with effective roles", + "operationId": "getGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Group found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/GroupDetail" + } + } + } + }, + "404": { + "description": "Group not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/GroupDetail" + } + } + } + } + } + }, + "put": { + "tags": [ + "Group Admin" + ], + "summary": "Update group name or parent", + "operationId": "updateGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Group updated" + }, + "404": { + "description": "Group not found" + }, + "409": { + "description": "Cycle detected in group hierarchy" + } + } + }, + "delete": { + "tags": [ + "Group Admin" + ], + "summary": "Delete group", + "operationId": "deleteGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Group deleted" + }, + "404": { + "description": "Group not found" + } + } + } + }, + "/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": "application", + "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 + } + }, + { + "name": "sortField", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "sortDir", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "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" + }, + "400": { + "description": "Invalid payload" + }, + "503": { + "description": "Buffer full, retry later" + } + } + } + }, + "/data/logs": { + "post": { + "tags": [ + "Ingestion" + ], + "summary": "Ingest application log entries", + "description": "Accepts a batch of log entries from an agent. Entries are indexed in OpenSearch.", + "operationId": "ingestLogs", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LogBatch" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Logs accepted for indexing" + } + } + } + }, + "/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" + } + } + } + }, + "/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" + } + } + } + }, + "/config/{application}/test-expression": { + "post": { + "tags": [ + "Application Config" + ], + "summary": "Test a tap expression against sample data via a live agent", + "operationId": "testExpression", + "parameters": [ + { + "name": "application", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TestExpressionRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Expression evaluated successfully", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TestExpressionResponse" + } + } + } + }, + "404": { + "description": "No live agent available for this application", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TestExpressionResponse" + } + } + } + }, + "504": { + "description": "Agent did not respond in time", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/TestExpressionResponse" + } + } + } + } + } + } + }, + "/auth/refresh": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Refresh access token", + "operationId": "refresh", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Token refreshed", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthTokenResponse" + } + } + } + }, + "401": { + "description": "Invalid refresh token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/auth/oidc/callback": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Exchange OIDC authorization code for JWTs", + "operationId": "callback", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CallbackRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Authentication successful", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthTokenResponse" + } + } + } + }, + "401": { + "description": "OIDC authentication failed", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "403": { + "description": "Account not provisioned", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "404": { + "description": "OIDC not configured or disabled", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthTokenResponse" + } + } + } + } + } + } + }, + "/auth/login": { + "post": { + "tags": [ + "Authentication" + ], + "summary": "Login with local credentials", + "operationId": "login", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Login successful", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuthTokenResponse" + } + } + } + }, + "401": { + "description": "Invalid credentials", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/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": { + "$ref": "#/components/schemas/AgentRefreshRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "New access token issued", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentRefreshResponse" + } + } + } + }, + "401": { + "description": "Invalid or expired refresh token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentRefreshResponse" + } + } + } + }, + "404": { + "description": "Agent not found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentRefreshResponse" + } + } + } + } + } + } + }, + "/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 command to the specified agent via SSE", + "operationId": "sendCommand", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandRequest" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Command accepted", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandSingleResponse" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandSingleResponse" + } + } + } + }, + "404": { + "description": "Agent not registered", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandSingleResponse" + } + } + } + } + } + } + }, + "/agents/{id}/commands/{commandId}/ack": { + "post": { + "tags": [ + "Agent Commands" + ], + "summary": "Acknowledge command receipt", + "description": "Agent acknowledges that it has received and processed a command, with result status and message", + "operationId": "acknowledgeCommand", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "commandId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CommandAckRequest" + } + } + } + }, + "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": { + "$ref": "#/components/schemas/AgentRegistrationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Agent registered successfully", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentRegistrationResponse" + } + } + } + }, + "400": { + "description": "Invalid registration payload", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, + "401": { + "description": "Missing or invalid bootstrap token", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentRegistrationResponse" + } + } + } + } + } + } + }, + "/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": { + "$ref": "#/components/schemas/CommandRequest" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Commands accepted", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandBroadcastResponse" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandBroadcastResponse" + } + } + } + } + } + } + }, + "/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": { + "$ref": "#/components/schemas/CommandRequest" + } + } + }, + "required": true + }, + "responses": { + "202": { + "description": "Commands accepted", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandBroadcastResponse" + } + } + } + }, + "400": { + "description": "Invalid command payload", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/CommandBroadcastResponse" + } + } + } + } + } + } + }, + "/admin/users": { + "get": { + "tags": [ + "User Admin" + ], + "summary": "List all users with RBAC detail", + "operationId": "listUsers", + "responses": { + "200": { + "description": "User list returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserDetail" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "User Admin" + ], + "summary": "Create a local user", + "operationId": "createUser", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateUserRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "User created", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/UserDetail" + } + } + } + } + } + } + }, + "/admin/users/{userId}/roles/{roleId}": { + "post": { + "tags": [ + "User Admin" + ], + "summary": "Assign a role to a user", + "operationId": "assignRoleToUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Role assigned" + }, + "404": { + "description": "User or role not found" + } + } + }, + "delete": { + "tags": [ + "User Admin" + ], + "summary": "Remove a role from a user", + "operationId": "removeRoleFromUser", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Role removed" + } + } + } + }, + "/admin/users/{userId}/password": { + "post": { + "tags": [ + "User Admin" + ], + "summary": "Reset user password", + "operationId": "resetPassword", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SetPasswordRequest" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "Password reset" + } + } + } + }, + "/admin/users/{userId}/groups/{groupId}": { + "post": { + "tags": [ + "User Admin" + ], + "summary": "Add a user to a group", + "operationId": "addUserToGroup", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "User added to group" + } + } + }, + "delete": { + "tags": [ + "User Admin" + ], + "summary": "Remove a user from a group", + "operationId": "removeUserFromGroup", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "groupId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "User removed from group" + } + } + } + }, + "/admin/roles": { + "get": { + "tags": [ + "Role Admin" + ], + "summary": "List all roles (system and custom)", + "operationId": "listRoles", + "responses": { + "200": { + "description": "Role list returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleDetail" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Role Admin" + ], + "summary": "Create a custom role", + "operationId": "createRole", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateRoleRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Role created", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "uuid" + } + } + } + } + } + } + } + }, + "/admin/oidc/test": { + "post": { + "tags": [ + "OIDC Config Admin" + ], + "summary": "Test OIDC provider connectivity", + "operationId": "testConnection", + "responses": { + "200": { + "description": "Provider reachable", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OidcTestResult" + } + } + } + }, + "400": { + "description": "Provider unreachable or misconfigured", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/admin/groups": { + "get": { + "tags": [ + "Group Admin" + ], + "summary": "List all groups with hierarchy and effective roles", + "operationId": "listGroups", + "responses": { + "200": { + "description": "Group list returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupDetail" + } + } + } + } + } + } + }, + "post": { + "tags": [ + "Group Admin" + ], + "summary": "Create a new group", + "operationId": "createGroup", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateGroupRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Group created", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string", + "format": "uuid" + } + } + } + } + } + } + } + }, + "/admin/groups/{id}/roles/{roleId}": { + "post": { + "tags": [ + "Group Admin" + ], + "summary": "Assign a role to a group", + "operationId": "assignRoleToGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "Role assigned to group" + }, + "404": { + "description": "Group not found" + } + } + }, + "delete": { + "tags": [ + "Group Admin" + ], + "summary": "Remove a role from a group", + "operationId": "removeRoleFromGroup", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "204": { + "description": "Role removed from group" + }, + "404": { + "description": "Group not found" + } + } + } + }, + "/admin/database/queries/{pid}/kill": { + "post": { + "tags": [ + "Database Admin" + ], + "summary": "Terminate a query by PID", + "operationId": "killQuery", + "parameters": [ + { + "name": "pid", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/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" + } + }, + { + "name": "routeId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "application", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "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 + } + }, + { + "name": "routeId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "application", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/StatsTimeseries" + } + } + } + } + } + } + }, + "/routes/metrics": { + "get": { + "tags": [ + "Route Metrics" + ], + "summary": "Get route metrics", + "description": "Returns aggregated performance metrics per route for the given time window", + "operationId": "getMetrics", + "parameters": [ + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "appId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Metrics returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteMetrics" + } + } + } + } + } + } + } + }, + "/routes/metrics/processors": { + "get": { + "tags": [ + "Route Metrics" + ], + "summary": "Get processor metrics", + "description": "Returns aggregated performance metrics per processor for the given route and time window", + "operationId": "getProcessorMetrics", + "parameters": [ + { + "name": "routeId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "appId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + } + ], + "responses": { + "200": { + "description": "Metrics returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessorMetrics" + } + } + } + } + } + } + } + }, + "/routes/catalog": { + "get": { + "tags": [ + "Route Catalog" + ], + "summary": "Get route catalog", + "description": "Returns all applications with their routes, agents, and health status", + "operationId": "getCatalog", + "parameters": [ + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Catalog returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AppCatalogEntry" + } + } + } + } + } + } + } + }, + "/logs": { + "get": { + "tags": [ + "Application Logs" + ], + "summary": "Search application log entries", + "description": "Returns log entries for a given application, optionally filtered by agent, level, time range, and text query", + "operationId": "searchLogs", + "parameters": [ + { + "name": "application", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "agentId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "level", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "query", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "exchangeId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 200 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogEntryResponse" + } + } + } + } + } + } + } + }, + "/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": "Execution detail found", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ExecutionDetail" + } + } + } + }, + "404": { + "description": "Execution not found", + "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": "Snapshot data", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + }, + "404": { + "description": "Snapshot not found", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + } + } + } + } + }, + "/diagrams": { + "get": { + "tags": [ + "Diagrams" + ], + "summary": "Find diagram by application and route ID", + "description": "Resolves application to agent IDs and finds the latest diagram for the route", + "operationId": "findByApplicationAndRoute", + "parameters": [ + { + "name": "application", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "routeId", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "direction", + "in": "query", + "description": "Layout direction: LR (left-to-right) or TB (top-to-bottom)", + "required": false, + "schema": { + "type": "string", + "default": "LR", + "enum": [ + "LR", + "TB" + ] + } + } + ], + "responses": { + "200": { + "description": "Diagram layout returned", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DiagramLayout" + } + } + } + }, + "404": { + "description": "No diagram found for the given application and route", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DiagramLayout" + } + } + } + } + } + } + }, + "/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" + } + }, + { + "name": "direction", + "in": "query", + "description": "Layout direction: LR (left-to-right) or TB (top-to-bottom)", + "required": false, + "schema": { + "type": "string", + "default": "LR", + "enum": [ + "LR", + "TB" + ] + } + } + ], + "responses": { + "200": { + "description": "Diagram rendered successfully", + "content": { + "image/svg+xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiagramLayout" + } + } + } + }, + "404": { + "description": "Diagram not found", + "content": { + "*/*": { + "schema": { + "type": "object" + } + } + } + } + } + } + }, + "/config": { + "get": { + "tags": [ + "Application Config" + ], + "summary": "List all application configs", + "description": "Returns stored configurations for all applications", + "operationId": "listConfigs", + "responses": { + "200": { + "description": "Configs returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ApplicationConfig" + } + } + } + } + } + } + } + }, + "/auth/oidc/config": { + "get": { + "tags": [ + "Authentication" + ], + "summary": "Get OIDC config for SPA login flow", + "operationId": "getConfig_2", + "responses": { + "200": { + "description": "OIDC configuration", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OidcPublicConfigResponse" + } + } + } + }, + "404": { + "description": "OIDC not configured or disabled", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OidcPublicConfigResponse" + } + } + } + }, + "500": { + "description": "Failed to retrieve OIDC provider metadata", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/agents": { + "get": { + "tags": [ + "Agent Management" + ], + "summary": "List all agents", + "description": "Returns all registered agents with runtime metrics, optionally filtered by status and/or application", + "operationId": "listAgents", + "parameters": [ + { + "name": "status", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "application", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Agent list returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentInstanceResponse" + } + } + } + } + }, + "400": { + "description": "Invalid status filter", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + } + } + } + }, + "/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" + } + } + } + } + } + } + }, + "/agents/{agentId}/metrics": { + "get": { + "tags": [ + "agent-metrics-controller" + ], + "operationId": "getMetrics_1", + "parameters": [ + { + "name": "agentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "names", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "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": 60 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AgentMetricsResponse" + } + } + } + } + } + } + }, + "/agents/events-log": { + "get": { + "tags": [ + "Agent Events" + ], + "summary": "Query agent events", + "description": "Returns agent lifecycle events, optionally filtered by app and/or agent ID", + "operationId": "getEvents", + "parameters": [ + { + "name": "appId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "agentId", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 50 + } + } + ], + "responses": { + "200": { + "description": "Events returned", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentEventResponse" + } + } + } + } + } + } + } + }, + "/admin/rbac/stats": { + "get": { + "tags": [ + "RBAC Stats" + ], + "summary": "Get RBAC statistics for the dashboard", + "operationId": "getStats", + "responses": { + "200": { + "description": "RBAC stats returned", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/RbacStats" + } + } + } + } + } + } + }, + "/admin/opensearch/status": { + "get": { + "tags": [ + "OpenSearch Admin" + ], + "summary": "Get OpenSearch cluster status and version", + "operationId": "getStatus", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/OpenSearchStatusResponse" + } + } + } + } + } + } + }, + "/admin/opensearch/pipeline": { + "get": { + "tags": [ + "OpenSearch Admin" + ], + "summary": "Get indexing pipeline statistics", + "operationId": "getPipeline", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PipelineStatsResponse" + } + } + } + } + } + } + }, + "/admin/opensearch/performance": { + "get": { + "tags": [ + "OpenSearch Admin" + ], + "summary": "Get OpenSearch performance metrics", + "operationId": "getPerformance", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/PerformanceResponse" + } + } + } + } + } + } + }, + "/admin/opensearch/indices": { + "get": { + "tags": [ + "OpenSearch Admin" + ], + "summary": "Get OpenSearch indices with pagination", + "operationId": "getIndices", + "parameters": [ + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "prefix", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "executions" + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/IndicesPageResponse" + } + } + } + } + } + } + }, + "/admin/database/tables": { + "get": { + "tags": [ + "Database Admin" + ], + "summary": "Get table sizes and row counts", + "operationId": "getTables", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TableSizeResponse" + } + } + } + } + } + } + } + }, + "/admin/database/status": { + "get": { + "tags": [ + "Database Admin" + ], + "summary": "Get database connection status and version", + "operationId": "getStatus_1", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/DatabaseStatusResponse" + } + } + } + } + } + } + }, + "/admin/database/queries": { + "get": { + "tags": [ + "Database Admin" + ], + "summary": "Get active queries", + "operationId": "getQueries", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ActiveQueryResponse" + } + } + } + } + } + } + } + }, + "/admin/database/pool": { + "get": { + "tags": [ + "Database Admin" + ], + "summary": "Get HikariCP connection pool stats", + "operationId": "getPool", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/ConnectionPoolResponse" + } + } + } + } + } + } + }, + "/admin/database/metrics-pipeline": { + "get": { + "tags": [ + "Database Admin" + ], + "summary": "Get metrics ingestion pipeline diagnostics", + "operationId": "getMetricsPipeline", + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + } + }, + "/admin/audit": { + "get": { + "tags": [ + "Audit Log" + ], + "summary": "Search audit log entries with pagination", + "operationId": "getAuditLog", + "parameters": [ + { + "name": "username", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "category", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "search", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "from", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "to", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + } + }, + { + "name": "sort", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "timestamp" + } + }, + { + "name": "order", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "desc" + } + }, + { + "name": "page", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "size", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "format": "int32", + "default": 25 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/AuditLogPageResponse" + } + } + } + } + } + } + }, + "/admin/opensearch/indices/{name}": { + "delete": { + "tags": [ + "OpenSearch Admin" + ], + "summary": "Delete an OpenSearch index", + "operationId": "deleteIndex", + "parameters": [ + { + "name": "name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "components": { + "schemas": { + "ApplicationConfig": { + "type": "object", + "properties": { + "application": { + "type": "string" + }, + "version": { + "type": "integer", + "format": "int32" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "engineLevel": { + "type": "string" + }, + "payloadCaptureMode": { + "type": "string" + }, + "metricsEnabled": { + "type": "boolean" + }, + "samplingRate": { + "type": "number", + "format": "double" + }, + "tracedProcessors": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "logForwardingLevel": { + "type": "string" + }, + "taps": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TapDefinition" + } + }, + "tapVersion": { + "type": "integer", + "format": "int32" + }, + "routeRecording": { + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "compressSuccess": { + "type": "boolean" + } + } + }, + "TapDefinition": { + "type": "object", + "properties": { + "tapId": { + "type": "string" + }, + "processorId": { + "type": "string" + }, + "target": { + "type": "string" + }, + "expression": { + "type": "string" + }, + "language": { + "type": "string" + }, + "attributeName": { + "type": "string" + }, + "attributeType": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "version": { + "type": "integer", + "format": "int32" + } + } + }, + "UpdateUserRequest": { + "type": "object", + "properties": { + "displayName": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "DatabaseThresholdsRequest": { + "type": "object", + "description": "Database monitoring thresholds", + "properties": { + "connectionPoolWarning": { + "type": "integer", + "format": "int32", + "description": "Connection pool usage warning threshold (percentage)", + "maximum": 100, + "minimum": 0 + }, + "connectionPoolCritical": { + "type": "integer", + "format": "int32", + "description": "Connection pool usage critical threshold (percentage)", + "maximum": 100, + "minimum": 0 + }, + "queryDurationWarning": { + "type": "number", + "format": "double", + "description": "Query duration warning threshold (seconds)" + }, + "queryDurationCritical": { + "type": "number", + "format": "double", + "description": "Query duration critical threshold (seconds)" + } + } + }, + "OpenSearchThresholdsRequest": { + "type": "object", + "description": "OpenSearch monitoring thresholds", + "properties": { + "clusterHealthWarning": { + "type": "string", + "description": "Cluster health warning threshold (GREEN, YELLOW, RED)", + "minLength": 1 + }, + "clusterHealthCritical": { + "type": "string", + "description": "Cluster health critical threshold (GREEN, YELLOW, RED)", + "minLength": 1 + }, + "queueDepthWarning": { + "type": "integer", + "format": "int32", + "description": "Queue depth warning threshold", + "minimum": 0 + }, + "queueDepthCritical": { + "type": "integer", + "format": "int32", + "description": "Queue depth critical threshold", + "minimum": 0 + }, + "jvmHeapWarning": { + "type": "integer", + "format": "int32", + "description": "JVM heap usage warning threshold (percentage)", + "maximum": 100, + "minimum": 0 + }, + "jvmHeapCritical": { + "type": "integer", + "format": "int32", + "description": "JVM heap usage critical threshold (percentage)", + "maximum": 100, + "minimum": 0 + }, + "failedDocsWarning": { + "type": "integer", + "format": "int32", + "description": "Failed document count warning threshold", + "minimum": 0 + }, + "failedDocsCritical": { + "type": "integer", + "format": "int32", + "description": "Failed document count critical threshold", + "minimum": 0 + } + } + }, + "ThresholdConfigRequest": { + "type": "object", + "description": "Threshold configuration for admin monitoring", + "properties": { + "database": { + "$ref": "#/components/schemas/DatabaseThresholdsRequest" + }, + "opensearch": { + "$ref": "#/components/schemas/OpenSearchThresholdsRequest" + } + }, + "required": [ + "database", + "opensearch" + ] + }, + "DatabaseThresholds": { + "type": "object", + "properties": { + "connectionPoolWarning": { + "type": "integer", + "format": "int32" + }, + "connectionPoolCritical": { + "type": "integer", + "format": "int32" + }, + "queryDurationWarning": { + "type": "number", + "format": "double" + }, + "queryDurationCritical": { + "type": "number", + "format": "double" + } + } + }, + "OpenSearchThresholds": { + "type": "object", + "properties": { + "clusterHealthWarning": { + "type": "string" + }, + "clusterHealthCritical": { + "type": "string" + }, + "queueDepthWarning": { + "type": "integer", + "format": "int32" + }, + "queueDepthCritical": { + "type": "integer", + "format": "int32" + }, + "jvmHeapWarning": { + "type": "integer", + "format": "int32" + }, + "jvmHeapCritical": { + "type": "integer", + "format": "int32" + }, + "failedDocsWarning": { + "type": "integer", + "format": "int32" + }, + "failedDocsCritical": { + "type": "integer", + "format": "int32" + } + } + }, + "ThresholdConfig": { + "type": "object", + "properties": { + "database": { + "$ref": "#/components/schemas/DatabaseThresholds" + }, + "opensearch": { + "$ref": "#/components/schemas/OpenSearchThresholds" + } + } + }, + "UpdateRoleRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "scope": { + "type": "string" + } + } + }, + "OidcAdminConfigRequest": { + "type": "object", + "description": "OIDC configuration update request", + "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" + }, + "displayNameClaim": { + "type": "string" + } + } + }, + "ErrorResponse": { + "type": "object", + "description": "Error response", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ] + }, + "OidcAdminConfigResponse": { + "type": "object", + "description": "OIDC configuration for admin management", + "properties": { + "configured": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "issuerUri": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "clientSecretSet": { + "type": "boolean" + }, + "rolesClaim": { + "type": "string" + }, + "defaultRoles": { + "type": "array", + "items": { + "type": "string" + } + }, + "autoSignup": { + "type": "boolean" + }, + "displayNameClaim": { + "type": "string" + } + } + }, + "UpdateGroupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parentGroupId": { + "type": "string", + "format": "uuid" + } + } + }, + "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" + }, + "application": { + "type": "string" + }, + "agentIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "offset": { + "type": "integer", + "format": "int32" + }, + "limit": { + "type": "integer", + "format": "int32" + }, + "sortField": { + "type": "string" + }, + "sortDir": { + "type": "string" + } + } + }, + "ExecutionSummary": { + "type": "object", + "properties": { + "executionId": { + "type": "string" + }, + "routeId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "applicationName": { + "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" + }, + "highlight": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "agentId", + "applicationName", + "attributes", + "correlationId", + "diagramContentHash", + "durationMs", + "endTime", + "errorMessage", + "executionId", + "highlight", + "routeId", + "startTime", + "status" + ] + }, + "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" + } + }, + "required": [ + "data", + "limit", + "offset", + "total" + ] + }, + "LogBatch": { + "type": "object", + "properties": { + "entries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LogEntry" + } + } + } + }, + "LogEntry": { + "type": "object", + "properties": { + "timestamp": { + "type": "string", + "format": "date-time" + }, + "level": { + "type": "string" + }, + "loggerName": { + "type": "string" + }, + "message": { + "type": "string" + }, + "threadName": { + "type": "string" + }, + "stackTrace": { + "type": "string" + }, + "mdc": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "TestExpressionRequest": { + "type": "object", + "properties": { + "expression": { + "type": "string" + }, + "language": { + "type": "string" + }, + "body": { + "type": "string" + }, + "target": { + "type": "string" + } + } + }, + "TestExpressionResponse": { + "type": "object", + "properties": { + "result": { + "type": "string" + }, + "error": { + "type": "string" + } + } + }, + "RefreshRequest": { + "type": "object", + "properties": { + "refreshToken": { + "type": "string" + } + } + }, + "AuthTokenResponse": { + "type": "object", + "description": "JWT token pair", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "idToken": { + "type": "string", + "description": "OIDC id_token for end-session logout (only present after OIDC login)" + } + }, + "required": [ + "accessToken", + "displayName", + "refreshToken" + ] + }, + "CallbackRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "redirectUri": { + "type": "string" + } + } + }, + "LoginRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "AgentRefreshRequest": { + "type": "object", + "description": "Agent token refresh request", + "properties": { + "refreshToken": { + "type": "string" + } + }, + "required": [ + "refreshToken" + ] + }, + "AgentRefreshResponse": { + "type": "object", + "description": "Refreshed access and refresh tokens", + "properties": { + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + } + }, + "required": [ + "accessToken", + "refreshToken" + ] + }, + "CommandRequest": { + "type": "object", + "description": "Command to send to agent(s)", + "properties": { + "type": { + "type": "string", + "description": "Command type: config-update, deep-trace, or replay" + }, + "payload": { + "type": "object", + "description": "Command payload JSON" + } + }, + "required": [ + "type" + ] + }, + "CommandSingleResponse": { + "type": "object", + "description": "Result of sending a command to a single agent", + "properties": { + "commandId": { + "type": "string" + }, + "status": { + "type": "string" + } + }, + "required": [ + "commandId", + "status" + ] + }, + "CommandAckRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + }, + "message": { + "type": "string" + }, + "data": { + "type": "string" + } + } + }, + "AgentRegistrationRequest": { + "type": "object", + "description": "Agent registration payload", + "properties": { + "agentId": { + "type": "string" + }, + "name": { + "type": "string" + }, + "application": { + "type": "string", + "default": "default" + }, + "version": { + "type": "string" + }, + "routeIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "capabilities": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + }, + "required": [ + "agentId", + "name" + ] + }, + "AgentRegistrationResponse": { + "type": "object", + "description": "Agent registration result with JWT tokens and SSE endpoint", + "properties": { + "agentId": { + "type": "string" + }, + "sseEndpoint": { + "type": "string" + }, + "heartbeatIntervalMs": { + "type": "integer", + "format": "int64" + }, + "serverPublicKey": { + "type": "string" + }, + "accessToken": { + "type": "string" + }, + "refreshToken": { + "type": "string" + } + }, + "required": [ + "accessToken", + "agentId", + "refreshToken", + "serverPublicKey", + "sseEndpoint" + ] + }, + "CommandBroadcastResponse": { + "type": "object", + "description": "Result of broadcasting a command to multiple agents", + "properties": { + "commandIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "targetCount": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "commandIds" + ] + }, + "CreateUserRequest": { + "type": "object", + "properties": { + "username": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "GroupSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + } + }, + "RoleSummary": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "system": { + "type": "boolean" + }, + "source": { + "type": "string" + } + } + }, + "UserDetail": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "provider": { + "type": "string" + }, + "email": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "directRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleSummary" + } + }, + "directGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupSummary" + } + }, + "effectiveRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleSummary" + } + }, + "effectiveGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupSummary" + } + } + } + }, + "SetPasswordRequest": { + "type": "object", + "properties": { + "password": { + "type": "string", + "minLength": 1 + } + } + }, + "CreateRoleRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "scope": { + "type": "string" + } + } + }, + "OidcTestResult": { + "type": "object", + "description": "OIDC provider connectivity test result", + "properties": { + "status": { + "type": "string" + }, + "authorizationEndpoint": { + "type": "string" + } + }, + "required": [ + "authorizationEndpoint", + "status" + ] + }, + "CreateGroupRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "parentGroupId": { + "type": "string", + "format": "uuid" + } + } + }, + "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" + } + }, + "required": [ + "activeCount", + "avgDurationMs", + "failedCount", + "p99LatencyMs", + "prevAvgDurationMs", + "prevFailedCount", + "prevP99LatencyMs", + "prevTotalCount", + "totalCount", + "totalToday" + ] + }, + "StatsTimeseries": { + "type": "object", + "properties": { + "buckets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TimeseriesBucket" + } + } + }, + "required": [ + "buckets" + ] + }, + "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" + } + }, + "required": [ + "activeCount", + "avgDurationMs", + "failedCount", + "p99DurationMs", + "time", + "totalCount" + ] + }, + "RouteMetrics": { + "type": "object", + "description": "Aggregated route performance metrics", + "properties": { + "routeId": { + "type": "string" + }, + "appId": { + "type": "string" + }, + "exchangeCount": { + "type": "integer", + "format": "int64" + }, + "successRate": { + "type": "number", + "format": "double" + }, + "avgDurationMs": { + "type": "number", + "format": "double" + }, + "p99DurationMs": { + "type": "number", + "format": "double" + }, + "errorRate": { + "type": "number", + "format": "double" + }, + "throughputPerSec": { + "type": "number", + "format": "double" + }, + "sparkline": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + } + }, + "required": [ + "appId", + "avgDurationMs", + "errorRate", + "exchangeCount", + "p99DurationMs", + "routeId", + "sparkline", + "successRate", + "throughputPerSec" + ] + }, + "ProcessorMetrics": { + "type": "object", + "properties": { + "processorId": { + "type": "string" + }, + "processorType": { + "type": "string" + }, + "routeId": { + "type": "string" + }, + "appId": { + "type": "string" + }, + "totalCount": { + "type": "integer", + "format": "int64" + }, + "failedCount": { + "type": "integer", + "format": "int64" + }, + "avgDurationMs": { + "type": "number", + "format": "double" + }, + "p99DurationMs": { + "type": "number", + "format": "double" + }, + "errorRate": { + "type": "number", + "format": "double" + } + }, + "required": [ + "appId", + "avgDurationMs", + "errorRate", + "failedCount", + "p99DurationMs", + "processorId", + "processorType", + "routeId", + "totalCount" + ] + }, + "AgentSummary": { + "type": "object", + "description": "Summary of an agent instance for sidebar display", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "tps": { + "type": "number", + "format": "double" + } + }, + "required": [ + "id", + "name", + "status", + "tps" + ] + }, + "AppCatalogEntry": { + "type": "object", + "description": "Application catalog entry with routes and agents", + "properties": { + "appId": { + "type": "string" + }, + "routes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RouteSummary" + } + }, + "agents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/AgentSummary" + } + }, + "agentCount": { + "type": "integer", + "format": "int32" + }, + "health": { + "type": "string" + }, + "exchangeCount": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "agentCount", + "agents", + "appId", + "exchangeCount", + "health", + "routes" + ] + }, + "RouteSummary": { + "type": "object", + "description": "Summary of a route within an application", + "properties": { + "routeId": { + "type": "string" + }, + "exchangeCount": { + "type": "integer", + "format": "int64" + }, + "lastSeen": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "exchangeCount", + "lastSeen", + "routeId" + ] + }, + "LogEntryResponse": { + "type": "object", + "description": "Application log entry from OpenSearch", + "properties": { + "timestamp": { + "type": "string", + "description": "Log timestamp (ISO-8601)" + }, + "level": { + "type": "string", + "description": "Log level (INFO, WARN, ERROR, DEBUG)" + }, + "loggerName": { + "type": "string", + "description": "Logger name" + }, + "message": { + "type": "string", + "description": "Log message" + }, + "threadName": { + "type": "string", + "description": "Thread name" + }, + "stackTrace": { + "type": "string", + "description": "Stack trace (if present)" + } + } + }, + "ExecutionDetail": { + "type": "object", + "properties": { + "executionId": { + "type": "string" + }, + "routeId": { + "type": "string" + }, + "agentId": { + "type": "string" + }, + "applicationName": { + "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" + } + }, + "inputBody": { + "type": "string" + }, + "outputBody": { + "type": "string" + }, + "inputHeaders": { + "type": "string" + }, + "outputHeaders": { + "type": "string" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "required": [ + "agentId", + "applicationName", + "attributes", + "correlationId", + "diagramContentHash", + "durationMs", + "endTime", + "errorMessage", + "errorStackTrace", + "exchangeId", + "executionId", + "inputBody", + "inputHeaders", + "outputBody", + "outputHeaders", + "processors", + "routeId", + "startTime", + "status" + ] + }, + "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" + }, + "attributes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "children": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProcessorNode" + } + } + }, + "required": [ + "attributes", + "children", + "diagramNodeId", + "durationMs", + "endTime", + "errorMessage", + "errorStackTrace", + "processorId", + "processorType", + "startTime", + "status" + ] + }, + "DiagramLayout": { + "type": "object", + "properties": { + "width": { + "type": "number", + "format": "double" + }, + "height": { + "type": "number", + "format": "double" + }, + "nodes": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PositionedNode" + } + }, + "edges": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PositionedEdge" + } + } + } + }, + "PositionedEdge": { + "type": "object", + "properties": { + "sourceId": { + "type": "string" + }, + "targetId": { + "type": "string" + }, + "label": { + "type": "string" + }, + "points": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number", + "format": "double" + } + } + } + } + }, + "PositionedNode": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "label": { + "type": "string" + }, + "type": { + "type": "string" + }, + "x": { + "type": "number", + "format": "double" + }, + "y": { + "type": "number", + "format": "double" + }, + "width": { + "type": "number", + "format": "double" + }, + "height": { + "type": "number", + "format": "double" + } + } + }, + "OidcPublicConfigResponse": { + "type": "object", + "description": "OIDC configuration for SPA login flow", + "properties": { + "issuer": { + "type": "string" + }, + "clientId": { + "type": "string" + }, + "authorizationEndpoint": { + "type": "string" + }, + "endSessionEndpoint": { + "type": "string", + "description": "Present if the provider supports RP-initiated logout" + } + }, + "required": [ + "authorizationEndpoint", + "clientId", + "issuer" + ] + }, + "AgentInstanceResponse": { + "type": "object", + "description": "Agent instance summary with runtime metrics", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "application": { + "type": "string" + }, + "status": { + "type": "string" + }, + "routeIds": { + "type": "array", + "items": { + "type": "string" + } + }, + "registeredAt": { + "type": "string", + "format": "date-time" + }, + "lastHeartbeat": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + }, + "capabilities": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "tps": { + "type": "number", + "format": "double" + }, + "errorRate": { + "type": "number", + "format": "double" + }, + "activeRoutes": { + "type": "integer", + "format": "int32" + }, + "totalRoutes": { + "type": "integer", + "format": "int32" + }, + "uptimeSeconds": { + "type": "integer", + "format": "int64" + } + }, + "required": [ + "activeRoutes", + "application", + "capabilities", + "errorRate", + "id", + "lastHeartbeat", + "name", + "registeredAt", + "routeIds", + "status", + "totalRoutes", + "tps", + "uptimeSeconds", + "version" + ] + }, + "SseEmitter": { + "type": "object", + "properties": { + "timeout": { + "type": "integer", + "format": "int64" + } + } + }, + "AgentMetricsResponse": { + "type": "object", + "properties": { + "metrics": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MetricBucket" + } + } + } + }, + "required": [ + "metrics" + ] + }, + "MetricBucket": { + "type": "object", + "properties": { + "time": { + "type": "string", + "format": "date-time" + }, + "value": { + "type": "number", + "format": "double" + } + }, + "required": [ + "time", + "value" + ] + }, + "AgentEventResponse": { + "type": "object", + "description": "Agent lifecycle event", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "agentId": { + "type": "string" + }, + "appId": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "detail": { + "type": "string" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "agentId", + "appId", + "detail", + "eventType", + "id", + "timestamp" + ] + }, + "RoleDetail": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "system": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "assignedGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupSummary" + } + }, + "directUsers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + }, + "effectivePrincipals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + } + } + }, + "UserSummary": { + "type": "object", + "properties": { + "userId": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "provider": { + "type": "string" + } + } + }, + "RbacStats": { + "type": "object", + "properties": { + "userCount": { + "type": "integer", + "format": "int32" + }, + "activeUserCount": { + "type": "integer", + "format": "int32" + }, + "groupCount": { + "type": "integer", + "format": "int32" + }, + "maxGroupDepth": { + "type": "integer", + "format": "int32" + }, + "roleCount": { + "type": "integer", + "format": "int32" + } + } + }, + "OpenSearchStatusResponse": { + "type": "object", + "description": "OpenSearch cluster status", + "properties": { + "reachable": { + "type": "boolean", + "description": "Whether the cluster is reachable" + }, + "clusterHealth": { + "type": "string", + "description": "Cluster health status (GREEN, YELLOW, RED)" + }, + "version": { + "type": "string", + "description": "OpenSearch version" + }, + "nodeCount": { + "type": "integer", + "format": "int32", + "description": "Number of nodes in the cluster" + }, + "host": { + "type": "string", + "description": "OpenSearch host" + } + } + }, + "PipelineStatsResponse": { + "type": "object", + "description": "Search indexing pipeline statistics", + "properties": { + "queueDepth": { + "type": "integer", + "format": "int32", + "description": "Current queue depth" + }, + "maxQueueSize": { + "type": "integer", + "format": "int32", + "description": "Maximum queue size" + }, + "failedCount": { + "type": "integer", + "format": "int64", + "description": "Number of failed indexing operations" + }, + "indexedCount": { + "type": "integer", + "format": "int64", + "description": "Number of successfully indexed documents" + }, + "debounceMs": { + "type": "integer", + "format": "int64", + "description": "Debounce interval in milliseconds" + }, + "indexingRate": { + "type": "number", + "format": "double", + "description": "Current indexing rate (docs/sec)" + }, + "lastIndexedAt": { + "type": "string", + "format": "date-time", + "description": "Timestamp of last indexed document" + } + } + }, + "PerformanceResponse": { + "type": "object", + "description": "OpenSearch performance metrics", + "properties": { + "queryCacheHitRate": { + "type": "number", + "format": "double", + "description": "Query cache hit rate (0.0-1.0)" + }, + "requestCacheHitRate": { + "type": "number", + "format": "double", + "description": "Request cache hit rate (0.0-1.0)" + }, + "searchLatencyMs": { + "type": "number", + "format": "double", + "description": "Average search latency in milliseconds" + }, + "indexingLatencyMs": { + "type": "number", + "format": "double", + "description": "Average indexing latency in milliseconds" + }, + "jvmHeapUsedBytes": { + "type": "integer", + "format": "int64", + "description": "JVM heap used in bytes" + }, + "jvmHeapMaxBytes": { + "type": "integer", + "format": "int64", + "description": "JVM heap max in bytes" + } + } + }, + "IndexInfoResponse": { + "type": "object", + "description": "OpenSearch index information", + "properties": { + "name": { + "type": "string", + "description": "Index name" + }, + "docCount": { + "type": "integer", + "format": "int64", + "description": "Document count" + }, + "size": { + "type": "string", + "description": "Human-readable index size" + }, + "sizeBytes": { + "type": "integer", + "format": "int64", + "description": "Index size in bytes" + }, + "health": { + "type": "string", + "description": "Index health status" + }, + "primaryShards": { + "type": "integer", + "format": "int32", + "description": "Number of primary shards" + }, + "replicaShards": { + "type": "integer", + "format": "int32", + "description": "Number of replica shards" + } + } + }, + "IndicesPageResponse": { + "type": "object", + "description": "Paginated list of OpenSearch indices", + "properties": { + "indices": { + "type": "array", + "description": "Index list for current page", + "items": { + "$ref": "#/components/schemas/IndexInfoResponse" + } + }, + "totalIndices": { + "type": "integer", + "format": "int64", + "description": "Total number of indices" + }, + "totalDocs": { + "type": "integer", + "format": "int64", + "description": "Total document count across all indices" + }, + "totalSize": { + "type": "string", + "description": "Human-readable total size" + }, + "page": { + "type": "integer", + "format": "int32", + "description": "Current page number (0-based)" + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Page size" + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages" + } + } + }, + "GroupDetail": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "parentGroupId": { + "type": "string", + "format": "uuid" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "directRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleSummary" + } + }, + "effectiveRoles": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RoleSummary" + } + }, + "members": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSummary" + } + }, + "childGroups": { + "type": "array", + "items": { + "$ref": "#/components/schemas/GroupSummary" + } + } + } + }, + "TableSizeResponse": { + "type": "object", + "description": "Table size and row count information", + "properties": { + "tableName": { + "type": "string", + "description": "Table name" + }, + "rowCount": { + "type": "integer", + "format": "int64", + "description": "Approximate row count" + }, + "dataSize": { + "type": "string", + "description": "Human-readable data size" + }, + "indexSize": { + "type": "string", + "description": "Human-readable index size" + }, + "dataSizeBytes": { + "type": "integer", + "format": "int64", + "description": "Data size in bytes" + }, + "indexSizeBytes": { + "type": "integer", + "format": "int64", + "description": "Index size in bytes" + } + } + }, + "DatabaseStatusResponse": { + "type": "object", + "description": "Database connection and version status", + "properties": { + "connected": { + "type": "boolean", + "description": "Whether the database is reachable" + }, + "version": { + "type": "string", + "description": "PostgreSQL version string" + }, + "host": { + "type": "string", + "description": "Database host" + }, + "schema": { + "type": "string", + "description": "Current schema search path" + }, + "timescaleDb": { + "type": "boolean", + "description": "Whether TimescaleDB extension is available" + } + } + }, + "ActiveQueryResponse": { + "type": "object", + "description": "Currently running database query", + "properties": { + "pid": { + "type": "integer", + "format": "int32", + "description": "Backend process ID" + }, + "durationSeconds": { + "type": "number", + "format": "double", + "description": "Query duration in seconds" + }, + "state": { + "type": "string", + "description": "Backend state (active, idle, etc.)" + }, + "query": { + "type": "string", + "description": "SQL query text" + } + } + }, + "ConnectionPoolResponse": { + "type": "object", + "description": "HikariCP connection pool statistics", + "properties": { + "activeConnections": { + "type": "integer", + "format": "int32", + "description": "Number of currently active connections" + }, + "idleConnections": { + "type": "integer", + "format": "int32", + "description": "Number of idle connections" + }, + "pendingThreads": { + "type": "integer", + "format": "int32", + "description": "Number of threads waiting for a connection" + }, + "maxWaitMs": { + "type": "integer", + "format": "int64", + "description": "Maximum wait time in milliseconds" + }, + "maxPoolSize": { + "type": "integer", + "format": "int32", + "description": "Maximum pool size" + } + } + }, + "AuditLogPageResponse": { + "type": "object", + "description": "Paginated audit log entries", + "properties": { + "items": { + "type": "array", + "description": "Audit log entries", + "items": { + "$ref": "#/components/schemas/AuditRecord" + } + }, + "totalCount": { + "type": "integer", + "format": "int64", + "description": "Total number of matching entries" + }, + "page": { + "type": "integer", + "format": "int32", + "description": "Current page number (0-based)" + }, + "pageSize": { + "type": "integer", + "format": "int32", + "description": "Page size" + }, + "totalPages": { + "type": "integer", + "format": "int32", + "description": "Total number of pages" + } + } + }, + "AuditRecord": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "timestamp": { + "type": "string", + "format": "date-time" + }, + "username": { + "type": "string" + }, + "action": { + "type": "string" + }, + "category": { + "type": "string", + "enum": [ + "INFRA", + "AUTH", + "USER_MGMT", + "CONFIG", + "RBAC", + "AGENT" + ] + }, + "target": { + "type": "string" + }, + "detail": { + "type": "object", + "additionalProperties": { + "type": "object" + } + }, + "result": { + "type": "string", + "enum": [ + "SUCCESS", + "FAILURE" + ] + }, + "ipAddress": { + "type": "string" + }, + "userAgent": { + "type": "string" + } + } + } + }, + "securitySchemes": { + "bearer": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + } + } + } +} diff --git a/ui/src/api/queries/diagrams.ts b/ui/src/api/queries/diagrams.ts index 7dde3bd6..1c805fe1 100644 --- a/ui/src/api/queries/diagrams.ts +++ b/ui/src/api/queries/diagrams.ts @@ -12,19 +12,32 @@ export interface DiagramNode { children?: DiagramNode[]; } -interface DiagramLayout { +export interface DiagramEdge { + sourceId: string; + targetId: string; + label?: string; + points: number[][]; +} + +export interface DiagramLayout { width?: number; height?: number; nodes?: DiagramNode[]; - edges?: Array<{ from?: string; to?: string }>; + edges?: DiagramEdge[]; } -export function useDiagramLayout(contentHash: string | null) { +export function useDiagramLayout( + contentHash: string | null, + direction: 'LR' | 'TB' = 'LR', +) { return useQuery({ - queryKey: ['diagrams', 'layout', contentHash], + queryKey: ['diagrams', 'layout', contentHash, direction], queryFn: async () => { const { data, error } = await api.GET('/diagrams/{contentHash}/render', { - params: { path: { contentHash: contentHash! } }, + params: { + path: { contentHash: contentHash! }, + query: { direction }, + }, headers: { Accept: 'application/json' }, }); if (error) throw new Error('Failed to load diagram layout'); @@ -34,15 +47,19 @@ export function useDiagramLayout(contentHash: string | null) { }); } -export function useDiagramByRoute(application: string | undefined, routeId: string | undefined) { +export function useDiagramByRoute( + application: string | undefined, + routeId: string | undefined, + direction: 'LR' | 'TB' = 'LR', +) { return useQuery({ - queryKey: ['diagrams', 'byRoute', application, routeId], + queryKey: ['diagrams', 'byRoute', application, routeId, direction], queryFn: async () => { const { data, error } = await api.GET('/diagrams', { - params: { query: { application: application!, routeId: routeId! } }, + params: { query: { application: application!, routeId: routeId!, direction } }, }); if (error) throw new Error('Failed to load diagram for route'); - return data!; + return data as DiagramLayout; }, enabled: !!application && !!routeId, }); diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 90bc2e28..79389d45 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -3642,6 +3642,8 @@ export interface operations { query: { application: string; routeId: string; + /** @description Layout direction: LR (left-to-right) or TB (top-to-bottom) */ + direction?: "LR" | "TB"; }; header?: never; path?: never; @@ -3671,7 +3673,10 @@ export interface operations { }; renderDiagram: { parameters: { - query?: never; + query?: { + /** @description Layout direction: LR (left-to-right) or TB (top-to-bottom) */ + direction?: "LR" | "TB"; + }; header?: never; path: { contentHash: string; diff --git a/ui/src/components/ProcessDiagram/CompoundNode.tsx b/ui/src/components/ProcessDiagram/CompoundNode.tsx new file mode 100644 index 00000000..63ea6ca9 --- /dev/null +++ b/ui/src/components/ProcessDiagram/CompoundNode.tsx @@ -0,0 +1,82 @@ +import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; +import type { NodeConfig } from './types'; +import { colorForType } from './node-colors'; +import { DiagramNode } from './DiagramNode'; + +const HEADER_HEIGHT = 22; +const CORNER_RADIUS = 4; + +interface CompoundNodeProps { + node: DiagramNodeType; + selectedNodeId?: string; + hoveredNodeId: string | null; + nodeConfigs?: Map; + onNodeClick: (nodeId: string) => void; + onNodeEnter: (nodeId: string) => void; + onNodeLeave: () => void; +} + +export function CompoundNode({ + node, selectedNodeId, hoveredNodeId, nodeConfigs, + onNodeClick, onNodeEnter, onNodeLeave, +}: CompoundNodeProps) { + const x = node.x ?? 0; + const y = node.y ?? 0; + const w = node.width ?? 200; + const h = node.height ?? 100; + const color = colorForType(node.type); + const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; + const label = node.label ? `${typeName}: ${node.label}` : typeName; + + return ( + + {/* Container body */} + + + {/* Colored header bar */} + + + + {/* Header label */} + + {label} + + + {/* Children nodes (positioned relative to compound) */} + {node.children?.map(child => ( + child.id && onNodeClick(child.id)} + onMouseEnter={() => child.id && onNodeEnter(child.id)} + onMouseLeave={onNodeLeave} + /> + ))} + + ); +} diff --git a/ui/src/components/ProcessDiagram/ConfigBadge.tsx b/ui/src/components/ProcessDiagram/ConfigBadge.tsx new file mode 100644 index 00000000..ccbdff60 --- /dev/null +++ b/ui/src/components/ProcessDiagram/ConfigBadge.tsx @@ -0,0 +1,48 @@ +import type { NodeConfig } from './types'; + +const BADGE_HEIGHT = 14; +const BADGE_RADIUS = 7; +const BADGE_FONT_SIZE = 8; +const BADGE_GAP = 4; + +interface ConfigBadgeProps { + nodeWidth: number; + config: NodeConfig; +} + +export function ConfigBadge({ nodeWidth, config }: ConfigBadgeProps) { + const badges: { label: string; color: string }[] = []; + if (config.tapExpression) badges.push({ label: 'TAP', color: '#7C3AED' }); + if (config.traceEnabled) badges.push({ label: 'TRACE', color: '#1A7F8E' }); + if (badges.length === 0) return null; + + let xOffset = nodeWidth; + return ( + + {badges.map((badge, i) => { + const textWidth = badge.label.length * 5.5 + 8; + xOffset -= textWidth + (i > 0 ? BADGE_GAP : 0); + return ( + + + + {badge.label} + + + ); + })} + + ); +} diff --git a/ui/src/components/ProcessDiagram/DiagramEdge.tsx b/ui/src/components/ProcessDiagram/DiagramEdge.tsx new file mode 100644 index 00000000..7c6c5a9c --- /dev/null +++ b/ui/src/components/ProcessDiagram/DiagramEdge.tsx @@ -0,0 +1,49 @@ +import type { DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'; + +interface DiagramEdgeProps { + edge: DiagramEdgeType; + offsetY?: number; +} + +export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) { + const pts = edge.points; + if (!pts || pts.length < 2) return null; + + // Build SVG path: move to first point, then cubic bezier or line to rest + let d = `M ${pts[0][0]} ${pts[0][1] + offsetY}`; + + if (pts.length === 2) { + d += ` L ${pts[1][0]} ${pts[1][1] + offsetY}`; + } else if (pts.length === 4) { + // 4 points: start, control1, control2, end → cubic bezier + d += ` C ${pts[1][0]} ${pts[1][1] + offsetY}, ${pts[2][0]} ${pts[2][1] + offsetY}, ${pts[3][0]} ${pts[3][1] + offsetY}`; + } else { + // Multiple points: connect with line segments through intermediate points + for (let i = 1; i < pts.length; i++) { + d += ` L ${pts[i][0]} ${pts[i][1] + offsetY}`; + } + } + + return ( + + + {edge.label && pts.length >= 2 && ( + + {edge.label} + + )} + + ); +} diff --git a/ui/src/components/ProcessDiagram/DiagramNode.tsx b/ui/src/components/ProcessDiagram/DiagramNode.tsx new file mode 100644 index 00000000..0702f807 --- /dev/null +++ b/ui/src/components/ProcessDiagram/DiagramNode.tsx @@ -0,0 +1,93 @@ +import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; +import type { NodeConfig } from './types'; +import { colorForType, iconForType } from './node-colors'; +import { ConfigBadge } from './ConfigBadge'; + +const TOP_BAR_HEIGHT = 6; +const CORNER_RADIUS = 4; + +interface DiagramNodeProps { + node: DiagramNodeType; + isHovered: boolean; + isSelected: boolean; + config?: NodeConfig; + onClick: () => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +export function DiagramNode({ + node, isHovered, isSelected, config, onClick, onMouseEnter, onMouseLeave, +}: DiagramNodeProps) { + const x = node.x ?? 0; + const y = node.y ?? 0; + const w = node.width ?? 120; + const h = node.height ?? 40; + const color = colorForType(node.type); + const icon = iconForType(node.type); + + // Extract label parts: type name and detail + const typeName = node.type?.replace(/^EIP_/, '').replace(/_/g, ' ') ?? ''; + const detail = node.label || ''; + + return ( + { e.stopPropagation(); onClick(); }} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + style={{ cursor: 'pointer' }} + > + {/* Selection ring */} + {isSelected && ( + + )} + + {/* Card background */} + + + {/* Colored top bar */} + + + + {/* Icon */} + + {icon} + + + {/* Type name */} + + {typeName} + + + {/* Detail label (truncated) */} + {detail && detail !== typeName && ( + + {detail.length > 22 ? detail.slice(0, 20) + '...' : detail} + + )} + + {/* Config badges */} + {config && } + + ); +} diff --git a/ui/src/components/ProcessDiagram/ErrorSection.tsx b/ui/src/components/ProcessDiagram/ErrorSection.tsx new file mode 100644 index 00000000..580da0b5 --- /dev/null +++ b/ui/src/components/ProcessDiagram/ErrorSection.tsx @@ -0,0 +1,93 @@ +import type { DiagramSection } from './types'; +import type { NodeConfig } from './types'; +import { DiagramEdge } from './DiagramEdge'; +import { DiagramNode } from './DiagramNode'; +import { CompoundNode } from './CompoundNode'; +import { isCompoundType } from './node-colors'; + +interface ErrorSectionProps { + section: DiagramSection; + totalWidth: number; + selectedNodeId?: string; + hoveredNodeId: string | null; + nodeConfigs?: Map; + onNodeClick: (nodeId: string) => void; + onNodeEnter: (nodeId: string) => void; + onNodeLeave: () => void; +} + +export function ErrorSection({ + section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, + onNodeClick, onNodeEnter, onNodeLeave, +}: ErrorSectionProps) { + return ( + + {/* Divider line */} + + + {/* Section label */} + + {section.label} + + + {/* Subtle red tint background */} + + + {/* Edges */} + + {section.edges.map((edge, i) => ( + + ))} + + + {/* Nodes */} + + {section.nodes.map(node => { + if (isCompoundType(node.type) && node.children && node.children.length > 0) { + return ( + + ); + } + return ( + node.id && onNodeClick(node.id)} + onMouseEnter={() => node.id && onNodeEnter(node.id)} + onMouseLeave={onNodeLeave} + /> + ); + })} + + + ); +} diff --git a/ui/src/components/ProcessDiagram/NodeToolbar.tsx b/ui/src/components/ProcessDiagram/NodeToolbar.tsx new file mode 100644 index 00000000..ccfb2651 --- /dev/null +++ b/ui/src/components/ProcessDiagram/NodeToolbar.tsx @@ -0,0 +1,118 @@ +import { useCallback, useRef, useState } from 'react'; +import type { NodeAction } from './types'; + +const TOOLBAR_HEIGHT = 28; +const TOOLBAR_WIDTH = 140; +const HIDE_DELAY = 150; + +interface NodeToolbarProps { + nodeId: string; + nodeX: number; + nodeY: number; + nodeWidth: number; + onAction: (nodeId: string, action: NodeAction) => void; + onMouseEnter: () => void; + onMouseLeave: () => void; +} + +const ACTIONS: { label: string; icon: string; action: NodeAction; title: string }[] = [ + { label: 'Inspect', icon: '\uD83D\uDD0D', action: 'inspect', title: 'Inspect node' }, + { label: 'Trace', icon: 'T', action: 'toggle-trace', title: 'Toggle tracing' }, + { label: 'Tap', icon: '\u270E', action: 'configure-tap', title: 'Configure tap' }, + { label: 'More', icon: '\u22EF', action: 'copy-id', title: 'Copy processor ID' }, +]; + +export function NodeToolbar({ + nodeId, nodeX, nodeY, nodeWidth, onAction, onMouseEnter, onMouseLeave, +}: NodeToolbarProps) { + const x = nodeX + (nodeWidth - TOOLBAR_WIDTH) / 2; + const y = nodeY - TOOLBAR_HEIGHT - 6; + + return ( + +
+ {ACTIONS.map(a => ( + + ))} +
+
+ ); +} + +/** Hook to manage toolbar visibility with hide delay */ +export function useToolbarHover() { + const [hoveredNodeId, setHoveredNodeId] = useState(null); + const hideTimer = useRef | null>(null); + + const onNodeEnter = useCallback((nodeId: string) => { + if (hideTimer.current) { + clearTimeout(hideTimer.current); + hideTimer.current = null; + } + setHoveredNodeId(nodeId); + }, []); + + const onNodeLeave = useCallback(() => { + hideTimer.current = setTimeout(() => { + setHoveredNodeId(null); + hideTimer.current = null; + }, HIDE_DELAY); + }, []); + + const onToolbarEnter = useCallback(() => { + if (hideTimer.current) { + clearTimeout(hideTimer.current); + hideTimer.current = null; + } + }, []); + + const onToolbarLeave = useCallback(() => { + hideTimer.current = setTimeout(() => { + setHoveredNodeId(null); + hideTimer.current = null; + }, HIDE_DELAY); + }, []); + + return { hoveredNodeId, onNodeEnter, onNodeLeave, onToolbarEnter, onToolbarLeave }; +} diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.module.css b/ui/src/components/ProcessDiagram/ProcessDiagram.module.css new file mode 100644 index 00000000..9211045a --- /dev/null +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.module.css @@ -0,0 +1,79 @@ +.container { + position: relative; + width: 100%; + height: 100%; + min-height: 300px; + overflow: hidden; + background: var(--bg-surface, #FFFFFF); + border: 1px solid var(--border, #E4DFD8); + border-radius: var(--radius-md, 8px); +} + +.svg { + width: 100%; + height: 100%; + display: block; + outline: none; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 300px; + color: var(--text-muted, #9C9184); + font-size: 14px; +} + +.error { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + min-height: 300px; + color: var(--error, #C0392B); + font-size: 14px; +} + +.zoomControls { + position: absolute; + bottom: 12px; + right: 12px; + display: flex; + align-items: center; + gap: 4px; + background: var(--bg-surface, #FFFFFF); + border: 1px solid var(--border, #E4DFD8); + border-radius: var(--radius-sm, 5px); + padding: 4px; + box-shadow: var(--shadow-md, 0 2px 8px rgba(44, 37, 32, 0.08)); +} + +.zoomBtn { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border: none; + background: transparent; + color: var(--text-primary, #1A1612); + font-size: 16px; + cursor: pointer; + border-radius: var(--radius-sm, 5px); +} + +.zoomBtn:hover { + background: var(--bg-hover, #F5F0EA); +} + +.zoomLevel { + font-size: 11px; + color: var(--text-muted, #9C9184); + min-width: 36px; + text-align: center; + font-variant-numeric: tabular-nums; +} diff --git a/ui/src/components/ProcessDiagram/ProcessDiagram.tsx b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx new file mode 100644 index 00000000..bfec2b0b --- /dev/null +++ b/ui/src/components/ProcessDiagram/ProcessDiagram.tsx @@ -0,0 +1,221 @@ +import { useCallback, useEffect } from 'react'; +import type { ProcessDiagramProps } from './types'; +import { useDiagramData } from './useDiagramData'; +import { useZoomPan } from './useZoomPan'; +import { useToolbarHover, NodeToolbar } from './NodeToolbar'; +import { DiagramNode } from './DiagramNode'; +import { DiagramEdge } from './DiagramEdge'; +import { CompoundNode } from './CompoundNode'; +import { ErrorSection } from './ErrorSection'; +import { ZoomControls } from './ZoomControls'; +import { isCompoundType } from './node-colors'; +import styles from './ProcessDiagram.module.css'; + +const PADDING = 40; + +export function ProcessDiagram({ + application, + routeId, + direction = 'LR', + selectedNodeId, + onNodeSelect, + onNodeAction, + nodeConfigs, + className, +}: ProcessDiagramProps) { + const { sections, totalWidth, totalHeight, isLoading, error } = useDiagramData( + application, routeId, direction, + ); + + const zoom = useZoomPan(); + const toolbar = useToolbarHover(); + + const contentWidth = totalWidth + PADDING * 2; + const contentHeight = totalHeight + PADDING * 2; + + // Fit to view on first data load + useEffect(() => { + if (totalWidth > 0 && totalHeight > 0) { + zoom.fitToView(contentWidth, contentHeight); + } + }, [totalWidth, totalHeight]); // eslint-disable-line react-hooks/exhaustive-deps + + const handleNodeClick = useCallback( + (nodeId: string) => { onNodeSelect?.(nodeId); }, + [onNodeSelect], + ); + + const handleNodeAction = useCallback( + (nodeId: string, action: import('./types').NodeAction) => { + if (action === 'inspect') onNodeSelect?.(nodeId); + onNodeAction?.(nodeId, action); + }, + [onNodeSelect, onNodeAction], + ); + + const handleKeyDown = useCallback( + (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + onNodeSelect?.(''); + return; + } + zoom.onKeyDown(e, contentWidth, contentHeight); + }, + [onNodeSelect, zoom, contentWidth, contentHeight], + ); + + if (isLoading) { + return ( +
+
Loading diagram...
+
+ ); + } + + if (error) { + return ( +
+
Failed to load diagram
+
+ ); + } + + if (sections.length === 0) { + return ( +
+
No diagram data available
+
+ ); + } + + const mainSection = sections[0]; + const errorSections = sections.slice(1); + + return ( +
+ onNodeSelect?.('')} + > + + + + + + + + {/* Main section edges */} + + {mainSection.edges.map((edge, i) => ( + + ))} + + + {/* Main section nodes */} + + {mainSection.nodes.map(node => { + if (isCompoundType(node.type) && node.children && node.children.length > 0) { + return ( + + ); + } + return ( + node.id && handleNodeClick(node.id)} + onMouseEnter={() => node.id && toolbar.onNodeEnter(node.id)} + onMouseLeave={toolbar.onNodeLeave} + /> + ); + })} + + + {/* Toolbar for hovered node */} + {toolbar.hoveredNodeId && onNodeAction && (() => { + const hNode = findNodeById(sections, toolbar.hoveredNodeId!); + if (!hNode) return null; + return ( + + ); + })()} + + {/* Error handler sections */} + {errorSections.map((section, i) => ( + + ))} + + + + zoom.fitToView(contentWidth, contentHeight)} + scale={zoom.state.scale} + /> +
+ ); +} + +function findNodeById( + sections: import('./types').DiagramSection[], + nodeId: string, +): import('../../api/queries/diagrams').DiagramNode | undefined { + for (const section of sections) { + for (const node of section.nodes) { + if (node.id === nodeId) return node; + if (node.children) { + const child = node.children.find(c => c.id === nodeId); + if (child) return child; + } + } + } + return undefined; +} diff --git a/ui/src/components/ProcessDiagram/ZoomControls.tsx b/ui/src/components/ProcessDiagram/ZoomControls.tsx new file mode 100644 index 00000000..a157a850 --- /dev/null +++ b/ui/src/components/ProcessDiagram/ZoomControls.tsx @@ -0,0 +1,19 @@ +import styles from './ProcessDiagram.module.css'; + +interface ZoomControlsProps { + onZoomIn: () => void; + onZoomOut: () => void; + onFitToView: () => void; + scale: number; +} + +export function ZoomControls({ onZoomIn, onZoomOut, onFitToView, scale }: ZoomControlsProps) { + return ( +
+ + {Math.round(scale * 100)}% + + +
+ ); +} diff --git a/ui/src/components/ProcessDiagram/index.ts b/ui/src/components/ProcessDiagram/index.ts new file mode 100644 index 00000000..6cfa0449 --- /dev/null +++ b/ui/src/components/ProcessDiagram/index.ts @@ -0,0 +1,2 @@ +export { ProcessDiagram } from './ProcessDiagram'; +export type { ProcessDiagramProps, NodeAction, NodeConfig } from './types'; diff --git a/ui/src/components/ProcessDiagram/node-colors.ts b/ui/src/components/ProcessDiagram/node-colors.ts new file mode 100644 index 00000000..2a521b76 --- /dev/null +++ b/ui/src/components/ProcessDiagram/node-colors.ts @@ -0,0 +1,93 @@ +/** Maps backend NodeType strings to CSS color values using design system tokens. */ + +const ENDPOINT_COLOR = '#1A7F8E'; // --running +const PROCESSOR_COLOR = '#C6820E'; // --amber +const TARGET_COLOR = '#3D7C47'; // --success +const EIP_COLOR = '#7C3AED'; // --purple +const ERROR_COLOR = '#C0392B'; // --error +const CROSS_ROUTE_COLOR = '#06B6D4'; +const DEFAULT_COLOR = '#9C9184'; // --text-muted + +const TYPE_MAP: Record = { + ENDPOINT: ENDPOINT_COLOR, + + PROCESSOR: PROCESSOR_COLOR, + BEAN: PROCESSOR_COLOR, + LOG: PROCESSOR_COLOR, + SET_HEADER: PROCESSOR_COLOR, + SET_BODY: PROCESSOR_COLOR, + TRANSFORM: PROCESSOR_COLOR, + MARSHAL: PROCESSOR_COLOR, + UNMARSHAL: PROCESSOR_COLOR, + + TO: TARGET_COLOR, + TO_DYNAMIC: TARGET_COLOR, + DIRECT: TARGET_COLOR, + SEDA: TARGET_COLOR, + + EIP_CHOICE: EIP_COLOR, + EIP_WHEN: EIP_COLOR, + EIP_OTHERWISE: EIP_COLOR, + EIP_SPLIT: EIP_COLOR, + EIP_MULTICAST: EIP_COLOR, + EIP_LOOP: EIP_COLOR, + EIP_AGGREGATE: EIP_COLOR, + EIP_FILTER: EIP_COLOR, + EIP_RECIPIENT_LIST: EIP_COLOR, + EIP_ROUTING_SLIP: EIP_COLOR, + EIP_DYNAMIC_ROUTER: EIP_COLOR, + EIP_LOAD_BALANCE: EIP_COLOR, + EIP_THROTTLE: EIP_COLOR, + EIP_DELAY: EIP_COLOR, + EIP_IDEMPOTENT_CONSUMER: EIP_COLOR, + EIP_CIRCUIT_BREAKER: EIP_COLOR, + EIP_PIPELINE: EIP_COLOR, + + ERROR_HANDLER: ERROR_COLOR, + ON_EXCEPTION: ERROR_COLOR, + TRY_CATCH: ERROR_COLOR, + DO_TRY: ERROR_COLOR, + DO_CATCH: ERROR_COLOR, + DO_FINALLY: ERROR_COLOR, + + EIP_WIRE_TAP: CROSS_ROUTE_COLOR, + EIP_ENRICH: CROSS_ROUTE_COLOR, + EIP_POLL_ENRICH: CROSS_ROUTE_COLOR, +}; + +const COMPOUND_TYPES = new Set([ + 'EIP_CHOICE', 'EIP_SPLIT', 'TRY_CATCH', 'DO_TRY', + 'EIP_LOOP', 'EIP_MULTICAST', 'EIP_AGGREGATE', + 'ON_EXCEPTION', 'ERROR_HANDLER', +]); + +const ERROR_COMPOUND_TYPES = new Set([ + 'ON_EXCEPTION', 'ERROR_HANDLER', +]); + +export function colorForType(type: string | undefined): string { + if (!type) return DEFAULT_COLOR; + return TYPE_MAP[type] ?? DEFAULT_COLOR; +} + +export function isCompoundType(type: string | undefined): boolean { + return !!type && COMPOUND_TYPES.has(type); +} + +export function isErrorCompoundType(type: string | undefined): boolean { + return !!type && ERROR_COMPOUND_TYPES.has(type); +} + +/** Icon character for a node type */ +export function iconForType(type: string | undefined): string { + if (!type) return '\u2699'; // gear + const t = type.toUpperCase(); + if (t === 'ENDPOINT') return '\u25B6'; // play + if (t === 'TO' || t === 'TO_DYNAMIC' || t === 'DIRECT' || t === 'SEDA') return '\u25A0'; // square + if (t.startsWith('EIP_CHOICE') || t === 'EIP_WHEN' || t === 'EIP_OTHERWISE') return '\u25C6'; // diamond + if (t === 'ON_EXCEPTION' || t === 'ERROR_HANDLER' || t.startsWith('TRY') || t.startsWith('DO_')) return '\u26A0'; // warning + if (t === 'EIP_SPLIT' || t === 'EIP_MULTICAST') return '\u2442'; // fork + if (t === 'EIP_LOOP') return '\u21BA'; // loop arrow + if (t === 'EIP_WIRE_TAP' || t === 'EIP_ENRICH' || t === 'EIP_POLL_ENRICH') return '\u2197'; // arrow + return '\u2699'; // gear +} diff --git a/ui/src/components/ProcessDiagram/types.ts b/ui/src/components/ProcessDiagram/types.ts new file mode 100644 index 00000000..5ab1e32f --- /dev/null +++ b/ui/src/components/ProcessDiagram/types.ts @@ -0,0 +1,27 @@ +import type { DiagramNode, DiagramEdge } from '../../api/queries/diagrams'; + +export type NodeAction = 'inspect' | 'toggle-trace' | 'configure-tap' | 'copy-id'; + +export interface NodeConfig { + traceEnabled?: boolean; + tapExpression?: string; +} + +export interface DiagramSection { + label: string; + nodes: DiagramNode[]; + edges: DiagramEdge[]; + offsetY: number; + variant?: 'error'; +} + +export interface ProcessDiagramProps { + application: string; + routeId: string; + direction?: 'LR' | 'TB'; + selectedNodeId?: string; + onNodeSelect?: (nodeId: string) => void; + onNodeAction?: (nodeId: string, action: NodeAction) => void; + nodeConfigs?: Map; + className?: string; +} diff --git a/ui/src/components/ProcessDiagram/useDiagramData.ts b/ui/src/components/ProcessDiagram/useDiagramData.ts new file mode 100644 index 00000000..ca0547a7 --- /dev/null +++ b/ui/src/components/ProcessDiagram/useDiagramData.ts @@ -0,0 +1,115 @@ +import { useMemo } from 'react'; +import { useDiagramByRoute } from '../../api/queries/diagrams'; +import type { DiagramNode, DiagramEdge } from '../../api/queries/diagrams'; +import type { DiagramSection } from './types'; +import { isErrorCompoundType } from './node-colors'; + +const SECTION_GAP = 40; + +export function useDiagramData( + application: string, + routeId: string, + direction: 'LR' | 'TB' = 'LR', +) { + const { data: layout, isLoading, error } = useDiagramByRoute(application, routeId, direction); + + const result = useMemo(() => { + if (!layout?.nodes) { + return { sections: [], totalWidth: 0, totalHeight: 0 }; + } + + const allEdges = layout.edges ?? []; + + // Separate main nodes from error handler compound sections + const mainNodes: DiagramNode[] = []; + const errorSections: { label: string; nodes: DiagramNode[] }[] = []; + + for (const node of layout.nodes) { + if (isErrorCompoundType(node.type) && node.children && node.children.length > 0) { + errorSections.push({ + label: node.label || 'Error Handler', + nodes: node.children, + }); + } else { + mainNodes.push(node); + } + } + + // Collect node IDs for edge filtering + const mainNodeIds = new Set(); + collectNodeIds(mainNodes, mainNodeIds); + + const mainEdges = allEdges.filter( + e => mainNodeIds.has(e.sourceId) && mainNodeIds.has(e.targetId), + ); + + // Compute main section bounding box + const mainBounds = computeBounds(mainNodes); + + const sections: DiagramSection[] = [ + { + label: 'Main Route', + nodes: mainNodes, + edges: mainEdges, + offsetY: 0, + }, + ]; + + let currentY = mainBounds.maxY + SECTION_GAP; + + for (const es of errorSections) { + const errorNodeIds = new Set(); + collectNodeIds(es.nodes, errorNodeIds); + const errorEdges = allEdges.filter( + e => errorNodeIds.has(e.sourceId) && errorNodeIds.has(e.targetId), + ); + + sections.push({ + label: es.label, + nodes: es.nodes, + edges: errorEdges, + offsetY: currentY, + variant: 'error', + }); + + const errorBounds = computeBounds(es.nodes); + currentY += (errorBounds.maxY - errorBounds.minY) + SECTION_GAP; + } + + const totalWidth = layout.width ?? mainBounds.maxX; + const totalHeight = currentY; + + return { sections, totalWidth, totalHeight }; + }, [layout]); + + return { ...result, isLoading, error }; +} + +function collectNodeIds(nodes: DiagramNode[], set: Set) { + for (const n of nodes) { + if (n.id) set.add(n.id); + if (n.children) collectNodeIds(n.children, set); + } +} + +function computeBounds(nodes: DiagramNode[]): { + minX: number; minY: number; maxX: number; maxY: number; +} { + let minX = Infinity, minY = Infinity, maxX = 0, maxY = 0; + for (const n of nodes) { + const x = n.x ?? 0; + const y = n.y ?? 0; + const w = n.width ?? 80; + const h = n.height ?? 40; + if (x < minX) minX = x; + if (y < minY) minY = y; + if (x + w > maxX) maxX = x + w; + if (y + h > maxY) maxY = y + h; + if (n.children) { + const childBounds = computeBounds(n.children); + if (childBounds.maxX > maxX) maxX = childBounds.maxX; + if (childBounds.maxY > maxY) maxY = childBounds.maxY; + } + } + return { minX: minX === Infinity ? 0 : minX, minY: minY === Infinity ? 0 : minY, maxX, maxY }; +} diff --git a/ui/src/components/ProcessDiagram/useZoomPan.ts b/ui/src/components/ProcessDiagram/useZoomPan.ts new file mode 100644 index 00000000..6598177f --- /dev/null +++ b/ui/src/components/ProcessDiagram/useZoomPan.ts @@ -0,0 +1,171 @@ +import { useCallback, useRef, useState } from 'react'; + +interface ZoomPanState { + scale: number; + translateX: number; + translateY: number; +} + +const MIN_SCALE = 0.25; +const MAX_SCALE = 4.0; +const ZOOM_STEP = 0.15; +const FIT_PADDING = 40; + +export function useZoomPan() { + const [state, setState] = useState({ + scale: 1, + translateX: 0, + translateY: 0, + }); + const isPanning = useRef(false); + const panStart = useRef({ x: 0, y: 0 }); + const containerRef = useRef(null); + + const clampScale = (s: number) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s)); + + const viewBox = useCallback( + (contentWidth: number, contentHeight: number) => { + const vw = contentWidth / state.scale; + const vh = contentHeight / state.scale; + const vx = -state.translateX / state.scale; + const vy = -state.translateY / state.scale; + return `${vx} ${vy} ${vw} ${vh}`; + }, + [state], + ); + + const onWheel = useCallback( + (e: React.WheelEvent) => { + e.preventDefault(); + const direction = e.deltaY < 0 ? 1 : -1; + const factor = 1 + direction * ZOOM_STEP; + + setState(prev => { + const newScale = clampScale(prev.scale * factor); + // Zoom centered on cursor + const rect = (e.currentTarget as SVGSVGElement).getBoundingClientRect(); + const cursorX = e.clientX - rect.left; + const cursorY = e.clientY - rect.top; + const scaleRatio = newScale / prev.scale; + const newTx = cursorX - scaleRatio * (cursorX - prev.translateX); + const newTy = cursorY - scaleRatio * (cursorY - prev.translateY); + return { scale: newScale, translateX: newTx, translateY: newTy }; + }); + }, + [], + ); + + const onPointerDown = useCallback( + (e: React.PointerEvent) => { + // Only pan on background click (not on nodes) + if ((e.target as Element).closest('[data-node-id]')) return; + isPanning.current = true; + panStart.current = { x: e.clientX - state.translateX, y: e.clientY - state.translateY }; + (e.currentTarget as SVGSVGElement).setPointerCapture(e.pointerId); + }, + [state.translateX, state.translateY], + ); + + const onPointerMove = useCallback( + (e: React.PointerEvent) => { + if (!isPanning.current) return; + setState(prev => ({ + ...prev, + translateX: e.clientX - panStart.current.x, + translateY: e.clientY - panStart.current.y, + })); + }, + [], + ); + + const onPointerUp = useCallback( + (e: React.PointerEvent) => { + isPanning.current = false; + (e.currentTarget as SVGSVGElement).releasePointerCapture(e.pointerId); + }, + [], + ); + + const zoomIn = useCallback(() => { + setState(prev => { + const newScale = clampScale(prev.scale * (1 + ZOOM_STEP)); + const container = containerRef.current; + if (!container) return { ...prev, scale: newScale }; + const cx = container.clientWidth / 2; + const cy = container.clientHeight / 2; + const ratio = newScale / prev.scale; + return { + scale: newScale, + translateX: cx - ratio * (cx - prev.translateX), + translateY: cy - ratio * (cy - prev.translateY), + }; + }); + }, []); + + const zoomOut = useCallback(() => { + setState(prev => { + const newScale = clampScale(prev.scale * (1 - ZOOM_STEP)); + const container = containerRef.current; + if (!container) return { ...prev, scale: newScale }; + const cx = container.clientWidth / 2; + const cy = container.clientHeight / 2; + const ratio = newScale / prev.scale; + return { + scale: newScale, + translateX: cx - ratio * (cx - prev.translateX), + translateY: cy - ratio * (cy - prev.translateY), + }; + }); + }, []); + + const fitToView = useCallback( + (contentWidth: number, contentHeight: number) => { + const container = containerRef.current; + if (!container) return; + const cw = container.clientWidth - FIT_PADDING * 2; + const ch = container.clientHeight - FIT_PADDING * 2; + const scaleX = cw / contentWidth; + const scaleY = ch / contentHeight; + const newScale = clampScale(Math.min(scaleX, scaleY)); + const tx = (container.clientWidth - contentWidth * newScale) / 2; + const ty = (container.clientHeight - contentHeight * newScale) / 2; + setState({ scale: newScale, translateX: tx, translateY: ty }); + }, + [], + ); + + const onKeyDown = useCallback( + (e: React.KeyboardEvent, contentWidth: number, contentHeight: number) => { + switch (e.key) { + case '+': + case '=': + e.preventDefault(); + zoomIn(); + break; + case '-': + e.preventDefault(); + zoomOut(); + break; + case '0': + e.preventDefault(); + fitToView(contentWidth, contentHeight); + break; + } + }, + [zoomIn, zoomOut, fitToView], + ); + + return { + state, + containerRef, + viewBox, + onWheel, + onPointerDown, + onPointerMove, + onPointerUp, + zoomIn, + zoomOut, + fitToView, + onKeyDown, + }; +} diff --git a/ui/src/pages/DevDiagram/DevDiagram.module.css b/ui/src/pages/DevDiagram/DevDiagram.module.css new file mode 100644 index 00000000..aa1ff878 --- /dev/null +++ b/ui/src/pages/DevDiagram/DevDiagram.module.css @@ -0,0 +1,131 @@ +.page { + display: flex; + flex-direction: column; + height: 100%; + gap: 12px; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + flex-shrink: 0; +} + +.header h2 { + margin: 0; + font-size: 18px; + color: var(--text-primary, #1A1612); +} + +.controls { + display: flex; + gap: 8px; +} + +.controls select { + padding: 6px 10px; + border: 1px solid var(--border, #E4DFD8); + border-radius: var(--radius-sm, 5px); + background: var(--bg-surface, #FFFFFF); + color: var(--text-primary, #1A1612); + font-size: 13px; +} + +.content { + display: flex; + gap: 12px; + flex: 1; + min-height: 0; +} + +.diagramPane { + flex: 1; + min-width: 0; +} + +.placeholder { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 400px; + border: 1px dashed var(--border, #E4DFD8); + border-radius: var(--radius-md, 8px); + color: var(--text-muted, #9C9184); + font-size: 14px; +} + +.sidePanel { + width: 280px; + flex-shrink: 0; + padding: 12px; + border: 1px solid var(--border, #E4DFD8); + border-radius: var(--radius-md, 8px); + background: var(--bg-surface, #FFFFFF); + overflow-y: auto; +} + +.sidePanel h3 { + margin: 0 0 8px; + font-size: 13px; + font-weight: 600; + color: var(--text-secondary, #5C5347); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.nodeInfo { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; +} + +.field { + display: flex; + flex-direction: column; + gap: 2px; +} + +.fieldLabel { + font-size: 11px; + color: var(--text-muted, #9C9184); + text-transform: uppercase; +} + +.field code, .field pre { + font-family: var(--font-mono, monospace); + font-size: 12px; + color: var(--text-primary, #1A1612); + background: var(--bg-inset, #F0EDE8); + padding: 4px 6px; + border-radius: 3px; + margin: 0; + white-space: pre-wrap; + word-break: break-all; +} + +.hint { + color: var(--text-muted, #9C9184); + font-size: 12px; + font-style: italic; + margin: 0; +} + +.log { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 200px; + overflow-y: auto; +} + +.logEntry { + font-family: var(--font-mono, monospace); + font-size: 11px; + color: var(--text-secondary, #5C5347); + padding: 2px 0; + border-bottom: 1px solid var(--border-subtle, #EDE9E3); +} diff --git a/ui/src/pages/DevDiagram/DevDiagram.tsx b/ui/src/pages/DevDiagram/DevDiagram.tsx new file mode 100644 index 00000000..97646093 --- /dev/null +++ b/ui/src/pages/DevDiagram/DevDiagram.tsx @@ -0,0 +1,127 @@ +import { useMemo, useState } from 'react'; +import { ProcessDiagram } from '../../components/ProcessDiagram'; +import type { NodeConfig, NodeAction } from '../../components/ProcessDiagram'; +import { useRouteCatalog } from '../../api/queries/catalog'; +import styles from './DevDiagram.module.css'; + +export default function DevDiagram() { + const { data: catalog } = useRouteCatalog(); + + const [selectedApp, setSelectedApp] = useState(''); + const [selectedRoute, setSelectedRoute] = useState(''); + const [selectedNodeId, setSelectedNodeId] = useState(''); + const [direction, setDirection] = useState<'LR' | 'TB'>('LR'); + const [actionLog, setActionLog] = useState([]); + + // Extract unique applications and routes from catalog + const { apps, routes } = useMemo(() => { + if (!catalog) return { apps: [] as string[], routes: [] as string[] }; + const appSet = new Set(); + const routeList: { app: string; routeId: string }[] = []; + for (const entry of catalog as Array<{ application?: string; routeId?: string }>) { + if (entry.application) appSet.add(entry.application); + if (entry.application && entry.routeId) { + routeList.push({ app: entry.application, routeId: entry.routeId }); + } + } + const appArr = Array.from(appSet).sort(); + const filtered = selectedApp + ? routeList.filter(r => r.app === selectedApp).map(r => r.routeId) + : []; + return { apps: appArr, routes: filtered }; + }, [catalog, selectedApp]); + + // Mock node configs for testing + const nodeConfigs = useMemo(() => { + const map = new Map(); + // We'll add some mock configs if we have a route loaded + map.set('log1', { traceEnabled: true }); + map.set('to1', { tapExpression: '${header.orderId}' }); + return map; + }, []); + + const handleNodeAction = (nodeId: string, action: NodeAction) => { + const msg = `[${new Date().toLocaleTimeString()}] ${action}: ${nodeId}`; + setActionLog(prev => [msg, ...prev.slice(0, 19)]); + }; + + return ( +
+
+

Process Diagram (Dev)

+
+ + + +
+
+ +
+
+ {selectedApp && selectedRoute ? ( + + ) : ( +
+ Select an application and route to view the diagram +
+ )} +
+ +
+

Selected Node

+ {selectedNodeId ? ( +
+
+ Node ID + {selectedNodeId} +
+ {nodeConfigs.has(selectedNodeId) && ( +
+ Config +
{JSON.stringify(nodeConfigs.get(selectedNodeId), null, 2)}
+
+ )} +
+ ) : ( +

Click a node to inspect it

+ )} + +

Action Log

+
+ {actionLog.length === 0 ? ( +

Hover a node and use the toolbar

+ ) : ( + actionLog.map((msg, i) => ( +
{msg}
+ )) + )} +
+
+
+
+ ); +} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 48eadc04..3e6cbf1f 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -20,6 +20,7 @@ const DatabaseAdminPage = lazy(() => import('./pages/Admin/DatabaseAdminPage')); const OpenSearchAdminPage = lazy(() => import('./pages/Admin/OpenSearchAdminPage')); const AppConfigPage = lazy(() => import('./pages/Admin/AppConfigPage')); const SwaggerPage = lazy(() => import('./pages/Swagger/SwaggerPage')); +const DevDiagram = lazy(() => import('./pages/DevDiagram/DevDiagram')); function SuspenseWrapper({ children }: { children: React.ReactNode }) { return ( @@ -63,6 +64,7 @@ export const router = createBrowserRouter([ ], }, { path: 'api-docs', element: }, + { path: 'dev/diagram', element: }, ], }, ],