From 3d0a4d289bf252fc4928b87172cffedceaf54c2d Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:43:19 +0200 Subject: [PATCH 01/49] =?UTF-8?q?docs(alerts):=20Design=20spec=20=E2=80=94?= =?UTF-8?q?=20design-system=20alignment=20for=20/alerts=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rework all pages under /alerts to use @cameleer/design-system components and tokens. Unified DataTable shell for Inbox/All/History with expandable rows; DataTable + Dropdown + ConfirmDialog for Rules list; FormField grid + DataTable for Silences; DS Alert for wizard banners. Replaces undefined CSS variables (--bg, --fg, --muted, --accent) with DS tokens and removes raw /
` (RulesList, Silences), raw `
` → `DataTable` inside `tableStyles.tableSection`. +- Columns: **Name (Link) | Kind (Badge) | Severity (SeverityBadge) | Enabled (Toggle) | Targets (count) | Actions**. +- Actions cell: DS `Dropdown` for **Promote to env** (replaces raw `
` → `DataTable` below the form. +- Columns: **Matcher (MonoText) | Reason | Starts | Ends | End action**. +- `End` action → `ConfirmDialog`. +- Empty state: `EmptyState` "No active or scheduled silences." + +### Rule editor wizard (`/alerts/rules/new`, `/alerts/rules/:id`) + +Keep the current custom tab stepper — DS has no `Stepper`, and the existing layout is appropriate. + +Changes: +- `wizard.module.css` — replace undefined tokens with DS tokens. `.wizard` uses `--space-md` gap; `.steps` underline uses `--border-subtle`; `.stepActive` border uses `--amber` (the DS accent color); `.step` idle color uses `--text-muted`, active/done uses `--text-primary`. +- Promote banner → DS ``. +- Warnings block → DS `` with the list as children. +- Step body wraps in `sectionStyles.section` for card affordance matching other forms. + +## Shared changes + +### `alerts-page.module.css` + +Reduced to layout-only: + +```css +.page { + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-md); +} +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-sm); +} +.filterBar { + display: flex; + gap: var(--space-sm); + align-items: center; +} +``` + +Delete: `.row`, `.rowUnread`, `.body`, `.meta`, `.time`, `.message`, `.actions`, `.empty` — all replaced by DS components. + +### `AlertRow.tsx` + +**Delete.** Logic migrates into: +- A column renderer for the Title cell (handles the `Link` + `markRead` side-effect on click). +- An `expandedContent` renderer shared across the three list pages (extracted into `ui/src/pages/Alerts/alert-expanded.tsx`). +- An Ack action button rendered via DataTable `Actions` column. + +### `ConfirmDialog` migration + +Replaces native `confirm()` in `RulesListPage` (delete), `SilencesPage` (end), and the wizard if it grows a delete path (not currently present). + +### Helpers + +Two small pure-logic helpers, co-located in `ui/src/pages/Alerts/`: + +- `time-utils.ts` — `formatRelativeTime(iso: string, now?: Date): string` returning `2m ago` / `1h ago` / `3d ago`. With a Vitest. +- `severity-utils.ts` — `severityToAccent(severity: AlertSeverity): DataTableRowAccent` mapping `CRITICAL→error`, `MAJOR→warning`, `MINOR→warning`, `INFO→info`. With a Vitest. + +## Components touched + +| File | Change | +|------|--------| +| `ui/src/pages/Alerts/InboxPage.tsx` | Rewrite: DataTable + bulk toolbar + EmptyState | +| `ui/src/pages/Alerts/AllAlertsPage.tsx` | Rewrite: DataTable + ButtonGroup filter + EmptyState | +| `ui/src/pages/Alerts/HistoryPage.tsx` | Rewrite: DataTable + DateRangePicker + EmptyState | +| `ui/src/pages/Alerts/RulesListPage.tsx` | Table → DataTable; select → Dropdown; confirm → ConfirmDialog | +| `ui/src/pages/Alerts/SilencesPage.tsx` | Table → DataTable; FormField grid; confirm → ConfirmDialog | +| `ui/src/pages/Alerts/AlertRow.tsx` | **Delete** | +| `ui/src/pages/Alerts/alert-expanded.tsx` | **New** — shared expandedContent renderer | +| `ui/src/pages/Alerts/time-utils.ts` | **New** | +| `ui/src/pages/Alerts/time-utils.test.ts` | **New** | +| `ui/src/pages/Alerts/severity-utils.ts` | **New** | +| `ui/src/pages/Alerts/severity-utils.test.ts` | **New** | +| `ui/src/pages/Alerts/alerts-page.module.css` | Slim down to layout-only, DS tokens | +| `ui/src/pages/Alerts/RuleEditor/wizard.module.css` | Replace legacy tokens → DS tokens | +| `ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx` | Promote banner / warnings → DS `Alert`; step body → section-card wrap | + +## Testing + +1. **Unit (Vitest):** + - `time-utils.test.ts` — relative time formatting at 0s / 30s / 5m / 2h / 3d / 10d boundaries. + - `severity-utils.test.ts` — all four severities map correctly. +2. **Component tests:** + - Existing `AlertStateChip.test.tsx`, `SeverityBadge.test.tsx` keep passing (no change). + - `NotificationBell.test.tsx` — unchanged (this component already uses DS correctly per audit). +3. **E2E (Playwright):** + - Inbox empty state renders. + - AllAlerts filter ButtonGroup switches active state and requery fires. + - Rules list delete opens ConfirmDialog, confirms, row disappears. + - Wizard promote banner renders as `Alert`. +4. **Manual smoke:** + - Light + dark theme on all five pages — verify no raw `
` borders bleeding through; all surfaces use DS card shadows. + - Screenshot comparison pre/post via already-present Playwright config. + +## Open questions + +None — DS v0.1.56 ships every primitive we need (`DataTable`, `EmptyState`, `Alert`, `ButtonGroup`, `Dropdown`, `ConfirmDialog`, `DateRangePicker`, `FormField`, `MonoText`). If a gap surfaces during implementation, flag it as a separate DS-change discussion per user's standing rule. + +## Out of scope + +- Keyboard shortcuts (j/k nav, e to ack) — future enhancement. +- Grouping alerts by rule (collapse duplicates) — future enhancement. +- `MustacheEditor` visual restyling — separate concern. +- Replacing native `confirm()` outside `/alerts` — project-wide pattern; changing it all requires separate decision. + +## Migration risk + +- **Low.** Changes are localized to `ui/src/pages/Alerts/` plus one CSS module file for the wizard. No backend, no DS, no router changes. +- Openapi schema regeneration **not required** (no controller changes). +- Existing tests don't exercise feed-row DOM directly (component tests cover badges/chips only), so row-to-table conversion won't break assertions. From 52a08a8769a566c47aec94e348ffa4e2a9edc3f2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:49:47 +0200 Subject: [PATCH 02/49] =?UTF-8?q?docs(alerts):=20Implementation=20plan=20?= =?UTF-8?q?=E2=80=94=20design-system=20alignment=20for=20/alerts=20pages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task-by-task TDD plan implementing the design spec. Splits the work into 14 tasks: helper utilities (TDD), shared renderer, CSS token migration, per-page rewrites (Inbox/All/History/Rules/Silences), wizard banner migration, AlertRow deletion, E2E adaptation for ConfirmDialog, and full verification pass. Each task produces an atomic commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...26-04-21-alerts-design-system-alignment.md | 1906 +++++++++++++++++ 1 file changed, 1906 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-21-alerts-design-system-alignment.md diff --git a/docs/superpowers/plans/2026-04-21-alerts-design-system-alignment.md b/docs/superpowers/plans/2026-04-21-alerts-design-system-alignment.md new file mode 100644 index 00000000..e1ae29c5 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-alerts-design-system-alignment.md @@ -0,0 +1,1906 @@ +# Alerts design-system alignment — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate all pages under `/alerts` to `@cameleer/design-system` components and CSS tokens, matching the visual and structural conventions used by Admin/Audit/Apps pages. + +**Architecture:** Unified `DataTable` shell for Inbox/All/History with expandable rows; `DataTable + Dropdown + ConfirmDialog` for Rules list; `FormField` grid + `DataTable` for Silences; DS `Alert` for wizard info/warning banners. Undefined CSS variables (`--bg`, `--fg`, `--muted`, `--accent`) replaced with DS tokens (`--bg-surface`, `--text-primary`, etc.). + +**Tech Stack:** React 19, TypeScript, `@cameleer/design-system` 0.1.56, TanStack Query v5, Vitest, Playwright. + +--- + +## File Structure + +**New files:** +- `ui/src/pages/Alerts/time-utils.ts` — pure function `formatRelativeTime(iso, now?)`. +- `ui/src/pages/Alerts/time-utils.test.ts` — Vitest unit tests. +- `ui/src/pages/Alerts/severity-utils.ts` — pure function `severityToAccent(severity)`. +- `ui/src/pages/Alerts/severity-utils.test.ts` — Vitest unit tests. +- `ui/src/pages/Alerts/alert-expanded.tsx` — shared `expandedContent` renderer for `DataTable`. + +**Rewritten files:** +- `ui/src/pages/Alerts/InboxPage.tsx` +- `ui/src/pages/Alerts/AllAlertsPage.tsx` +- `ui/src/pages/Alerts/HistoryPage.tsx` +- `ui/src/pages/Alerts/RulesListPage.tsx` +- `ui/src/pages/Alerts/SilencesPage.tsx` +- `ui/src/pages/Alerts/alerts-page.module.css` (slimmed) +- `ui/src/pages/Alerts/RuleEditor/wizard.module.css` (token replacement) + +**Modified files:** +- `ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx` — banners become DS `Alert`s; step body wraps in `sectionStyles.section`. +- `ui/src/test/e2e/alerting.spec.ts` — adapt selectors for `ConfirmDialog` (replaces `dialog.accept()` pattern). + +**Deleted files:** +- `ui/src/pages/Alerts/AlertRow.tsx` + +**Key design-system contracts discovered during planning:** +- Severity enum is **3-value**: `CRITICAL | WARNING | INFO` (confirmed in `SeverityBadge.tsx`). +- `DataTable` `rowAccent` prop returns `'error' | 'warning' | undefined` — no `'info'`. INFO severity → `undefined` (no row tint). +- `DataTable` requires row type `extends { id: string }` — `AlertDto` already satisfies this. +- `ButtonGroup` uses `value: Set` (multi-select). For single-select state filter, use DS **`SegmentedTabs`** (`value: string`, `onChange(value: string)`) — cleaner fit. +- `ConfirmDialog` required props: `open`, `onClose`, `onConfirm`, `message`, `confirmText`. +- `SectionHeader` has an `action` prop — preferred over inline-flex toolbars. + +--- + +## Pre-flight + +### Task 0: Baseline verification + +Run once before starting to confirm the working tree builds and existing tests pass. No code changes. + +- [ ] **Step 1: Confirm clean working tree** + +```bash +git status --short +``` +Expected: only untracked files left over from earlier sessions (`runtime-*.png`, `ui/playwright.config.js`, `ui/vitest.config.js`). No staged/unstaged edits to `ui/src/**`. + +- [ ] **Step 2: Install / refresh UI deps** + +```bash +cd ui && npm install +``` +Expected: exits 0. `@cameleer/design-system@0.1.56` in `node_modules`. + +- [ ] **Step 3: Run UI unit tests baseline** + +```bash +cd ui && npm run test -- --run +``` +Expected: all green. Record count — any new task that adds tests should raise this count. + +- [ ] **Step 4: Run Maven compile baseline (sanity)** + +```bash +mvn -pl cameleer-server-app -am compile -q +``` +Expected: exits 0. + +- [ ] **Step 5: Inspect DS tokens** + +```bash +grep -oE "\--[a-z][a-z0-9-]+" ui/node_modules/@cameleer/design-system/dist/style.css | sort -u > /tmp/ds-tokens.txt +head -20 /tmp/ds-tokens.txt +``` +Expected output includes: `--amber`, `--bg-surface`, `--border-subtle`, `--error`, `--radius-lg`, `--shadow-card`, `--space-md`, `--text-muted`, `--text-primary`, `--warning`. **Do not** expect `--bg`, `--fg`, `--muted`, `--accent` (these are the undefined tokens we're replacing). + +--- + +## Helpers (TDD) + +### Task 1: `severity-utils.ts` — severity-to-accent helper + +**Files:** +- Create: `ui/src/pages/Alerts/severity-utils.ts` +- Test: `ui/src/pages/Alerts/severity-utils.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `ui/src/pages/Alerts/severity-utils.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { severityToAccent } from './severity-utils'; + +describe('severityToAccent', () => { + it('maps CRITICAL → error', () => { + expect(severityToAccent('CRITICAL')).toBe('error'); + }); + + it('maps WARNING → warning', () => { + expect(severityToAccent('WARNING')).toBe('warning'); + }); + + it('maps INFO → undefined (no row tint)', () => { + expect(severityToAccent('INFO')).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +cd ui && npm run test -- --run src/pages/Alerts/severity-utils.test.ts +``` +Expected: FAIL with `Cannot find module './severity-utils'`. + +- [ ] **Step 3: Implement the helper** + +Create `ui/src/pages/Alerts/severity-utils.ts`: + +```typescript +import type { AlertDto } from '../../api/queries/alerts'; + +type Severity = NonNullable; +export type RowAccent = 'error' | 'warning' | undefined; + +export function severityToAccent(severity: Severity): RowAccent { + switch (severity) { + case 'CRITICAL': return 'error'; + case 'WARNING': return 'warning'; + case 'INFO': return undefined; + } +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +cd ui && npm run test -- --run src/pages/Alerts/severity-utils.test.ts +``` +Expected: 3 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/pages/Alerts/severity-utils.ts ui/src/pages/Alerts/severity-utils.test.ts +git commit -m "$(cat <<'EOF' +feat(alerts/ui): add severityToAccent helper for DataTable rowAccent + +Pure function mapping the 3-value AlertDto.severity enum to the 2-value +DataTable rowAccent prop. INFO maps to undefined (no tint) because the +DS DataTable rowAccent only supports error|warning. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 2: `time-utils.ts` — relative-time helper + +**Files:** +- Create: `ui/src/pages/Alerts/time-utils.ts` +- Test: `ui/src/pages/Alerts/time-utils.test.ts` + +- [ ] **Step 1: Write the failing test** + +Create `ui/src/pages/Alerts/time-utils.test.ts`: + +```typescript +import { describe, it, expect } from 'vitest'; +import { formatRelativeTime } from './time-utils'; + +const NOW = new Date('2026-04-21T12:00:00Z'); + +describe('formatRelativeTime', () => { + it('returns "just now" for < 30s', () => { + expect(formatRelativeTime('2026-04-21T11:59:50Z', NOW)).toBe('just now'); + }); + + it('returns minutes for < 60m', () => { + expect(formatRelativeTime('2026-04-21T11:57:00Z', NOW)).toBe('3m ago'); + }); + + it('returns hours for < 24h', () => { + expect(formatRelativeTime('2026-04-21T10:00:00Z', NOW)).toBe('2h ago'); + }); + + it('returns days for < 30d', () => { + expect(formatRelativeTime('2026-04-18T12:00:00Z', NOW)).toBe('3d ago'); + }); + + it('returns locale date string for older than 30d', () => { + // Absolute fallback — we don't assert format, just that it isn't "Xd ago". + const out = formatRelativeTime('2025-01-01T00:00:00Z', NOW); + expect(out).not.toMatch(/ago$/); + expect(out.length).toBeGreaterThan(0); + }); + + it('handles future timestamps by clamping to "just now"', () => { + expect(formatRelativeTime('2026-04-21T12:00:30Z', NOW)).toBe('just now'); + }); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +cd ui && npm run test -- --run src/pages/Alerts/time-utils.test.ts +``` +Expected: FAIL with `Cannot find module './time-utils'`. + +- [ ] **Step 3: Implement the helper** + +Create `ui/src/pages/Alerts/time-utils.ts`: + +```typescript +export function formatRelativeTime(iso: string, now: Date = new Date()): string { + const then = new Date(iso).getTime(); + const diffSec = Math.max(0, Math.floor((now.getTime() - then) / 1000)); + if (diffSec < 30) return 'just now'; + if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; + if (diffSec < 86_400) return `${Math.floor(diffSec / 3600)}h ago`; + const diffDays = Math.floor(diffSec / 86_400); + if (diffDays < 30) return `${diffDays}d ago`; + return new Date(iso).toLocaleDateString('en-GB', { + year: 'numeric', month: 'short', day: '2-digit', + }); +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +cd ui && npm run test -- --run src/pages/Alerts/time-utils.test.ts +``` +Expected: 6 tests pass. + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/pages/Alerts/time-utils.ts ui/src/pages/Alerts/time-utils.test.ts +git commit -m "$(cat <<'EOF' +feat(alerts/ui): add formatRelativeTime helper + +Formats ISO timestamps as `Nm ago` / `Nh ago` / `Nd ago`, falling back +to an absolute locale date string for values older than 30 days. Used +by the alert DataTable Age column. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Shared renderer + +### Task 3: `alert-expanded.tsx` — shared expandedContent component + +**Files:** +- Create: `ui/src/pages/Alerts/alert-expanded.tsx` + +- [ ] **Step 1: Implement the component** + +Create `ui/src/pages/Alerts/alert-expanded.tsx`: + +```tsx +import type { AlertDto } from '../../api/queries/alerts'; +import css from './alerts-page.module.css'; + +/** + * Shared DataTable expandedContent renderer for alert rows. + * Used by Inbox, All alerts, and History pages. + */ +export function renderAlertExpanded(alert: AlertDto) { + return ( +
+ {alert.message && ( +
+ Message +

{alert.message}

+
+ )} +
+
+ Fired at + {alert.firedAt ?? '—'} +
+ {alert.resolvedAt && ( +
+ Resolved at + {alert.resolvedAt} +
+ )} + {alert.ackedAt && ( +
+ Acknowledged at + {alert.ackedAt} +
+ )} +
+ Rule + {alert.ruleName ?? alert.ruleId ?? '—'} +
+ {alert.appSlug && ( +
+ App + {alert.appSlug} +
+ )} +
+
+ ); +} +``` + +Note: referenced classes (`.expanded`, `.expandedGrid`, `.expandedField`, `.expandedLabel`, `.expandedValue`) are added to `alerts-page.module.css` in Task 4. Any field name not present on `AlertDto` (check via Task 3 Step 2 below) must be removed. + +- [ ] **Step 2: Typecheck against generated schema** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | head -40 +``` +Expected: no errors referencing `alert-expanded.tsx`. If `alert.ruleName`, `alert.resolvedAt`, `alert.ackedAt`, or `alert.appSlug` do not exist on `AlertDto`: + +1. Open `ui/src/api/schema.d.ts` and search for `AlertDto` to see the actual field names. +2. Remove or rename the missing fields in `alert-expanded.tsx`. +3. Re-run typecheck until clean. + +Do NOT add fields that don't exist — the expansion is best-effort and safely renders partial data. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/alert-expanded.tsx +git commit -m "$(cat <<'EOF' +feat(alerts/ui): add shared renderAlertExpanded for DataTable rows + +Extracts the per-row detail block used by Inbox/All/History DataTables +so the three pages share one rendering. Consumes AlertDto fields that +are nullable in the schema; hides missing fields instead of rendering +placeholders. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## CSS token migration + +### Task 4: Slim `alerts-page.module.css` + +**Files:** +- Modify: `ui/src/pages/Alerts/alerts-page.module.css` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/alerts-page.module.css`: + +```css +.page { + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.filterBar { + display: flex; + gap: var(--space-sm); + align-items: center; + flex-wrap: wrap; +} + +.bulkBar { + display: flex; + gap: var(--space-sm); + align-items: center; + padding: var(--space-sm) var(--space-md); + background: var(--bg-hover); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); +} + +.titleCell { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.titleCell a { + color: var(--text-primary); + font-weight: 500; + text-decoration: none; +} + +.titleCell a:hover { + text-decoration: underline; +} + +.titleCellUnread a { + font-weight: 600; +} + +.titlePreview { + font-size: 12px; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 48ch; +} + +.expanded { + display: flex; + flex-direction: column; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); +} + +.expandedGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: var(--space-sm); +} + +.expandedField { + display: flex; + flex-direction: column; + gap: 2px; +} + +.expandedLabel { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); +} + +.expandedValue { + font-size: 13px; + color: var(--text-primary); + margin: 0; + word-break: break-word; +} +``` + +- [ ] **Step 2: Verify no other files still depend on the removed classes** + +```bash +grep -rn "css\.row\|css\.rowUnread\|css\.body\|css\.meta\|css\.time\|css\.message\|css\.actions\|css\.empty" ui/src/pages/Alerts/ +``` +Expected: all matches are inside `InboxPage.tsx`, `AllAlertsPage.tsx`, `HistoryPage.tsx`, or `AlertRow.tsx` — these are all being rewritten in Tasks 5-7 and deleted in Task 10. Do NOT stop here just because matches exist; they will be removed downstream. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/alerts-page.module.css +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): slim alerts-page.module.css to layout-only DS tokens + +Drop the feed-row classes (.row, .rowUnread, .body, .meta, .time, +.message, .actions, .empty) — these are replaced by DS DataTable + +EmptyState in follow-up tasks. Keep layout helpers for page shell, +toolbar, filter bar, bulk-action bar, title cell, and DataTable +expanded content. All colors / spacing use DS tokens. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## List pages + +### Task 5: Rewrite `InboxPage.tsx` + +**Files:** +- Modify: `ui/src/pages/Alerts/InboxPage.tsx` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/InboxPage.tsx`: + +```tsx +import { useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { Inbox } from 'lucide-react'; +import { + Button, SectionHeader, DataTable, EmptyState, useToast, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; +import { PageLoader } from '../../components/PageLoader'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { + useAlerts, useAckAlert, useBulkReadAlerts, useMarkAlertRead, + type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; +import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; + +export default function InboxPage() { + const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); + const bulkRead = useBulkReadAlerts(); + const markRead = useMarkAlertRead(); + const ack = useAckAlert(); + const { toast } = useToast(); + + const [selected, setSelected] = useState>(new Set()); + const rows = data ?? []; + + const unreadIds = useMemo( + () => rows.filter((a) => a.state === 'FIRING').map((a) => a.id), + [rows], + ); + + const toggleSelected = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + const onAck = async (id: string, title?: string) => { + try { + await ack.mutateAsync(id); + toast({ title: 'Acknowledged', description: title, variant: 'success' }); + } catch (e) { + toast({ title: 'Ack failed', description: String(e), variant: 'error' }); + } + }; + + const onBulkRead = async (ids: string[]) => { + if (ids.length === 0) return; + try { + await bulkRead.mutateAsync(ids); + setSelected(new Set()); + toast({ title: `Marked ${ids.length} as read`, variant: 'success' }); + } catch (e) { + toast({ title: 'Bulk read failed', description: String(e), variant: 'error' }); + } + }; + + const columns: Column[] = [ + { + key: 'select', header: '', width: '40px', + render: (_, row) => ( + toggleSelected(row.id)} + aria-label={`Select ${row.title ?? row.id}`} + onClick={(e) => e.stopPropagation()} + /> + ), + }, + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => + row.severity ? : null, + }, + { + key: 'state', header: 'State', width: '140px', + render: (_, row) => + row.state ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => { + const unread = row.state === 'FIRING'; + return ( +
+ markRead.mutate(row.id)}> + {row.title ?? '(untitled)'} + + {row.message && {row.message}} +
+ ); + }, + }, + { + key: 'age', header: 'Age', width: '100px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'ack', header: '', width: '70px', + render: (_, row) => + row.state === 'FIRING' ? ( + + ) : null, + }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load alerts: {String(error)}
; + + const selectedIds = Array.from(selected); + + return ( +
+
+ Inbox +
+ +
+ + {selectedIds.length > 0 + ? `${selectedIds.length} selected` + : `${unreadIds.length} unread`} + +
+ + +
+
+ + {rows.length === 0 ? ( + } + title="All clear" + description="No open alerts for you in this environment." + /> + ) : ( +
+ + columns={columns} + data={rows} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "InboxPage\.tsx|severity-utils|time-utils|alert-expanded" +``` +Expected: no output. + +- [ ] **Step 3: Unit tests still green** + +```bash +cd ui && npm run test -- --run +``` +Expected: no regressions; same or higher count than Task 0 Step 3 baseline. + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/pages/Alerts/InboxPage.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): rewrite Inbox as DataTable with expandable rows + +Replaces custom feed-row layout with the shared DataTable shell used +elsewhere in the app. Adds checkbox selection + bulk "Mark selected +read" toolbar alongside the existing "Mark all read". Uses DS +EmptyState for empty lists, severity-driven rowAccent for unread +tinting, and renderAlertExpanded for row detail. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 6: Rewrite `AllAlertsPage.tsx` + +**Files:** +- Modify: `ui/src/pages/Alerts/AllAlertsPage.tsx` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/AllAlertsPage.tsx`: + +```tsx +import { useState } from 'react'; +import { Link } from 'react-router'; +import { Bell } from 'lucide-react'; +import { + SectionHeader, DataTable, EmptyState, SegmentedTabs, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; +import { PageLoader } from '../../components/PageLoader'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { + useAlerts, useMarkAlertRead, + type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; +import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; + +type AlertState = NonNullable; + +const STATE_FILTERS: Record = { + open: { label: 'Open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, + firing: { label: 'Firing', values: ['FIRING'] }, + acked: { label: 'Acked', values: ['ACKNOWLEDGED'] }, + all: { label: 'All', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, +}; + +export default function AllAlertsPage() { + const [filterKey, setFilterKey] = useState('open'); + const filter = STATE_FILTERS[filterKey]; + const { data, isLoading, error } = useAlerts({ state: filter.values, limit: 200 }); + const markRead = useMarkAlertRead(); + + const rows = data ?? []; + + const columns: Column[] = [ + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => row.severity ? : null, + }, + { + key: 'state', header: 'State', width: '140px', + render: (_, row) => row.state ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => ( +
+ markRead.mutate(row.id)}> + {row.title ?? '(untitled)'} + + {row.message && {row.message}} +
+ ), + }, + { + key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load alerts: {String(error)}
; + + return ( +
+
+ All alerts +
+ +
+ ({ value, label: f.label }))} + active={filterKey} + onChange={setFilterKey} + /> +
+ + {rows.length === 0 ? ( + } + title="No alerts match this filter" + description={`Try switching to a different state or widening your criteria.`} + /> + ) : ( +
+ + columns={columns} + data={rows} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "AllAlertsPage\.tsx" +``` +Expected: no output. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/AllAlertsPage.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): rewrite All alerts as DataTable + SegmentedTabs filter + +Replaces 4-Button filter row with DS SegmentedTabs and custom row +rendering with DataTable. Shares expandedContent renderer and +severity-driven rowAccent with Inbox. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 7: Rewrite `HistoryPage.tsx` + +**Files:** +- Modify: `ui/src/pages/Alerts/HistoryPage.tsx` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/HistoryPage.tsx`: + +```tsx +import { useState } from 'react'; +import { Link } from 'react-router'; +import { History } from 'lucide-react'; +import { + SectionHeader, DataTable, EmptyState, DateRangePicker, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; +import { PageLoader } from '../../components/PageLoader'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { + useAlerts, type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; +import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; + +/** Duration in minutes/hours/days. Pure, best-effort. */ +function formatDuration(from?: string | null, to?: string | null): string { + if (!from || !to) return '—'; + const ms = new Date(to).getTime() - new Date(from).getTime(); + if (ms < 0 || Number.isNaN(ms)) return '—'; + const sec = Math.floor(ms / 1000); + if (sec < 60) return `${sec}s`; + if (sec < 3600) return `${Math.floor(sec / 60)}m`; + if (sec < 86_400) return `${Math.floor(sec / 3600)}h`; + return `${Math.floor(sec / 86_400)}d`; +} + +export default function HistoryPage() { + const [dateRange, setDateRange] = useState({ + start: new Date(Date.now() - 7 * 24 * 3600_000), + end: new Date(), + }); + + // useAlerts doesn't accept a time range today; we fetch RESOLVED and + // filter client-side. A server-side range param is a future enhancement. + const { data, isLoading, error } = useAlerts({ state: 'RESOLVED', limit: 200 }); + + const filtered = (data ?? []).filter((a) => { + if (!a.firedAt) return false; + const t = new Date(a.firedAt).getTime(); + return t >= dateRange.start.getTime() && t <= dateRange.end.getTime(); + }); + + const columns: Column[] = [ + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => row.severity ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => ( +
+ {row.title ?? '(untitled)'} + {row.message && {row.message}} +
+ ), + }, + { + key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'resolvedAt', header: 'Resolved at', width: '140px', sortable: true, + render: (_, row) => + row.resolvedAt ? ( + + {formatRelativeTime(row.resolvedAt)} + + ) : '—', + }, + { + key: 'duration', header: 'Duration', width: '90px', + render: (_, row) => formatDuration(row.firedAt, row.resolvedAt), + }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load history: {String(error)}
; + + return ( +
+
+ History +
+ +
+ +
+ + {filtered.length === 0 ? ( + } + title="No resolved alerts" + description="Nothing in the selected date range. Try widening it." + /> + ) : ( +
+ + columns={columns} + data={filtered} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
+ )} +
+ ); +} +``` + +If `AlertDto.resolvedAt` does not exist in the generated schema (TSC will report it), remove the `resolvedAt` and `duration` columns and omit the field from the table. Do NOT add a backend field. + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "HistoryPage\.tsx" +``` +Expected: no output. If `resolvedAt` errors appear, adjust per note above. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/HistoryPage.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): rewrite History as DataTable + DateRangePicker + +Replaces custom feed rows with DataTable. Adds a DateRangePicker +filter (client-side) defaulting to the last 7 days. Client-side +range filter is a stopgap; a server-side range param is a future +enhancement captured in the design spec. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 8: Rewrite `RulesListPage.tsx` + +**Files:** +- Modify: `ui/src/pages/Alerts/RulesListPage.tsx` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/RulesListPage.tsx`: + +```tsx +import { useState } from 'react'; +import { Link, useNavigate } from 'react-router'; +import { FilePlus } from 'lucide-react'; +import { + Button, SectionHeader, Toggle, useToast, Badge, DataTable, + EmptyState, Dropdown, ConfirmDialog, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; +import { PageLoader } from '../../components/PageLoader'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { + useAlertRules, + useDeleteAlertRule, + useSetAlertRuleEnabled, + type AlertRuleResponse, +} from '../../api/queries/alertRules'; +import { useEnvironments } from '../../api/queries/admin/environments'; +import { useSelectedEnv } from '../../api/queries/alertMeta'; +import tableStyles from '../../styles/table-section.module.css'; +import css from './alerts-page.module.css'; + +export default function RulesListPage() { + const navigate = useNavigate(); + const env = useSelectedEnv(); + const { data: rules, isLoading, error } = useAlertRules(); + const { data: envs } = useEnvironments(); + const setEnabled = useSetAlertRuleEnabled(); + const deleteRule = useDeleteAlertRule(); + const { toast } = useToast(); + + const [pendingDelete, setPendingDelete] = useState(null); + + if (isLoading) return ; + if (error) return
Failed to load rules: {String(error)}
; + + const rows = rules ?? []; + const otherEnvs = (envs ?? []).filter((e) => e.slug !== env); + + const onToggle = async (r: AlertRuleResponse) => { + try { + await setEnabled.mutateAsync({ id: r.id, enabled: !r.enabled }); + toast({ title: r.enabled ? 'Disabled' : 'Enabled', description: r.name, variant: 'success' }); + } catch (e) { + toast({ title: 'Toggle failed', description: String(e), variant: 'error' }); + } + }; + + const confirmDelete = async () => { + if (!pendingDelete) return; + try { + await deleteRule.mutateAsync(pendingDelete.id); + toast({ title: 'Deleted', description: pendingDelete.name, variant: 'success' }); + } catch (e) { + toast({ title: 'Delete failed', description: String(e), variant: 'error' }); + } finally { + setPendingDelete(null); + } + }; + + const onPromote = (r: AlertRuleResponse, targetEnvSlug: string) => { + navigate(`/alerts/rules/new?promoteFrom=${env}&ruleId=${r.id}&targetEnv=${targetEnvSlug}`); + }; + + const columns: Column[] = [ + { + key: 'name', header: 'Name', + render: (_, r) => {r.name}, + }, + { + key: 'conditionKind', header: 'Kind', width: '160px', + render: (_, r) => , + }, + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, r) => , + }, + { + key: 'enabled', header: 'Enabled', width: '90px', + render: (_, r) => ( + onToggle(r)} + disabled={setEnabled.isPending} + /> + ), + }, + { + key: 'targets', header: 'Targets', width: '90px', + render: (_, r) => String(r.targets.length), + }, + { + key: 'actions', header: '', width: '220px', + render: (_, r) => ( +
+ {otherEnvs.length > 0 && ( + Promote to ▾} + items={otherEnvs.map((e) => ({ + label: e.slug, + onClick: () => onPromote(r, e.slug), + }))} + /> + )} + +
+ ), + }, + ]; + + return ( +
+
+ + + + } + > + Alert rules + +
+ + {rows.length === 0 ? ( + } + title="No alert rules" + description="Create one to start evaluating alerts for this environment." + action={ + + + + } + /> + ) : ( +
+ + columns={columns} + data={rows} + flush + /> +
+ )} + + setPendingDelete(null)} + onConfirm={confirmDelete} + title="Delete alert rule?" + message={ + pendingDelete + ? `Delete rule "${pendingDelete.name}"? Fired alerts are preserved via rule_snapshot.` + : '' + } + confirmText="Delete" + variant="danger" + loading={deleteRule.isPending} + /> +
+ ); +} +``` + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "RulesListPage\.tsx" +``` +Expected: no output. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/RulesListPage.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): rewrite Rules list with DataTable + Dropdown + ConfirmDialog + +Replaces raw
with DataTable, raw setMatcherRuleId(e.target.value)} /> + + + setMatcherAppSlug(e.target.value)} /> + + + setHours(Number(e.target.value))} + /> + + + setReason(e.target.value)} + placeholder="Maintenance window" + /> + + + + + + {rows.length === 0 ? ( + } + title="No silences" + description="Nothing is currently silenced in this environment." + /> + ) : ( +
+ + columns={columns} + data={rows.map((s) => ({ ...s, id: s.id ?? '' }))} + flush + /> +
+ )} + + setPendingEnd(null)} + onConfirm={confirmEnd} + title="End silence?" + message="End this silence early? Affected rules will resume firing." + confirmText="End silence" + variant="warning" + loading={remove.isPending} + /> + + ); +} +``` + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "SilencesPage\.tsx" +``` +Expected: no output. If `AlertSilenceResponse` doesn't have a definite `id`, the `.map((s) => ({ ...s, id: s.id ?? '' }))` cast keeps DataTable happy without altering semantics. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/SilencesPage.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): rewrite Silences with DataTable + FormField + ConfirmDialog + +Replaces raw
with DataTable, inline-styled form with proper +FormField hints, and native confirm() end-early with ConfirmDialog +(warning variant). Adds DS EmptyState for no-silences case. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 10: Delete `AlertRow.tsx` + +**Files:** +- Delete: `ui/src/pages/Alerts/AlertRow.tsx` + +- [ ] **Step 1: Verify no remaining imports** + +```bash +grep -rn "AlertRow" ui/src/ | grep -v "\.test\.\|\.d\.ts" +``` +Expected: no matches. (The `AlertStateChip` tests mention "state chip" strings, not `AlertRow`.) + +If any matches remain, they must be from a previous task that wasn't finished — fix before proceeding. + +- [ ] **Step 2: Delete the file** + +```bash +git rm ui/src/pages/Alerts/AlertRow.tsx +``` + +- [ ] **Step 3: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json +``` +Expected: exits 0. + +- [ ] **Step 4: Commit** + +```bash +git commit -m "$(cat <<'EOF' +chore(alerts/ui): remove obsolete AlertRow.tsx + +The feed-row component is replaced by DataTable column renderers and +the shared renderAlertExpanded content renderer. No callers remain. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Wizard + +### Task 11: Token + layout fix in `wizard.module.css` + +**Files:** +- Modify: `ui/src/pages/Alerts/RuleEditor/wizard.module.css` + +- [ ] **Step 1: Replace entire file** + +Overwrite `ui/src/pages/Alerts/RuleEditor/wizard.module.css`: + +```css +.wizard { + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.steps { + display: flex; + gap: var(--space-sm); + border-bottom: 1px solid var(--border-subtle); + padding-bottom: var(--space-sm); +} + +.step { + background: none; + border: none; + padding: 8px 12px; + border-bottom: 2px solid transparent; + cursor: pointer; + color: var(--text-muted); + font-size: 13px; + font-family: inherit; +} + +.step:hover { + color: var(--text-primary); +} + +.stepActive { + color: var(--text-primary); + border-bottom-color: var(--amber); +} + +.stepDone { + color: var(--text-primary); +} + +.stepBody { + min-height: 320px; +} + +.footer { + display: flex; + justify-content: space-between; +} +``` + +Note: `.promoteBanner` is removed — the wizard will switch to DS `` in Task 12. + +- [ ] **Step 2: Commit** + +```bash +git add ui/src/pages/Alerts/RuleEditor/wizard.module.css +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): replace undefined CSS vars in wizard.module.css + +Replace undefined tokens (--muted, --fg, --accent, --border, +--amber-bg) with DS tokens (--text-muted, --text-primary, --amber, +--border-subtle, --space-sm|md). Drop .promoteBanner — replaced by +DS Alert in follow-up commit. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +### Task 12: Wizard banners → DS `Alert`; step body → section card + +**Files:** +- Modify: `ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx` + +- [ ] **Step 1: Replace the JSX return of `RuleEditorWizard`** + +Open `ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx`. At the top of the file, update imports: + +```tsx +import { useEffect, useState } from 'react'; +import { useNavigate, useParams, useSearchParams } from 'react-router'; +import { Alert, Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { PageLoader } from '../../../components/PageLoader'; +import { + useAlertRule, + useCreateAlertRule, + useUpdateAlertRule, +} from '../../../api/queries/alertRules'; +import { + initialForm, + toRequest, + validateStep, + WIZARD_STEPS, + type FormState, + type WizardStep, +} from './form-state'; +import { ScopeStep } from './ScopeStep'; +import { ConditionStep } from './ConditionStep'; +import { TriggerStep } from './TriggerStep'; +import { NotifyStep } from './NotifyStep'; +import { ReviewStep } from './ReviewStep'; +import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill'; +import { useCatalog } from '../../../api/queries/catalog'; +import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections'; +import { useSelectedEnv } from '../../../api/queries/alertMeta'; +import sectionStyles from '../../../styles/section-card.module.css'; +import css from './wizard.module.css'; +``` + +Then replace the `return (...)` block at the bottom of the component with: + +```tsx + return ( +
+
+ {isEdit ? `Edit rule: ${form.name}` : 'New alert rule'} +
+ + {promoteFrom && ( + + Promoting from {promoteFrom} — review and adjust, then save. + + )} + + {warnings.length > 0 && ( + +
    + {warnings.map((w) => ( +
  • + {w.field}: {w.message} +
  • + ))} +
+
+ )} + + + +
{body}
+ +
+ + {idx < WIZARD_STEPS.length - 1 ? ( + + ) : ( + + )} +
+
+ ); +``` + +- [ ] **Step 2: Typecheck** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json 2>&1 | grep -E "RuleEditorWizard\.tsx" +``` +Expected: no output. + +- [ ] **Step 3: Unit tests still green** + +```bash +cd ui && npm run test -- --run +``` +Expected: no regressions. + +- [ ] **Step 4: Commit** + +```bash +git add ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx +git commit -m "$(cat <<'EOF' +refactor(alerts/ui): wizard banners → DS Alert, step body → section card + +Promote banner and prefill warnings now render as DS Alert components +(info / warning variants). Step body wraps in sectionStyles.section +for card affordance matching other forms in the app. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## E2E test adaptation + +### Task 13: Update `alerting.spec.ts` for ConfirmDialog + +**Files:** +- Modify: `ui/src/test/e2e/alerting.spec.ts` + +Context: two tests currently rely on `page.once('dialog', (d) => d.accept())` which captures native `confirm()` dialogs. Tasks 8 and 9 replaced those with DS `ConfirmDialog`. Update the selectors. + +- [ ] **Step 1: Patch the rule delete assertion** + +Open `ui/src/test/e2e/alerting.spec.ts`. Find the "create + delete a rule via the wizard" test. Replace: + +```ts + // Cleanup: delete. + page.once('dialog', (d) => d.accept()); + await page + .getByRole('row', { name: new RegExp(ruleName) }) + .getByRole('button', { name: /^delete$/i }) + .click(); + await expect(main.getByRole('link', { name: ruleName })).toHaveCount(0); +``` + +With: + +```ts + // Cleanup: open ConfirmDialog via row Delete button, confirm in dialog. + await page + .getByRole('row', { name: new RegExp(ruleName) }) + .getByRole('button', { name: /^delete$/i }) + .click(); + const confirmDelete = page.getByRole('dialog'); + await expect(confirmDelete.getByText(/delete alert rule/i)).toBeVisible(); + await confirmDelete.getByRole('button', { name: /^delete$/i }).click(); + await expect(main.getByRole('link', { name: ruleName })).toHaveCount(0); +``` + +- [ ] **Step 2: Patch the silence end-early assertion** + +In the same file, find the "silence create + end-early" test. Replace: + +```ts + page.once('dialog', (d) => d.accept()); + await page + .getByRole('row', { name: new RegExp(unique) }) + .getByRole('button', { name: /^end$/i }) + .click(); + await expect(page.getByText(unique)).toHaveCount(0); +``` + +With: + +```ts + await page + .getByRole('row', { name: new RegExp(unique) }) + .getByRole('button', { name: /^end$/i }) + .click(); + const confirmEnd = page.getByRole('dialog'); + await expect(confirmEnd.getByText(/end silence/i)).toBeVisible(); + await confirmEnd.getByRole('button', { name: /end silence/i }).click(); + await expect(page.getByText(unique)).toHaveCount(0); +``` + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/test/e2e/alerting.spec.ts +git commit -m "$(cat <<'EOF' +test(alerts/e2e): adapt smoke suite to DS ConfirmDialog + +The Rules list Delete and Silences End-early flows now use DS +ConfirmDialog instead of native confirm(). Update selectors to +target the dialog's role=dialog + confirm button instead of +listening for the native `dialog` event. + +Co-Authored-By: Claude Opus 4.7 (1M context) +EOF +)" +``` + +--- + +## Verification + +### Task 14: Full verification + +No code changes — run the gate checks in order and fix any failure before moving on. + +- [ ] **Step 1: Full TypeScript compile** + +```bash +cd ui && npx tsc --noEmit -p tsconfig.app.json +``` +Expected: exits 0. + +- [ ] **Step 2: UI unit tests** + +```bash +cd ui && npm run test -- --run +``` +Expected: all pass, count ≥ baseline + 9 (3 severity-utils + 6 time-utils). + +- [ ] **Step 3: Production build** + +```bash +cd ui && npm run build +``` +Expected: exits 0 (no dead imports, no TS errors). + +- [ ] **Step 4: Grep sanity — no undefined tokens remain in the alerts tree** + +```bash +grep -rn "var(--bg)\|var(--fg)\|var(--muted)\|var(--accent)\|var(--amber-bg" ui/src/pages/Alerts/ +``` +Expected: no matches. `var(--amber)` IS valid (DS token) — only flag if you see `--amber-bg` used on a legacy surface (the token itself is fine, but we removed all usages in this plan). + +- [ ] **Step 5: Grep sanity — no raw `
` in alerts** + +```bash +grep -rn "&targetEnv=other`) renders as info `Alert`. Delete a rule → `ConfirmDialog` opens; confirm → row disappears. +6. **Silences** — create form grid uses FormField labels/hints; table renders; `End` → `ConfirmDialog`. + +- [ ] **Step 9: Run Playwright e2e** + +```bash +cd ui && npm run test:e2e +``` + +If `test:e2e` script doesn't exist, run: + +```bash +cd ui && npx playwright test --config=playwright.config.js +``` + +Expected: all 4 tests pass. If the Inbox smoke now asserts `Mark all read` but a regression made the button label differ, update the assertion or the UI — do not suppress the test. + +- [ ] **Step 10: GitNexus scope check** + +Before the final wrap-up commit (there may or may not be one): + +```bash +# Using the MCP tool, run: +# gitnexus_detect_changes({scope: "compare", base_ref: "main"}) +``` + +Expected: affected files match the plan — everything is under `ui/src/pages/Alerts/`, `ui/src/test/e2e/alerting.spec.ts`, or new helper files. No unexpected changes in `cameleer-server-core`, `cameleer-server-app/src/main/java`, `cameleer-server-app/src/main/resources`, or `ui/src/api/`. + +If anything outside these scopes shows up — stop and investigate. + +--- + +## Self-review + +**Spec coverage check:** + +| Spec item | Task | +|-----------|------| +| Tokens in `alerts-page.module.css` → DS tokens | Task 4 | +| Tokens in `wizard.module.css` → DS tokens | Task 11 | +| Inbox → DataTable + EmptyState + bulk toolbar | Task 5 | +| All alerts → DataTable + SegmentedTabs filter | Task 6 (spec said ButtonGroup; adopted SegmentedTabs because `ButtonGroup` is multi-select — documented in header) | +| History → DataTable + DateRangePicker | Task 7 | +| Rules list → DataTable + Dropdown + ConfirmDialog | Task 8 | +| Silences → DataTable + FormField + ConfirmDialog | Task 9 | +| AlertRow delete | Task 10 | +| Wizard banners → DS Alert | Task 12 | +| Step body wraps in section-card | Task 12 | +| `severityToAccent` helper + test | Task 1 | +| `formatRelativeTime` helper + test | Task 2 | +| Shared `renderAlertExpanded` | Task 3 | +| E2E adaptation for ConfirmDialog | Task 13 | +| Manual light/dark smoke | Task 14 Step 8 | + +**Placeholder scan:** No "TBD"/"TODO"/"fill in"/"handle edge cases". Every task has full code blocks and exact commands. + +**Type consistency:** Severity enum (`CRITICAL | WARNING | INFO`) is consistent across Tasks 1, 3, 5, 6, 7. `rowAccent` return type matches DS (`error | warning | undefined`). `AlertDto` fields used in Task 3 have a safety-check fallback step. From a2b2ccbab72cbac60b5e142e22df7c9f6d38102d Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:57:58 +0200 Subject: [PATCH 03/49] feat(alerts/ui): add severityToAccent helper for DataTable rowAccent Pure function mapping the 3-value AlertDto.severity enum to the 2-value DataTable rowAccent prop. INFO maps to undefined (no tint) because the DS DataTable rowAccent only supports error|warning. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/severity-utils.test.ts | 16 ++++++++++++++++ ui/src/pages/Alerts/severity-utils.ts | 12 ++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 ui/src/pages/Alerts/severity-utils.test.ts create mode 100644 ui/src/pages/Alerts/severity-utils.ts diff --git a/ui/src/pages/Alerts/severity-utils.test.ts b/ui/src/pages/Alerts/severity-utils.test.ts new file mode 100644 index 00000000..a4dcb3ab --- /dev/null +++ b/ui/src/pages/Alerts/severity-utils.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest'; +import { severityToAccent } from './severity-utils'; + +describe('severityToAccent', () => { + it('maps CRITICAL → error', () => { + expect(severityToAccent('CRITICAL')).toBe('error'); + }); + + it('maps WARNING → warning', () => { + expect(severityToAccent('WARNING')).toBe('warning'); + }); + + it('maps INFO → undefined (no row tint)', () => { + expect(severityToAccent('INFO')).toBeUndefined(); + }); +}); diff --git a/ui/src/pages/Alerts/severity-utils.ts b/ui/src/pages/Alerts/severity-utils.ts new file mode 100644 index 00000000..c98e176c --- /dev/null +++ b/ui/src/pages/Alerts/severity-utils.ts @@ -0,0 +1,12 @@ +import type { AlertDto } from '../../api/queries/alerts'; + +type Severity = NonNullable; +export type RowAccent = 'error' | 'warning' | undefined; + +export function severityToAccent(severity: Severity): RowAccent { + switch (severity) { + case 'CRITICAL': return 'error'; + case 'WARNING': return 'warning'; + case 'INFO': return undefined; + } +} From 4a63149338665a05b39972af8d036ab5c2b657bb Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:00:15 +0200 Subject: [PATCH 04/49] feat(alerts/ui): add formatRelativeTime helper Formats ISO timestamps as `Nm ago` / `Nh ago` / `Nd ago`, falling back to an absolute locale date string for values older than 30 days. Used by the alert DataTable Age column. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/time-utils.test.ts | 32 ++++++++++++++++++++++++++ ui/src/pages/Alerts/time-utils.ts | 12 ++++++++++ 2 files changed, 44 insertions(+) create mode 100644 ui/src/pages/Alerts/time-utils.test.ts create mode 100644 ui/src/pages/Alerts/time-utils.ts diff --git a/ui/src/pages/Alerts/time-utils.test.ts b/ui/src/pages/Alerts/time-utils.test.ts new file mode 100644 index 00000000..e541a1ba --- /dev/null +++ b/ui/src/pages/Alerts/time-utils.test.ts @@ -0,0 +1,32 @@ +import { describe, it, expect } from 'vitest'; +import { formatRelativeTime } from './time-utils'; + +const NOW = new Date('2026-04-21T12:00:00Z'); + +describe('formatRelativeTime', () => { + it('returns "just now" for < 30s', () => { + expect(formatRelativeTime('2026-04-21T11:59:50Z', NOW)).toBe('just now'); + }); + + it('returns minutes for < 60m', () => { + expect(formatRelativeTime('2026-04-21T11:57:00Z', NOW)).toBe('3m ago'); + }); + + it('returns hours for < 24h', () => { + expect(formatRelativeTime('2026-04-21T10:00:00Z', NOW)).toBe('2h ago'); + }); + + it('returns days for < 30d', () => { + expect(formatRelativeTime('2026-04-18T12:00:00Z', NOW)).toBe('3d ago'); + }); + + it('returns locale date string for older than 30d', () => { + const out = formatRelativeTime('2025-01-01T00:00:00Z', NOW); + expect(out).not.toMatch(/ago$/); + expect(out.length).toBeGreaterThan(0); + }); + + it('handles future timestamps by clamping to "just now"', () => { + expect(formatRelativeTime('2026-04-21T12:00:30Z', NOW)).toBe('just now'); + }); +}); diff --git a/ui/src/pages/Alerts/time-utils.ts b/ui/src/pages/Alerts/time-utils.ts new file mode 100644 index 00000000..eb2a810d --- /dev/null +++ b/ui/src/pages/Alerts/time-utils.ts @@ -0,0 +1,12 @@ +export function formatRelativeTime(iso: string, now: Date = new Date()): string { + const then = new Date(iso).getTime(); + const diffSec = Math.max(0, Math.floor((now.getTime() - then) / 1000)); + if (diffSec < 30) return 'just now'; + if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`; + if (diffSec < 86_400) return `${Math.floor(diffSec / 3600)}h ago`; + const diffDays = Math.floor(diffSec / 86_400); + if (diffDays < 30) return `${diffDays}d ago`; + return new Date(iso).toLocaleDateString('en-GB', { + year: 'numeric', month: 'short', day: '2-digit', + }); +} From b16ea8b18582d4b8e43a64787a4d4c284e83b67c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:01:39 +0200 Subject: [PATCH 05/49] feat(alerts/ui): add shared renderAlertExpanded for DataTable rows Extracts the per-row detail block used by Inbox/All/History DataTables so the three pages share one rendering. Consumes AlertDto fields that are nullable in the schema; hides missing fields instead of rendering placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/alert-expanded.tsx | 41 ++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 ui/src/pages/Alerts/alert-expanded.tsx diff --git a/ui/src/pages/Alerts/alert-expanded.tsx b/ui/src/pages/Alerts/alert-expanded.tsx new file mode 100644 index 00000000..161f9358 --- /dev/null +++ b/ui/src/pages/Alerts/alert-expanded.tsx @@ -0,0 +1,41 @@ +import type { AlertDto } from '../../api/queries/alerts'; +import css from './alerts-page.module.css'; + +/** + * Shared DataTable expandedContent renderer for alert rows. + * Used by Inbox, All alerts, and History pages. + */ +export function renderAlertExpanded(alert: AlertDto) { + return ( +
+ {alert.message && ( +
+ Message +

{alert.message}

+
+ )} +
+
+ Fired at + {alert.firedAt ?? '—'} +
+ {alert.resolvedAt && ( +
+ Resolved at + {alert.resolvedAt} +
+ )} + {alert.ackedAt && ( +
+ Acknowledged at + {alert.ackedAt} +
+ )} +
+ Rule + {alert.ruleId ?? '—'} +
+
+
+ ); +} From c87c77c1cfcf9b11fc0689a1882f3c6fe42e27aa Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:03:20 +0200 Subject: [PATCH 06/49] refactor(alerts/ui): slim alerts-page.module.css to layout-only DS tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the feed-row classes (.row, .rowUnread, .body, .meta, .time, .message, .actions, .empty) — these are replaced by DS DataTable + EmptyState in follow-up tasks. Keep layout helpers for page shell, toolbar, filter bar, bulk-action bar, title cell, and DataTable expanded content. All colors / spacing use DS tokens. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/alerts-page.module.css | 110 +++++++++++++++++---- 1 file changed, 93 insertions(+), 17 deletions(-) diff --git a/ui/src/pages/Alerts/alerts-page.module.css b/ui/src/pages/Alerts/alerts-page.module.css index 71047bfd..3ed159ae 100644 --- a/ui/src/pages/Alerts/alerts-page.module.css +++ b/ui/src/pages/Alerts/alerts-page.module.css @@ -1,18 +1,94 @@ -.page { padding: 16px; display: flex; flex-direction: column; gap: 12px; } -.toolbar { display: flex; justify-content: space-between; align-items: center; gap: 12px; } -.row { - display: grid; - grid-template-columns: 72px 1fr auto; - gap: 12px; - padding: 12px; - border: 1px solid var(--border); - border-radius: 8px; - background: var(--bg); +.page { + padding: var(--space-md); + display: flex; + flex-direction: column; + gap: var(--space-md); +} + +.toolbar { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--space-sm); + flex-wrap: wrap; +} + +.filterBar { + display: flex; + gap: var(--space-sm); + align-items: center; + flex-wrap: wrap; +} + +.bulkBar { + display: flex; + gap: var(--space-sm); + align-items: center; + padding: var(--space-sm) var(--space-md); + background: var(--bg-hover); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); +} + +.titleCell { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.titleCell a { + color: var(--text-primary); + font-weight: 500; + text-decoration: none; +} + +.titleCell a:hover { + text-decoration: underline; +} + +.titleCellUnread a { + font-weight: 600; +} + +.titlePreview { + font-size: 12px; + color: var(--text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 48ch; +} + +.expanded { + display: flex; + flex-direction: column; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); +} + +.expandedGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); + gap: var(--space-sm); +} + +.expandedField { + display: flex; + flex-direction: column; + gap: 2px; +} + +.expandedLabel { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); +} + +.expandedValue { + font-size: 13px; + color: var(--text-primary); + margin: 0; + word-break: break-word; } -.rowUnread { border-left: 3px solid var(--accent); } -.body { display: flex; flex-direction: column; gap: 4px; min-width: 0; } -.meta { display: flex; gap: 8px; font-size: 12px; color: var(--muted); } -.time { font-variant-numeric: tabular-nums; } -.message { margin: 0; font-size: 13px; color: var(--fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.actions { display: flex; align-items: center; } -.empty { padding: 48px; text-align: center; color: var(--muted); } From 588e0b723a192b5078b30329233ab8af0aa4be31 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:05:39 +0200 Subject: [PATCH 07/49] refactor(alerts/ui): rewrite Inbox as DataTable with expandable rows Replaces custom feed-row layout with the shared DataTable shell used elsewhere in the app. Adds checkbox selection + bulk "Mark selected read" toolbar alongside the existing "Mark all read". Uses DS EmptyState for empty lists, severity-driven rowAccent for unread tinting, and renderAlertExpanded for row detail. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/InboxPage.tsx | 172 ++++++++++++++++++++++++++---- 1 file changed, 151 insertions(+), 21 deletions(-) diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index ba9f2849..1c5213f8 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -1,47 +1,177 @@ -import { useMemo } from 'react'; -import { Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { useMemo, useState } from 'react'; +import { Link } from 'react-router'; +import { Inbox } from 'lucide-react'; +import { + Button, SectionHeader, DataTable, EmptyState, useToast, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; -import { useAlerts, useBulkReadAlerts } from '../../api/queries/alerts'; -import { AlertRow } from './AlertRow'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { + useAlerts, useAckAlert, useBulkReadAlerts, useMarkAlertRead, + type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; export default function InboxPage() { const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); const bulkRead = useBulkReadAlerts(); + const markRead = useMarkAlertRead(); + const ack = useAckAlert(); const { toast } = useToast(); - const unreadIds = useMemo( - () => (data ?? []).filter((a) => a.state === 'FIRING').map((a) => a.id), - [data], - ); - - if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; - + const [selected, setSelected] = useState>(new Set()); const rows = data ?? []; - const onMarkAllRead = async () => { - if (unreadIds.length === 0) return; + const unreadIds = useMemo( + () => rows.filter((a) => a.state === 'FIRING').map((a) => a.id), + [rows], + ); + + const toggleSelected = (id: string) => { + setSelected((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; + + const onAck = async (id: string, title?: string) => { try { - await bulkRead.mutateAsync(unreadIds); - toast({ title: `Marked ${unreadIds.length} as read`, variant: 'success' }); + await ack.mutateAsync(id); + toast({ title: 'Acknowledged', description: title, variant: 'success' }); + } catch (e) { + toast({ title: 'Ack failed', description: String(e), variant: 'error' }); + } + }; + + const onBulkRead = async (ids: string[]) => { + if (ids.length === 0) return; + try { + await bulkRead.mutateAsync(ids); + setSelected(new Set()); + toast({ title: `Marked ${ids.length} as read`, variant: 'success' }); } catch (e) { toast({ title: 'Bulk read failed', description: String(e), variant: 'error' }); } }; + const columns: Column[] = [ + { + key: 'select', header: '', width: '40px', + render: (_, row) => ( + toggleSelected(row.id)} + aria-label={`Select ${row.title ?? row.id}`} + onClick={(e) => e.stopPropagation()} + /> + ), + }, + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => + row.severity ? : null, + }, + { + key: 'state', header: 'State', width: '140px', + render: (_, row) => + row.state ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => { + const unread = row.state === 'FIRING'; + return ( +
+ markRead.mutate(row.id)}> + {row.title ?? '(untitled)'} + + {row.message && {row.message}} +
+ ); + }, + }, + { + key: 'age', header: 'Age', width: '100px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'ack', header: '', width: '70px', + render: (_, row) => + row.state === 'FIRING' ? ( + + ) : null, + }, + ]; + + if (isLoading) return ; + if (error) return
Failed to load alerts: {String(error)}
; + + const selectedIds = Array.from(selected); + return (
Inbox -
+ +
+ + {selectedIds.length > 0 + ? `${selectedIds.length} selected` + : `${unreadIds.length} unread`} + +
+ + +
+
+ {rows.length === 0 ? ( -
No open alerts for you in this environment.
+ } + title="All clear" + description="No open alerts for you in this environment." + /> ) : ( - rows.map((a) => ) +
+ + columns={columns as Column[]} + data={rows as Array} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
)}
); From a74785f64d5b06e1d2af7444466da86fd1f9e9ae Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:07:38 +0200 Subject: [PATCH 08/49] refactor(alerts/ui): rewrite All alerts as DataTable + SegmentedTabs filter Replaces 4-Button filter row with DS SegmentedTabs and custom row rendering with DataTable. Shares expandedContent renderer and severity-driven rowAccent with Inbox. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/AllAlertsPage.tsx | 109 +++++++++++++++++++------- 1 file changed, 82 insertions(+), 27 deletions(-) diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx index db462ba4..4212e7b7 100644 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ b/ui/src/pages/Alerts/AllAlertsPage.tsx @@ -1,50 +1,105 @@ import { useState } from 'react'; -import { SectionHeader, Button } from '@cameleer/design-system'; +import { Link } from 'react-router'; +import { Bell } from 'lucide-react'; +import { + SectionHeader, DataTable, EmptyState, SegmentedTabs, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; -import { useAlerts, type AlertDto } from '../../api/queries/alerts'; -import { AlertRow } from './AlertRow'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { AlertStateChip } from '../../components/AlertStateChip'; +import { + useAlerts, useMarkAlertRead, + type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; type AlertState = NonNullable; -const STATE_FILTERS: Array<{ label: string; values: AlertState[] }> = [ - { label: 'Open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, - { label: 'Firing', values: ['FIRING'] }, - { label: 'Acked', values: ['ACKNOWLEDGED'] }, - { label: 'All', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, -]; +const STATE_FILTERS: Record = { + open: { label: 'Open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, + firing: { label: 'Firing', values: ['FIRING'] }, + acked: { label: 'Acked', values: ['ACKNOWLEDGED'] }, + all: { label: 'All', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, +}; export default function AllAlertsPage() { - const [filterIdx, setFilterIdx] = useState(0); - const filter = STATE_FILTERS[filterIdx]; + const [filterKey, setFilterKey] = useState('open'); + const filter = STATE_FILTERS[filterKey]; const { data, isLoading, error } = useAlerts({ state: filter.values, limit: 200 }); + const markRead = useMarkAlertRead(); + + const rows = data ?? []; + + const columns: Column[] = [ + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => row.severity ? : null, + }, + { + key: 'state', header: 'State', width: '140px', + render: (_, row) => row.state ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => ( +
+ row.id && markRead.mutate(row.id)}> + {row.title ?? '(untitled)'} + + {row.message && {row.message}} +
+ ), + }, + { + key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + ]; if (isLoading) return ; if (error) return
Failed to load alerts: {String(error)}
; - const rows = data ?? []; - return (
All alerts -
- {STATE_FILTERS.map((f, i) => ( - - ))} -
+ +
+ ({ value, label: f.label }))} + active={filterKey} + onChange={setFilterKey} + /> +
+ {rows.length === 0 ? ( -
No alerts match this filter.
+ } + title="No alerts match this filter" + description="Try switching to a different state or widening your criteria." + /> ) : ( - rows.map((a) => ) +
+ + columns={columns as Column[]} + data={rows as Array} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
)}
); From 436a0e4d4c797e18a83317d877e07ecc9f36cf61 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:10:10 +0200 Subject: [PATCH 09/49] refactor(alerts/ui): rewrite History as DataTable + DateRangePicker Replaces custom feed rows with DataTable. Adds a DateRangePicker filter (client-side) defaulting to the last 7 days. Client-side range filter is a stopgap; a server-side range param is a future enhancement captured in the design spec. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/HistoryPage.tsx | 106 +++++++++++++++++++++++++--- 1 file changed, 98 insertions(+), 8 deletions(-) diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx index 27f01bb0..d80bdb7c 100644 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ b/ui/src/pages/Alerts/HistoryPage.tsx @@ -1,26 +1,116 @@ -import { SectionHeader } from '@cameleer/design-system'; +import { useState } from 'react'; +import { Link } from 'react-router'; +import { History } from 'lucide-react'; +import { + SectionHeader, DataTable, EmptyState, DateRangePicker, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; -import { useAlerts } from '../../api/queries/alerts'; -import { AlertRow } from './AlertRow'; +import { SeverityBadge } from '../../components/SeverityBadge'; +import { + useAlerts, type AlertDto, +} from '../../api/queries/alerts'; +import { severityToAccent } from './severity-utils'; +import { formatRelativeTime } from './time-utils'; +import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; +import tableStyles from '../../styles/table-section.module.css'; + +/** Duration in s/m/h/d. Pure, best-effort. */ +function formatDuration(from?: string | null, to?: string | null): string { + if (!from || !to) return '—'; + const ms = new Date(to).getTime() - new Date(from).getTime(); + if (ms < 0 || Number.isNaN(ms)) return '—'; + const sec = Math.floor(ms / 1000); + if (sec < 60) return `${sec}s`; + if (sec < 3600) return `${Math.floor(sec / 60)}m`; + if (sec < 86_400) return `${Math.floor(sec / 3600)}h`; + return `${Math.floor(sec / 86_400)}d`; +} export default function HistoryPage() { + const [dateRange, setDateRange] = useState({ + start: new Date(Date.now() - 7 * 24 * 3600_000), + end: new Date(), + }); + + // useAlerts doesn't accept a time range today; filter client-side. const { data, isLoading, error } = useAlerts({ state: 'RESOLVED', limit: 200 }); + const filtered = (data ?? []).filter((a) => { + if (!a.firedAt) return false; + const t = new Date(a.firedAt).getTime(); + return t >= dateRange.start.getTime() && t <= dateRange.end.getTime(); + }); + + const columns: Column[] = [ + { + key: 'severity', header: 'Severity', width: '110px', + render: (_, row) => row.severity ? : null, + }, + { + key: 'title', header: 'Title', + render: (_, row) => ( +
+ {row.title ?? '(untitled)'} + {row.message && {row.message}} +
+ ), + }, + { + key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, + render: (_, row) => + row.firedAt ? ( + + {formatRelativeTime(row.firedAt)} + + ) : '—', + }, + { + key: 'resolvedAt', header: 'Resolved at', width: '140px', sortable: true, + render: (_, row) => + row.resolvedAt ? ( + + {formatRelativeTime(row.resolvedAt)} + + ) : '—', + }, + { + key: 'duration', header: 'Duration', width: '90px', + render: (_, row) => formatDuration(row.firedAt, row.resolvedAt), + }, + ]; + if (isLoading) return ; if (error) return
Failed to load history: {String(error)}
; - const rows = data ?? []; - return (
History
- {rows.length === 0 ? ( -
No resolved alerts in retention window.
+ +
+ +
+ + {filtered.length === 0 ? ( + } + title="No resolved alerts" + description="Nothing in the selected date range. Try widening it." + /> ) : ( - rows.map((a) => ) +
+ + columns={columns as Column[]} + data={filtered as Array} + sortable + flush + rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} + expandedContent={renderAlertExpanded} + /> +
)}
); From 23f3c3990c8f0bcfcf8fe34db667d14b6c17c485 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:12:25 +0200 Subject: [PATCH 10/49] refactor(alerts/ui): rewrite Rules list with DataTable + Dropdown + ConfirmDialog Replaces raw
with DataTable, raw
- - - - - - - - - - - - {rows.map((r) => ( - - - - - - - - - ))} - -
NameKindSeverityEnabledTargets
{r.name} - onToggle(r)} - disabled={setEnabled.isPending} - /> - {r.targets.length} - {otherEnvs.length > 0 && ( - - )} - -
- )} +
+
+ + + + } + > + Alert rules +
+ + {rows.length === 0 ? ( + } + title="No alert rules" + description="Create one to start evaluating alerts for this environment." + action={ + + + + } + /> + ) : ( +
+ + columns={columns} + data={rows as (AlertRuleResponse & { id: string })[]} + flush + /> +
+ )} + + setPendingDelete(null)} + onConfirm={confirmDelete} + title="Delete alert rule?" + message={ + pendingDelete + ? `Delete rule "${pendingDelete.name}"? Fired alerts are preserved via rule_snapshot.` + : '' + } + confirmText="Delete" + variant="danger" + loading={deleteRule.isPending} + />
); } From 3e815724773f104cc79ace92550c464a080a8229 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:14:19 +0200 Subject: [PATCH 11/49] refactor(alerts/ui): rewrite Silences with DataTable + FormField + ConfirmDialog Replaces raw with DataTable, inline-styled form with proper FormField hints, and native confirm() end-early with ConfirmDialog (warning variant). Adds DS EmptyState for no-silences case. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/SilencesPage.tsx | 127 +++++++++++++++++---------- 1 file changed, 80 insertions(+), 47 deletions(-) diff --git a/ui/src/pages/Alerts/SilencesPage.tsx b/ui/src/pages/Alerts/SilencesPage.tsx index 2703b34b..77596eed 100644 --- a/ui/src/pages/Alerts/SilencesPage.tsx +++ b/ui/src/pages/Alerts/SilencesPage.tsx @@ -1,5 +1,10 @@ import { useState } from 'react'; -import { Button, FormField, Input, SectionHeader, useToast } from '@cameleer/design-system'; +import { BellOff } from 'lucide-react'; +import { + Button, FormField, Input, SectionHeader, useToast, DataTable, + EmptyState, ConfirmDialog, MonoText, +} from '@cameleer/design-system'; +import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { useAlertSilences, @@ -8,6 +13,8 @@ import { type AlertSilenceResponse, } from '../../api/queries/alertSilences'; import sectionStyles from '../../styles/section-card.module.css'; +import tableStyles from '../../styles/table-section.module.css'; +import css from './alerts-page.module.css'; export default function SilencesPage() { const { data, isLoading, error } = useAlertSilences(); @@ -19,9 +26,12 @@ export default function SilencesPage() { const [matcherRuleId, setMatcherRuleId] = useState(''); const [matcherAppSlug, setMatcherAppSlug] = useState(''); const [hours, setHours] = useState(1); + const [pendingEnd, setPendingEnd] = useState(null); if (isLoading) return ; - if (error) return
Failed to load silences: {String(error)}
; + if (error) return
Failed to load silences: {String(error)}
; + + const rows = data ?? []; const onCreate = async () => { const now = new Date(); @@ -50,30 +60,58 @@ export default function SilencesPage() { } }; - const onRemove = async (s: AlertSilenceResponse) => { - if (!confirm(`End silence early?`)) return; + const confirmEnd = async () => { + if (!pendingEnd) return; try { - await remove.mutateAsync(s.id!); + await remove.mutateAsync(pendingEnd.id!); toast({ title: 'Silence removed', variant: 'success' }); } catch (e) { toast({ title: 'Remove failed', description: String(e), variant: 'error' }); + } finally { + setPendingEnd(null); } }; - const rows = data ?? []; + const columns: Column[] = [ + { + key: 'matcher', header: 'Matcher', + render: (_, s) => {JSON.stringify(s.matcher)}, + }, + { key: 'reason', header: 'Reason', render: (_, s) => s.reason ?? '—' }, + { key: 'startsAt', header: 'Starts', width: '200px' }, + { key: 'endsAt', header: 'Ends', width: '200px' }, + { + key: 'actions', header: '', width: '90px', + render: (_, s) => ( + + ), + }, + ]; return ( -
- Alert silences -
-
- +
+
+ Alert silences +
+ +
+
+ setMatcherRuleId(e.target.value)} /> - + setMatcherAppSlug(e.target.value)} /> - + setHours(Number(e.target.value))} /> - + setReason(e.target.value)} @@ -92,39 +130,34 @@ export default function SilencesPage() { Create silence
-
-
- {rows.length === 0 ? ( -

No active or scheduled silences.

- ) : ( -
- - - - - - - - - - - {rows.map((s) => ( - - - - - - - - ))} - -
MatcherReasonStartsEnds
{JSON.stringify(s.matcher)}{s.reason ?? '—'}{s.startsAt}{s.endsAt} - -
- )} - + + + {rows.length === 0 ? ( + } + title="No silences" + description="Nothing is currently silenced in this environment." + /> + ) : ( +
+ + columns={columns} + data={rows.map((s) => ({ ...s, id: s.id ?? '' }))} + flush + /> +
+ )} + + setPendingEnd(null)} + onConfirm={confirmEnd} + title="End silence?" + message="End this silence early? Affected rules will resume firing." + confirmText="End silence" + variant="warning" + loading={remove.isPending} + /> ); } From 0037309e4f4d1be884a162b6f99f0f11864e1a8a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:15:24 +0200 Subject: [PATCH 12/49] chore(alerts/ui): remove obsolete AlertRow.tsx The feed-row component is replaced by DataTable column renderers and the shared renderAlertExpanded content renderer. No callers remain. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/AlertRow.tsx | 48 -------------------------------- 1 file changed, 48 deletions(-) delete mode 100644 ui/src/pages/Alerts/AlertRow.tsx diff --git a/ui/src/pages/Alerts/AlertRow.tsx b/ui/src/pages/Alerts/AlertRow.tsx deleted file mode 100644 index 9f23c9bd..00000000 --- a/ui/src/pages/Alerts/AlertRow.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { Link } from 'react-router'; -import { Button, useToast } from '@cameleer/design-system'; -import { AlertStateChip } from '../../components/AlertStateChip'; -import { SeverityBadge } from '../../components/SeverityBadge'; -import type { AlertDto } from '../../api/queries/alerts'; -import { useAckAlert, useMarkAlertRead } from '../../api/queries/alerts'; -import css from './alerts-page.module.css'; - -export function AlertRow({ alert, unread }: { alert: AlertDto; unread: boolean }) { - const ack = useAckAlert(); - const markRead = useMarkAlertRead(); - const { toast } = useToast(); - - const onAck = async () => { - try { - await ack.mutateAsync(alert.id); - toast({ title: 'Acknowledged', description: alert.title, variant: 'success' }); - } catch (e) { - toast({ title: 'Ack failed', description: String(e), variant: 'error' }); - } - }; - - return ( -
- -
- markRead.mutate(alert.id)}> - {alert.title} - -
- - {alert.firedAt} -
-

{alert.message}

-
-
- {alert.state === 'FIRING' && ( - - )} -
-
- ); -} From 1b6e6ce40c5b59763e0249de6017b478713428e0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:16:47 +0200 Subject: [PATCH 13/49] refactor(alerts/ui): replace undefined CSS vars in wizard.module.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace undefined tokens (--muted, --fg, --accent, --border, --amber-bg) with DS tokens (--text-muted, --text-primary, --amber, --border-subtle, --space-sm|md). Drop .promoteBanner — replaced by DS Alert in follow-up commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../pages/Alerts/RuleEditor/wizard.module.css | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/ui/src/pages/Alerts/RuleEditor/wizard.module.css b/ui/src/pages/Alerts/RuleEditor/wizard.module.css index 4dfd103f..4aee7a9a 100644 --- a/ui/src/pages/Alerts/RuleEditor/wizard.module.css +++ b/ui/src/pages/Alerts/RuleEditor/wizard.module.css @@ -1,31 +1,23 @@ .wizard { - padding: 16px; + padding: var(--space-md); display: flex; flex-direction: column; - gap: 16px; + gap: var(--space-md); } .header { display: flex; justify-content: space-between; align-items: center; - gap: 12px; + gap: var(--space-sm); flex-wrap: wrap; } -.promoteBanner { - padding: 8px 12px; - background: var(--amber-bg, rgba(255, 180, 0, 0.12)); - border: 1px solid var(--amber); - border-radius: 6px; - font-size: 13px; -} - .steps { display: flex; - gap: 8px; - border-bottom: 1px solid var(--border); - padding-bottom: 8px; + gap: var(--space-sm); + border-bottom: 1px solid var(--border-subtle); + padding-bottom: var(--space-sm); } .step { @@ -34,17 +26,22 @@ padding: 8px 12px; border-bottom: 2px solid transparent; cursor: pointer; - color: var(--muted); + color: var(--text-muted); font-size: 13px; + font-family: inherit; +} + +.step:hover { + color: var(--text-primary); } .stepActive { - color: var(--fg); - border-bottom-color: var(--accent); + color: var(--text-primary); + border-bottom-color: var(--amber); } .stepDone { - color: var(--fg); + color: var(--text-primary); } .stepBody { From e861e0199ccc2c2197901194e587ca65cfdec32f Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:17:54 +0200 Subject: [PATCH 14/49] =?UTF-8?q?refactor(alerts/ui):=20wizard=20banners?= =?UTF-8?q?=20=E2=86=92=20DS=20Alert,=20step=20body=20=E2=86=92=20section?= =?UTF-8?q?=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Promote banner and prefill warnings now render as DS Alert components (info / warning variants). Step body wraps in sectionStyles.section for card affordance matching other forms in the app. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Alerts/RuleEditor/RuleEditorWizard.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx index d33f916b..6e8ca575 100644 --- a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx +++ b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; -import { Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { Alert, Button, SectionHeader, useToast } from '@cameleer/design-system'; import { PageLoader } from '../../../components/PageLoader'; import { useAlertRule, @@ -24,6 +24,7 @@ import { prefillFromPromotion, type PrefillWarning } from './promotion-prefill'; import { useCatalog } from '../../../api/queries/catalog'; import { useOutboundConnections } from '../../../api/queries/admin/outboundConnections'; import { useSelectedEnv } from '../../../api/queries/alertMeta'; +import sectionStyles from '../../../styles/section-card.module.css'; import css from './wizard.module.css'; const STEP_LABELS: Record = { @@ -148,15 +149,16 @@ export default function RuleEditorWizard() {
{isEdit ? `Edit rule: ${form.name}` : 'New alert rule'} - {promoteFrom && ( -
- Promoting from {promoteFrom} — review and adjust, then save. -
- )}
+ + {promoteFrom && ( + + Promoting from {promoteFrom} — review and adjust, then save. + + )} + {warnings.length > 0 && ( -
- Review before saving: +
    {warnings.map((w) => (
  • @@ -164,12 +166,14 @@ export default function RuleEditorWizard() {
  • ))}
-
+ )} + -
{body}
+ +
{body}
+
{!ruleId && ( -

+

Save the rule first to preview rendered output.

)} diff --git a/ui/src/pages/Alerts/RuleEditor/TriggerStep.tsx b/ui/src/pages/Alerts/RuleEditor/TriggerStep.tsx index 77da5c56..3ed2cf68 100644 --- a/ui/src/pages/Alerts/RuleEditor/TriggerStep.tsx +++ b/ui/src/pages/Alerts/RuleEditor/TriggerStep.tsx @@ -60,7 +60,7 @@ export function TriggerStep({ Test evaluate (uses saved rule) {!ruleId && ( -

+

Save the rule first to enable test-evaluate.

)} From 05f420d16259e1a3c885885258d6d156c57f3023 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:40:28 +0200 Subject: [PATCH 17/49] fix(alerts/ui): page header, scroll, title preview, bell badge polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Visual regressions surfaced during browser smoke: 1. Page headers — `SectionHeader` renders as 12px uppercase gray (a section divider, not a page title). Replace with proper h2 title + inline subtitle (`N firing · N total` etc.) and right-aligned actions, styled from `alerts-page.module.css`. 2. Undefined `--space-*` tokens — the project (and `@cameleer/design-system`) has never shipped `--space-sm|md|lg|xl`, even though many modules (SensitiveKeysPage, alerts CSS, …) reference them. The fallback to `initial` silently collapsed gaps/paddings to 0. Define the scale in `ui/src/index.css` so every consumer picks it up. 3. List scrolling — DataTable was using default pagination, but with no flex sizing the whole page scrolled. Add `fillHeight` and raise `pageSize`/list `limit` to 200 so the table gets sticky header + internal scroll + pinned pagination footer (Gmail-style). True cursor-based infinite scroll needs a backend change (filed as follow-up — `/alerts` only accepts `limit` today). 4. Title column clipping — `.titlePreview` used `white-space: nowrap` + fixed `max-width`, truncating message mid-UUID. Switch to a 2-line `-webkit-line-clamp` so full context is visible. 5. Notification bell badge invisible — `NotificationBell.module.css` referenced undefined tokens (`--fg`, `--hover-bg`, `--bg`, `--muted`). Map to real DS tokens (`--text-primary`, `--bg-hover`, `#fff`, `--text-muted`). The admin user currently sees no badge because the backend `/alerts/unread-count` returns 0 (read receipts) — that's data, not UI. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/components/NotificationBell.module.css | 10 ++-- ui/src/index.css | 8 +++ ui/src/pages/Alerts/AllAlertsPage.tsx | 30 +++++----- ui/src/pages/Alerts/HistoryPage.tsx | 22 +++++--- ui/src/pages/Alerts/InboxPage.tsx | 32 ++++++----- ui/src/pages/Alerts/RulesListPage.tsx | 27 ++++----- ui/src/pages/Alerts/SilencesPage.tsx | 14 +++-- ui/src/pages/Alerts/alerts-page.module.css | 55 +++++++++++++++---- 8 files changed, 126 insertions(+), 72 deletions(-) diff --git a/ui/src/components/NotificationBell.module.css b/ui/src/components/NotificationBell.module.css index af01b0ba..239ba3cb 100644 --- a/ui/src/components/NotificationBell.module.css +++ b/ui/src/components/NotificationBell.module.css @@ -5,11 +5,11 @@ justify-content: center; width: 32px; height: 32px; - border-radius: 8px; - color: var(--fg); + border-radius: var(--radius-md); + color: var(--text-primary); text-decoration: none; } -.bell:hover { background: var(--hover-bg); } +.bell:hover { background: var(--bg-hover); } .badge { position: absolute; top: 2px; @@ -18,7 +18,7 @@ height: 16px; padding: 0 4px; border-radius: 8px; - color: var(--bg); + color: #fff; font-size: 10px; font-weight: 600; line-height: 16px; @@ -26,4 +26,4 @@ } .badgeCritical { background: var(--error); } .badgeWarning { background: var(--amber); } -.badgeInfo { background: var(--muted); } +.badgeInfo { background: var(--text-muted); } diff --git a/ui/src/index.css b/ui/src/index.css index dd5ad60e..7765690d 100644 --- a/ui/src/index.css +++ b/ui/src/index.css @@ -64,6 +64,14 @@ --text-muted: #766A5E; /* White text on colored badge backgrounds (not in DS yet) */ --text-inverse: #fff; + + /* Spacing scale — DS doesn't ship these, but many app modules reference them. + Keep local here until the DS grows a real spacing system. */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; } [data-theme="dark"] { diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx index 4212e7b7..f607e1b1 100644 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ b/ui/src/pages/Alerts/AllAlertsPage.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link } from 'react-router'; import { Bell } from 'lucide-react'; import { - SectionHeader, DataTable, EmptyState, SegmentedTabs, + DataTable, EmptyState, SegmentedTabs, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -71,17 +71,19 @@ export default function AllAlertsPage() { return (
-
- All alerts -
- -
- ({ value, label: f.label }))} - active={filterKey} - onChange={setFilterKey} - /> -
+
+
+

All alerts

+ {rows.length} shown +
+
+ ({ value, label: f.label }))} + active={filterKey} + onChange={setFilterKey} + /> +
+
{rows.length === 0 ? ( ) : ( -
+
columns={columns as Column[]} data={rows as Array} sortable flush + fillHeight + pageSize={200} rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} expandedContent={renderAlertExpanded} /> diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx index d80bdb7c..7be14ccd 100644 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ b/ui/src/pages/Alerts/HistoryPage.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link } from 'react-router'; import { History } from 'lucide-react'; import { - SectionHeader, DataTable, EmptyState, DateRangePicker, + DataTable, EmptyState, DateRangePicker, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -86,13 +86,15 @@ export default function HistoryPage() { return (
-
- History -
- -
- -
+
+
+

History

+ {filtered.length} resolved +
+
+ +
+
{filtered.length === 0 ? ( ) : ( -
+
columns={columns as Column[]} data={filtered as Array} sortable flush + fillHeight + pageSize={200} rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} expandedContent={renderAlertExpanded} /> diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 1c5213f8..4d55c0fa 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { Link } from 'react-router'; import { Inbox } from 'lucide-react'; import { - Button, SectionHeader, DataTable, EmptyState, useToast, + Button, DataTable, EmptyState, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -19,7 +19,7 @@ import css from './alerts-page.module.css'; import tableStyles from '../../styles/table-section.module.css'; export default function InboxPage() { - const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 100 }); + const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 200 }); const bulkRead = useBulkReadAlerts(); const markRead = useMarkAlertRead(); const ack = useAckAlert(); @@ -123,19 +123,19 @@ export default function InboxPage() { const selectedIds = Array.from(selected); + const subtitle = + selectedIds.length > 0 + ? `${selectedIds.length} selected` + : `${unreadIds.length} firing · ${rows.length} total`; + return (
-
- Inbox -
- -
- - {selectedIds.length > 0 - ? `${selectedIds.length} selected` - : `${unreadIds.length} unread`} - -
+
+
+

Inbox

+ {subtitle} +
+
-
+ {rows.length === 0 ? ( ) : ( -
+
columns={columns as Column[]} data={rows as Array} sortable flush + fillHeight + pageSize={200} rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} expandedContent={renderAlertExpanded} /> diff --git a/ui/src/pages/Alerts/RulesListPage.tsx b/ui/src/pages/Alerts/RulesListPage.tsx index c4489bce..2f502ae6 100644 --- a/ui/src/pages/Alerts/RulesListPage.tsx +++ b/ui/src/pages/Alerts/RulesListPage.tsx @@ -2,7 +2,7 @@ import { useState } from 'react'; import { Link, useNavigate } from 'react-router'; import { FilePlus } from 'lucide-react'; import { - Button, SectionHeader, Toggle, useToast, Badge, DataTable, + Button, Toggle, useToast, Badge, DataTable, EmptyState, Dropdown, ConfirmDialog, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; @@ -111,17 +111,17 @@ export default function RulesListPage() { return (
-
- - - - } - > - Alert rules - -
+
+
+

Alert rules

+ {rows.length} total +
+
+ + + +
+
{rows.length === 0 ? ( ) : ( -
+
columns={columns} data={rows as (AlertRuleResponse & { id: string })[]} flush + fillHeight />
)} diff --git a/ui/src/pages/Alerts/SilencesPage.tsx b/ui/src/pages/Alerts/SilencesPage.tsx index 77596eed..1294031f 100644 --- a/ui/src/pages/Alerts/SilencesPage.tsx +++ b/ui/src/pages/Alerts/SilencesPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { BellOff } from 'lucide-react'; import { - Button, FormField, Input, SectionHeader, useToast, DataTable, + Button, FormField, Input, useToast, DataTable, EmptyState, ConfirmDialog, MonoText, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; @@ -92,9 +92,12 @@ export default function SilencesPage() { return (
-
- Alert silences -
+
+
+

Alert silences

+ {rows.length} active +
+
) : ( -
+
columns={columns} data={rows.map((s) => ({ ...s, id: s.id ?? '' }))} flush + fillHeight />
)} diff --git a/ui/src/pages/Alerts/alerts-page.module.css b/ui/src/pages/Alerts/alerts-page.module.css index 3ed159ae..a45a7c45 100644 --- a/ui/src/pages/Alerts/alerts-page.module.css +++ b/ui/src/pages/Alerts/alerts-page.module.css @@ -3,16 +3,48 @@ display: flex; flex-direction: column; gap: var(--space-md); + height: 100%; + min-height: 0; } -.toolbar { +.pageHeader { display: flex; justify-content: space-between; align-items: center; - gap: var(--space-sm); + gap: var(--space-md); + padding-bottom: var(--space-sm); + border-bottom: 1px solid var(--border-subtle); flex-wrap: wrap; } +.pageTitleGroup { + display: flex; + align-items: baseline; + gap: var(--space-sm); + min-width: 0; +} + +.pageTitle { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + letter-spacing: -0.01em; +} + +.pageSubtitle { + font-size: 13px; + color: var(--text-muted); + font-variant-numeric: tabular-nums; +} + +.pageActions { + display: flex; + gap: var(--space-sm); + align-items: center; + flex-shrink: 0; +} + .filterBar { display: flex; gap: var(--space-sm); @@ -20,14 +52,11 @@ flex-wrap: wrap; } -.bulkBar { +.tableWrap { + flex: 1 1 auto; + min-height: 0; display: flex; - gap: var(--space-sm); - align-items: center; - padding: var(--space-sm) var(--space-md); - background: var(--bg-hover); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-md); + flex-direction: column; } .titleCell { @@ -54,10 +83,12 @@ .titlePreview { font-size: 12px; color: var(--text-muted); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 48ch; + word-break: break-word; + line-height: 1.4; } .expanded { From c443fc606af25b71a5526aea14a18464eaf5923b Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:48:33 +0200 Subject: [PATCH 18/49] fix(alerts/ui): bell position, content tabs hidden, filters, novice labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Surfaced during second smoke: 1. Notification bell moved — was first child of TopBar (left of breadcrumb); now rendered inside the `environment` slot so it sits between the env selector and the user menu, matching user expectations. 2. Content tabs (Exchanges/Dashboard/Runtime/Deployments) hidden on `/alerts/*` — the operational tabs don't apply there. 3. Inbox / All alerts filters now actually filter. `AlertController.list` accepts only `limit` — `state`/`severity` query params are dropped server-side. Move `useAlerts` to fetch once per env (limit 200) and apply filters client-side via react-query `select`, with a stable queryKey so filter toggles are instant and don't re-request. True server-side filter needs a backend change (follow-up). 4. Novice-friendly labels: - Inbox subtitle: "99 firing · 100 total" → "99 need attention · 100 total in inbox" - All alerts filter: Open/Firing/Acked/All → "Currently open"/"Firing now"/"Acknowledged"/"All states" - All alerts subtitle: "N shown" → "N matching your filter" - History subtitle: "N resolved" → "N resolved alert(s) in range" - Rules subtitle: "N total" → "N rule(s) configured" - Silences subtitle: "N active" → "N active silence(s)" or "Nothing silenced right now" - Column headers: "State" → "Status", rules "Kind" → "Type", rules "Targets" → "Notifies" - Buttons: "Ack" → "Acknowledge", silence "End" → "End early" Updated alerts.test.tsx and e2e selector to match new behavior/labels. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/api/queries/alerts.test.tsx | 28 ++++++++++++++++----- ui/src/api/queries/alerts.ts | 36 +++++++++++++++++++++------ ui/src/components/LayoutShell.tsx | 16 ++++++------ ui/src/pages/Alerts/AllAlertsPage.tsx | 12 ++++----- ui/src/pages/Alerts/HistoryPage.tsx | 8 ++++-- ui/src/pages/Alerts/InboxPage.tsx | 8 +++--- ui/src/pages/Alerts/RulesListPage.tsx | 8 +++--- ui/src/pages/Alerts/SilencesPage.tsx | 8 ++++-- ui/src/test/e2e/alerting.spec.ts | 2 +- 9 files changed, 87 insertions(+), 39 deletions(-) diff --git a/ui/src/api/queries/alerts.test.tsx b/ui/src/api/queries/alerts.test.tsx index b8eb3e66..0da5266c 100644 --- a/ui/src/api/queries/alerts.test.tsx +++ b/ui/src/api/queries/alerts.test.tsx @@ -22,7 +22,10 @@ describe('useAlerts', () => { useEnvironmentStore.setState({ environment: 'dev' }); }); - it('fetches alerts for selected env and passes filter query params', async () => { + it('fetches up to 200 alerts for selected env (no server-side filter params)', async () => { + // Backend AlertController.list accepts only `limit`; state/severity are + // dropped server-side. We therefore fetch once per env and filter + // client-side via react-query `select`. (apiClient.GET as any).mockResolvedValue({ data: [], error: null }); const { result } = renderHook( () => useAlerts({ state: 'FIRING', severity: ['CRITICAL', 'WARNING'] }), @@ -34,16 +37,29 @@ describe('useAlerts', () => { expect.objectContaining({ params: expect.objectContaining({ path: { envSlug: 'dev' }, - query: expect.objectContaining({ - state: ['FIRING'], - severity: ['CRITICAL', 'WARNING'], - limit: 100, - }), + query: { limit: 200 }, }), }), ); }); + it('applies state + severity filters client-side via select', async () => { + const dataset = [ + { id: '1', state: 'FIRING', severity: 'CRITICAL', title: 'a' }, + { id: '2', state: 'FIRING', severity: 'WARNING', title: 'b' }, + { id: '3', state: 'ACKNOWLEDGED', severity: 'CRITICAL', title: 'c' }, + { id: '4', state: 'RESOLVED', severity: 'INFO', title: 'd' }, + ]; + (apiClient.GET as any).mockResolvedValue({ data: dataset, error: null }); + const { result } = renderHook( + () => useAlerts({ state: ['FIRING'], severity: ['CRITICAL', 'WARNING'] }), + { wrapper }, + ); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + const ids = (result.current.data ?? []).map((a: any) => a.id); + expect(ids).toEqual(['1', '2']); + }); + it('does not fetch when no env is selected', () => { useEnvironmentStore.setState({ environment: undefined }); const { result } = renderHook(() => useAlerts(), { wrapper }); diff --git a/ui/src/api/queries/alerts.ts b/ui/src/api/queries/alerts.ts index 63b167e7..a13c5a9a 100644 --- a/ui/src/api/queries/alerts.ts +++ b/ui/src/api/queries/alerts.ts @@ -28,11 +28,28 @@ function toArray(v: T | T[] | undefined): T[] | undefined { // openapi-fetch regardless of what the TS types say; we therefore cast the // call options to `any` to bypass the generated type oddity. -/** List alert instances in the current env. Polls every 30s (pauses in background). */ +/** List alert instances in the current env. Polls every 30s (pauses in background). + * + * The backend's `AlertController.list` accepts only `limit` — `state` / + * `severity` query params are dropped. We fetch up to 200 alerts once per + * env (cached under a stable key) and apply filters client-side via + * react-query's `select` so filter switches on the All / History / Inbox + * pages are instant and don't each fire their own request. True server-side + * filtering needs a backend change (follow-up). + */ export function useAlerts(filter: AlertsFilter = {}) { const env = useSelectedEnv(); + const fetchLimit = 200; + const stateSet = filter.state === undefined + ? undefined + : new Set(toArray(filter.state)); + const severitySet = filter.severity === undefined + ? undefined + : new Set(toArray(filter.severity)); + const applyLimit = filter.limit; + const ruleIdFilter = filter.ruleId; return useQuery({ - queryKey: ['alerts', env, filter], + queryKey: ['alerts', env, 'list', fetchLimit], enabled: !!env, refetchInterval: 30_000, refetchIntervalInBackground: false, @@ -43,18 +60,21 @@ export function useAlerts(filter: AlertsFilter = {}) { { params: { path: { envSlug: env }, - query: { - state: toArray(filter.state), - severity: toArray(filter.severity), - ruleId: filter.ruleId, - limit: filter.limit ?? 100, - }, + query: { limit: fetchLimit }, }, } as any, ); if (error) throw error; return data as AlertDto[]; }, + select: (all) => { + let out = all; + if (stateSet) out = out.filter((a) => a.state && stateSet.has(a.state)); + if (severitySet) out = out.filter((a) => a.severity && severitySet.has(a.severity)); + if (ruleIdFilter) out = out.filter((a) => a.ruleId === ruleIdFilter); + if (applyLimit !== undefined) out = out.slice(0, applyLimit); + return out; + }, }); } diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 5f017d21..4ae0be9f 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -957,18 +957,20 @@ function LayoutContent() { + <> + + + } user={username ? { name: username } : undefined} userMenuItems={userMenuItems} onLogout={handleLogout} onNavigate={navigate} > - setPaletteOpen(true)} /> - {!isAdminPage && ( + {!isAdminPage && !isAlertsPage && ( )} diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx index f607e1b1..7de47066 100644 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ b/ui/src/pages/Alerts/AllAlertsPage.tsx @@ -21,10 +21,10 @@ import tableStyles from '../../styles/table-section.module.css'; type AlertState = NonNullable; const STATE_FILTERS: Record = { - open: { label: 'Open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, - firing: { label: 'Firing', values: ['FIRING'] }, - acked: { label: 'Acked', values: ['ACKNOWLEDGED'] }, - all: { label: 'All', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, + open: { label: 'Currently open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, + firing: { label: 'Firing now', values: ['FIRING'] }, + acked: { label: 'Acknowledged', values: ['ACKNOWLEDGED'] }, + all: { label: 'All states', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, }; export default function AllAlertsPage() { @@ -41,7 +41,7 @@ export default function AllAlertsPage() { render: (_, row) => row.severity ? : null, }, { - key: 'state', header: 'State', width: '140px', + key: 'state', header: 'Status', width: '140px', render: (_, row) => row.state ? : null, }, { @@ -74,7 +74,7 @@ export default function AllAlertsPage() {

All alerts

- {rows.length} shown + {rows.length} matching your filter
-

History

- {filtered.length} resolved +

Alert history

+ + {filtered.length === 0 + ? 'No resolved alerts in range' + : `${filtered.length} resolved alert${filtered.length === 1 ? '' : 's'} in range`} +
diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 4d55c0fa..2ce460a0 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -80,7 +80,7 @@ export default function InboxPage() { row.severity ? : null, }, { - key: 'state', header: 'State', width: '140px', + key: 'state', header: 'Status', width: '140px', render: (_, row) => row.state ? : null, }, @@ -108,11 +108,11 @@ export default function InboxPage() { ) : '—', }, { - key: 'ack', header: '', width: '70px', + key: 'ack', header: '', width: '120px', render: (_, row) => row.state === 'FIRING' ? ( ) : null, }, @@ -126,7 +126,7 @@ export default function InboxPage() { const subtitle = selectedIds.length > 0 ? `${selectedIds.length} selected` - : `${unreadIds.length} firing · ${rows.length} total`; + : `${unreadIds.length} need attention · ${rows.length} total in inbox`; return (
diff --git a/ui/src/pages/Alerts/RulesListPage.tsx b/ui/src/pages/Alerts/RulesListPage.tsx index 2f502ae6..890a087a 100644 --- a/ui/src/pages/Alerts/RulesListPage.tsx +++ b/ui/src/pages/Alerts/RulesListPage.tsx @@ -67,7 +67,7 @@ export default function RulesListPage() { render: (_, r) => {r.name}, }, { - key: 'conditionKind', header: 'Kind', width: '160px', + key: 'conditionKind', header: 'Type', width: '160px', render: (_, r) => , }, { @@ -85,7 +85,7 @@ export default function RulesListPage() { ), }, { - key: 'targets', header: 'Targets', width: '90px', + key: 'targets', header: 'Notifies', width: '90px', render: (_, r) => String(r.targets?.length ?? 0), }, { @@ -114,7 +114,9 @@ export default function RulesListPage() {

Alert rules

- {rows.length} total + + {rows.length === 0 ? 'No rules yet' : `${rows.length} rule${rows.length === 1 ? '' : 's'} configured`} +
diff --git a/ui/src/pages/Alerts/SilencesPage.tsx b/ui/src/pages/Alerts/SilencesPage.tsx index 1294031f..52665235 100644 --- a/ui/src/pages/Alerts/SilencesPage.tsx +++ b/ui/src/pages/Alerts/SilencesPage.tsx @@ -84,7 +84,7 @@ export default function SilencesPage() { key: 'actions', header: '', width: '90px', render: (_, s) => ( ), }, @@ -95,7 +95,11 @@ export default function SilencesPage() {

Alert silences

- {rows.length} active + + {rows.length === 0 + ? 'Nothing silenced right now' + : `${rows.length} active silence${rows.length === 1 ? '' : 's'}`} +
diff --git a/ui/src/test/e2e/alerting.spec.ts b/ui/src/test/e2e/alerting.spec.ts index 90aaa1a8..396cc884 100644 --- a/ui/src/test/e2e/alerting.spec.ts +++ b/ui/src/test/e2e/alerting.spec.ts @@ -101,7 +101,7 @@ test.describe('alerting UI smoke', () => { await page .getByRole('row', { name: new RegExp(unique) }) - .getByRole('button', { name: /^end$/i }) + .getByRole('button', { name: /^end early$/i }) .click(); const confirmEnd = page.getByRole('dialog'); await expect(confirmEnd.getByText(/end silence/i)).toBeVisible(); From 468132d1dddeda7c6b31474d0028574423d6a3f8 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:10:20 +0200 Subject: [PATCH 19/49] fix(ui/alerts): bell spacing, rule editor width, inbox bulk controls Round 4 smoke feedback on /alerts: - Bell now has consistent 12px gap from env selector and user name (wrap env + bell in flex container inside TopBar's environment prop) - RuleEditorWizard constrained to max-width 840px (centered) and upgraded the page title from SectionHeader to h2 pattern used by the list pages - Inbox: added select-all checkbox, severity SegmentedTabs filter (All / Critical / Warning / Info), and bulk-ack actions (Acknowledge selected + Acknowledge all firing) alongside the existing mark-read actions --- ui/src/components/LayoutShell.tsx | 4 +- ui/src/pages/Alerts/InboxPage.tsx | 114 +++++++++++++++--- .../Alerts/RuleEditor/RuleEditorWizard.tsx | 4 +- .../pages/Alerts/RuleEditor/wizard.module.css | 11 ++ 4 files changed, 111 insertions(+), 22 deletions(-) diff --git a/ui/src/components/LayoutShell.tsx b/ui/src/components/LayoutShell.tsx index 4ae0be9f..f2c49ea7 100644 --- a/ui/src/components/LayoutShell.tsx +++ b/ui/src/components/LayoutShell.tsx @@ -957,14 +957,14 @@ function LayoutContent() { +
- +
} user={username ? { name: username } : undefined} userMenuItems={userMenuItems} diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 2ce460a0..99be8d12 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { Link } from 'react-router'; import { Inbox } from 'lucide-react'; import { - Button, DataTable, EmptyState, useToast, + Button, DataTable, EmptyState, SegmentedTabs, useToast, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -18,8 +18,24 @@ import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; import tableStyles from '../../styles/table-section.module.css'; +type Severity = NonNullable; + +const SEVERITY_FILTERS: Record = { + all: { label: 'All severities', values: undefined }, + critical: { label: 'Critical', values: ['CRITICAL'] }, + warning: { label: 'Warning', values: ['WARNING'] }, + info: { label: 'Info', values: ['INFO'] }, +}; + export default function InboxPage() { - const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], limit: 200 }); + const [severityKey, setSeverityKey] = useState('all'); + const severityFilter = SEVERITY_FILTERS[severityKey]; + + const { data, isLoading, error } = useAlerts({ + state: ['FIRING', 'ACKNOWLEDGED'], + severity: severityFilter.values, + limit: 200, + }); const bulkRead = useBulkReadAlerts(); const markRead = useMarkAlertRead(); const ack = useAckAlert(); @@ -33,6 +49,11 @@ export default function InboxPage() { [rows], ); + const firingIds = unreadIds; // FIRING alerts are the ones that can be ack'd + + const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id)); + const someSelected = selected.size > 0 && !allSelected; + const toggleSelected = (id: string) => { setSelected((prev) => { const next = new Set(prev); @@ -41,6 +62,14 @@ export default function InboxPage() { }); }; + const toggleSelectAll = () => { + if (allSelected) { + setSelected(new Set()); + } else { + setSelected(new Set(rows.map((r) => r.id))); + } + }; + const onAck = async (id: string, title?: string) => { try { await ack.mutateAsync(id); @@ -50,6 +79,17 @@ export default function InboxPage() { } }; + const onBulkAck = async (ids: string[]) => { + if (ids.length === 0) return; + try { + await Promise.all(ids.map((id) => ack.mutateAsync(id))); + setSelected(new Set()); + toast({ title: `Acknowledged ${ids.length} alert${ids.length === 1 ? '' : 's'}`, variant: 'success' }); + } catch (e) { + toast({ title: 'Bulk ack failed', description: String(e), variant: 'error' }); + } + }; + const onBulkRead = async (ids: string[]) => { if (ids.length === 0) return; try { @@ -122,6 +162,9 @@ export default function InboxPage() { if (error) return
Failed to load alerts: {String(error)}
; const selectedIds = Array.from(selected); + const selectedFiringIds = rows + .filter((r) => selected.has(r.id) && r.state === 'FIRING') + .map((r) => r.id); const subtitle = selectedIds.length > 0 @@ -136,25 +179,60 @@ export default function InboxPage() { {subtitle}
- - + ({ value, label: f.label }))} + active={severityKey} + onChange={setSeverityKey} + />
+
+ + + + + + +
+ {rows.length === 0 ? ( } diff --git a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx index 6e8ca575..71f12450 100644 --- a/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx +++ b/ui/src/pages/Alerts/RuleEditor/RuleEditorWizard.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams, useSearchParams } from 'react-router'; -import { Alert, Button, SectionHeader, useToast } from '@cameleer/design-system'; +import { Alert, Button, useToast } from '@cameleer/design-system'; import { PageLoader } from '../../../components/PageLoader'; import { useAlertRule, @@ -148,7 +148,7 @@ export default function RuleEditorWizard() { return (
- {isEdit ? `Edit rule: ${form.name}` : 'New alert rule'} +

{isEdit ? `Edit rule: ${form.name}` : 'New alert rule'}

{promoteFrom && ( diff --git a/ui/src/pages/Alerts/RuleEditor/wizard.module.css b/ui/src/pages/Alerts/RuleEditor/wizard.module.css index 4aee7a9a..3d99608c 100644 --- a/ui/src/pages/Alerts/RuleEditor/wizard.module.css +++ b/ui/src/pages/Alerts/RuleEditor/wizard.module.css @@ -3,6 +3,17 @@ display: flex; flex-direction: column; gap: var(--space-md); + max-width: 840px; + margin: 0 auto; + width: 100%; +} + +.pageTitle { + font-size: 20px; + font-weight: 600; + color: var(--text-primary); + margin: 0; + letter-spacing: -0.01em; } .header { From f037d8c922c6451581e729c8b67db7afe8e365fe Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 12:47:31 +0200 Subject: [PATCH 20/49] feat(alerting): server-side state+severity filters, ButtonGroup filter UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: `GET /environments/{envSlug}/alerts` now accepts optional multi-value `state=…` and `severity=…` query params. Filters are pushed down to PostgresAlertInstanceRepository, which appends `AND state::text = ANY(?)` / `AND severity::text = ANY(?)` to the inbox query (null/empty = no filter). `AlertInstanceRepository.listForInbox` gained a 7-arg overload; the old 5-arg form is preserved as a default delegate so existing callers (evaluator, AlertingFullLifecycleIT, PostgresAlertInstanceRepositoryIT) compile unchanged. `InAppInboxQuery.listInbox` also has a new filtered overload. UI: InboxPage severity filter migrated from `SegmentedTabs` (single-select, no color cues) to `ButtonGroup` (multi-select with severity-coloured dots), matching the topnavbar status-filter pattern. `useAlerts` forwards the filters as query params and cache-keys on the filter tuple so each combo is independently cached. Unit + hook tests updated to the new contract (5 UI tests + 8 Java unit tests passing). OpenAPI types regenerated from the fresh local backend. --- .../alerting/controller/AlertController.java | 8 +++- .../app/alerting/notify/InAppInboxQuery.java | 16 ++++++- .../PostgresAlertInstanceRepository.java | 26 +++++++++--- .../alerting/notify/InAppInboxQueryTest.java | 22 +++++++++- .../alerting/AlertInstanceRepository.java | 20 +++++++++ ui/src/api/openapi.json | 2 +- ui/src/api/queries/alerts.test.tsx | 36 +++++++++++----- ui/src/api/queries/alerts.ts | 42 ++++++++----------- ui/src/api/schema.d.ts | 2 + ui/src/pages/Alerts/InboxPage.tsx | 31 +++++++------- 10 files changed, 144 insertions(+), 61 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java index 88b48c5d..eb65b0d5 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java @@ -8,6 +8,8 @@ import com.cameleer.server.app.web.EnvPath; import com.cameleer.server.core.alerting.AlertInstance; import com.cameleer.server.core.alerting.AlertInstanceRepository; import com.cameleer.server.core.alerting.AlertReadRepository; +import com.cameleer.server.core.alerting.AlertSeverity; +import com.cameleer.server.core.alerting.AlertState; import com.cameleer.server.core.runtime.Environment; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -54,10 +56,12 @@ public class AlertController { @GetMapping public List list( @EnvPath Environment env, - @RequestParam(defaultValue = "50") int limit) { + @RequestParam(defaultValue = "50") int limit, + @RequestParam(required = false) List state, + @RequestParam(required = false) List severity) { String userId = currentUserId(); int effectiveLimit = Math.min(limit, 200); - return inboxQuery.listInbox(env.id(), userId, effectiveLimit) + return inboxQuery.listInbox(env.id(), userId, state, severity, effectiveLimit) .stream().map(AlertDto::from).toList(); } diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java index b0cf6088..88f8906b 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java @@ -4,6 +4,7 @@ import com.cameleer.server.app.alerting.dto.UnreadCountResponse; import com.cameleer.server.core.alerting.AlertInstance; import com.cameleer.server.core.alerting.AlertInstanceRepository; import com.cameleer.server.core.alerting.AlertSeverity; +import com.cameleer.server.core.alerting.AlertState; import com.cameleer.server.core.rbac.RbacService; import org.springframework.stereotype.Component; @@ -54,9 +55,22 @@ public class InAppInboxQuery { * or target a role the user holds. Empty target lists mean "broadcast to all". */ public List listInbox(UUID envId, String userId, int limit) { + return listInbox(envId, userId, null, null, limit); + } + + /** + * Filtered variant of {@link #listInbox(UUID, String, int)}: optional {@code states} + * and {@code severities} narrow the result set. {@code null} or empty lists mean + * "no filter on that dimension". + */ + public List listInbox(UUID envId, + String userId, + List states, + List severities, + int limit) { List groupIds = resolveGroupIds(userId); List roleNames = resolveRoleNames(userId); - return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, limit); + return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, limit); } /** diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index e69f0715..c2cfdd60 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -98,12 +98,14 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository List userGroupIdFilter, String userId, List userRoleNames, + List states, + List severities, int limit) { // Build arrays for group UUIDs and role names Array groupArray = toUuidArrayFromStrings(userGroupIdFilter); Array roleArray = toTextArray(userRoleNames); - String sql = """ + StringBuilder sql = new StringBuilder(""" SELECT * FROM alert_instances WHERE environment_id = ? AND ( @@ -111,10 +113,24 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository OR target_group_ids && ? OR target_role_names && ? ) - ORDER BY fired_at DESC - LIMIT ? - """; - return jdbc.query(sql, rowMapper(), environmentId, userId, groupArray, roleArray, limit); + """); + List args = new ArrayList<>(List.of(environmentId, userId, groupArray, roleArray)); + + if (states != null && !states.isEmpty()) { + Array stateArray = toTextArray(states.stream().map(Enum::name).toList()); + sql.append(" AND state::text = ANY(?)"); + args.add(stateArray); + } + if (severities != null && !severities.isEmpty()) { + Array severityArray = toTextArray(severities.stream().map(Enum::name).toList()); + sql.append(" AND severity::text = ANY(?)"); + args.add(severityArray); + } + + sql.append(" ORDER BY fired_at DESC LIMIT ?"); + args.add(limit); + + return jdbc.query(sql.toString(), rowMapper(), args.toArray()); } @Override diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java index 34246977..24430e35 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java @@ -75,13 +75,31 @@ class InAppInboxQueryTest { .thenReturn(List.of(new RoleSummary(roleId, "OPERATOR", true, "direct"))); when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of(groupId.toString())), - eq(USER_ID), eq(List.of("OPERATOR")), eq(20))) + eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), eq(20))) .thenReturn(List.of()); List result = query.listInbox(ENV_ID, USER_ID, 20); assertThat(result).isEmpty(); verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()), - USER_ID, List.of("OPERATOR"), 20); + USER_ID, List.of("OPERATOR"), null, null, 20); + } + + @Test + void listInbox_forwardsStateAndSeverityFilters() { + when(rbacService.getEffectiveGroupsForUser(USER_ID)).thenReturn(List.of()); + when(rbacService.getEffectiveRolesForUser(USER_ID)).thenReturn(List.of()); + + List states = + List.of(com.cameleer.server.core.alerting.AlertState.FIRING); + List severities = List.of(AlertSeverity.CRITICAL, AlertSeverity.WARNING); + + when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of()), eq(USER_ID), eq(List.of()), + eq(states), eq(severities), eq(25))) + .thenReturn(List.of()); + + query.listInbox(ENV_ID, USER_ID, states, severities, 25); + verify(instanceRepo).listForInbox(ENV_ID, List.of(), USER_ID, List.of(), + states, severities, 25); } // ------------------------------------------------------------------------- diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java index 1b41ec55..d2e22278 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java @@ -10,10 +10,30 @@ public interface AlertInstanceRepository { AlertInstance save(AlertInstance instance); // upsert by id Optional findById(UUID id); Optional findOpenForRule(UUID ruleId); // state IN ('PENDING','FIRING','ACKNOWLEDGED') + + /** + * Unfiltered inbox listing. Convenience overload that delegates to the filtered + * variant with {@code states}/{@code severities} set to {@code null} (no filter). + */ + default List listForInbox(UUID environmentId, + List userGroupIdFilter, + String userId, + List userRoleNames, + int limit) { + return listForInbox(environmentId, userGroupIdFilter, userId, userRoleNames, null, null, limit); + } + + /** + * Inbox listing with optional state + severity filters. {@code null} or empty lists mean + * "no filter on that field". When both lists are non-empty the row must match at least one + * value from each list (AND between dimensions, OR within). + */ List listForInbox(UUID environmentId, List userGroupIdFilter, String userId, List userRoleNames, + List states, + List severities, int limit); /** diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index 5a0bbebc..e4999b15 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReadRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkReadRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReadRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"state","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]}}},{"name":"severity","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["CRITICAL","WARNING","INFO"]}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkReadRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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 diff --git a/ui/src/api/queries/alerts.test.tsx b/ui/src/api/queries/alerts.test.tsx index 0da5266c..56c86b1b 100644 --- a/ui/src/api/queries/alerts.test.tsx +++ b/ui/src/api/queries/alerts.test.tsx @@ -22,16 +22,32 @@ describe('useAlerts', () => { useEnvironmentStore.setState({ environment: 'dev' }); }); - it('fetches up to 200 alerts for selected env (no server-side filter params)', async () => { - // Backend AlertController.list accepts only `limit`; state/severity are - // dropped server-side. We therefore fetch once per env and filter - // client-side via react-query `select`. + it('forwards state + severity filters to the server as query params', async () => { (apiClient.GET as any).mockResolvedValue({ data: [], error: null }); const { result } = renderHook( () => useAlerts({ state: 'FIRING', severity: ['CRITICAL', 'WARNING'] }), { wrapper }, ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(apiClient.GET).toHaveBeenCalledWith( + '/environments/{envSlug}/alerts', + expect.objectContaining({ + params: expect.objectContaining({ + path: { envSlug: 'dev' }, + query: { + limit: 200, + state: ['FIRING'], + severity: ['CRITICAL', 'WARNING'], + }, + }), + }), + ); + }); + + it('omits state + severity when no filter is set', async () => { + (apiClient.GET as any).mockResolvedValue({ data: [], error: null }); + const { result } = renderHook(() => useAlerts(), { wrapper }); + await waitFor(() => expect(result.current.isSuccess).toBe(true)); expect(apiClient.GET).toHaveBeenCalledWith( '/environments/{envSlug}/alerts', expect.objectContaining({ @@ -43,21 +59,19 @@ describe('useAlerts', () => { ); }); - it('applies state + severity filters client-side via select', async () => { + it('still applies ruleId client-side via select', async () => { const dataset = [ - { id: '1', state: 'FIRING', severity: 'CRITICAL', title: 'a' }, - { id: '2', state: 'FIRING', severity: 'WARNING', title: 'b' }, - { id: '3', state: 'ACKNOWLEDGED', severity: 'CRITICAL', title: 'c' }, - { id: '4', state: 'RESOLVED', severity: 'INFO', title: 'd' }, + { id: '1', ruleId: 'R1', state: 'FIRING', severity: 'WARNING', title: 'a' }, + { id: '2', ruleId: 'R2', state: 'FIRING', severity: 'WARNING', title: 'b' }, ]; (apiClient.GET as any).mockResolvedValue({ data: dataset, error: null }); const { result } = renderHook( - () => useAlerts({ state: ['FIRING'], severity: ['CRITICAL', 'WARNING'] }), + () => useAlerts({ ruleId: 'R2' }), { wrapper }, ); await waitFor(() => expect(result.current.isSuccess).toBe(true)); const ids = (result.current.data ?? []).map((a: any) => a.id); - expect(ids).toEqual(['1', '2']); + expect(ids).toEqual(['2']); }); it('does not fetch when no env is selected', () => { diff --git a/ui/src/api/queries/alerts.ts b/ui/src/api/queries/alerts.ts index a13c5a9a..eab53cb0 100644 --- a/ui/src/api/queries/alerts.ts +++ b/ui/src/api/queries/alerts.ts @@ -30,51 +30,45 @@ function toArray(v: T | T[] | undefined): T[] | undefined { /** List alert instances in the current env. Polls every 30s (pauses in background). * - * The backend's `AlertController.list` accepts only `limit` — `state` / - * `severity` query params are dropped. We fetch up to 200 alerts once per - * env (cached under a stable key) and apply filters client-side via - * react-query's `select` so filter switches on the All / History / Inbox - * pages are instant and don't each fire their own request. True server-side - * filtering needs a backend change (follow-up). + * State + severity filters are server-side (`state=FIRING&state=ACKNOWLEDGED&severity=CRITICAL`). + * `ruleId` is not a backend param and is still applied via react-query `select`. + * Each unique (state, severity) combo gets its own cache entry so the server + * honors the filter and stays as the source of truth. */ export function useAlerts(filter: AlertsFilter = {}) { const env = useSelectedEnv(); - const fetchLimit = 200; - const stateSet = filter.state === undefined - ? undefined - : new Set(toArray(filter.state)); - const severitySet = filter.severity === undefined - ? undefined - : new Set(toArray(filter.severity)); - const applyLimit = filter.limit; + const fetchLimit = Math.min(filter.limit ?? 200, 200); + const stateArr = toArray(filter.state); + const severityArr = toArray(filter.severity); const ruleIdFilter = filter.ruleId; + + // Stable, serialisable key — arrays must be sorted so order doesn't create cache misses. + const stateKey = stateArr ? [...stateArr].sort() : null; + const severityKey = severityArr ? [...severityArr].sort() : null; + return useQuery({ - queryKey: ['alerts', env, 'list', fetchLimit], + queryKey: ['alerts', env, 'list', fetchLimit, stateKey, severityKey], enabled: !!env, refetchInterval: 30_000, refetchIntervalInBackground: false, queryFn: async () => { if (!env) throw new Error('no env'); + const query: Record = { limit: fetchLimit }; + if (stateArr && stateArr.length > 0) query.state = stateArr; + if (severityArr && severityArr.length > 0) query.severity = severityArr; const { data, error } = await apiClient.GET( '/environments/{envSlug}/alerts', { params: { path: { envSlug: env }, - query: { limit: fetchLimit }, + query, }, } as any, ); if (error) throw error; return data as AlertDto[]; }, - select: (all) => { - let out = all; - if (stateSet) out = out.filter((a) => a.state && stateSet.has(a.state)); - if (severitySet) out = out.filter((a) => a.severity && severitySet.has(a.severity)); - if (ruleIdFilter) out = out.filter((a) => a.ruleId === ruleIdFilter); - if (applyLimit !== undefined) out = out.slice(0, applyLimit); - return out; - }, + select: (all) => ruleIdFilter ? all.filter((a) => a.ruleId === ruleIdFilter) : all, }); } diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 26a7f7e7..55270541 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -7213,6 +7213,8 @@ export interface operations { query: { env: components["schemas"]["Environment"]; limit?: number; + state?: ("PENDING" | "FIRING" | "ACKNOWLEDGED" | "RESOLVED")[]; + severity?: ("CRITICAL" | "WARNING" | "INFO")[]; }; header?: never; path?: never; diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 99be8d12..0ac3a1ff 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -2,9 +2,9 @@ import { useMemo, useState } from 'react'; import { Link } from 'react-router'; import { Inbox } from 'lucide-react'; import { - Button, DataTable, EmptyState, SegmentedTabs, useToast, + Button, ButtonGroup, DataTable, EmptyState, useToast, } from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; +import type { ButtonGroupItem, Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { SeverityBadge } from '../../components/SeverityBadge'; import { AlertStateChip } from '../../components/AlertStateChip'; @@ -20,20 +20,21 @@ import tableStyles from '../../styles/table-section.module.css'; type Severity = NonNullable; -const SEVERITY_FILTERS: Record = { - all: { label: 'All severities', values: undefined }, - critical: { label: 'Critical', values: ['CRITICAL'] }, - warning: { label: 'Warning', values: ['WARNING'] }, - info: { label: 'Info', values: ['INFO'] }, -}; +const SEVERITY_ITEMS: ButtonGroupItem[] = [ + { value: 'CRITICAL', label: 'Critical', color: 'var(--error)' }, + { value: 'WARNING', label: 'Warning', color: 'var(--warning)' }, + { value: 'INFO', label: 'Info', color: 'var(--text-muted)' }, +]; export default function InboxPage() { - const [severityKey, setSeverityKey] = useState('all'); - const severityFilter = SEVERITY_FILTERS[severityKey]; + const [severitySel, setSeveritySel] = useState>(new Set()); + const severityValues: Severity[] | undefined = severitySel.size === 0 + ? undefined + : [...severitySel] as Severity[]; const { data, isLoading, error } = useAlerts({ state: ['FIRING', 'ACKNOWLEDGED'], - severity: severityFilter.values, + severity: severityValues, limit: 200, }); const bulkRead = useBulkReadAlerts(); @@ -179,10 +180,10 @@ export default function InboxPage() { {subtitle}
- ({ value, label: f.label }))} - active={severityKey} - onChange={setSeverityKey} +
From e8de8d88adfeac156fbc5eff84787c8d3c07ae4a Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:05:32 +0200 Subject: [PATCH 21/49] refactor(ui/alerts/all): state filter to ButtonGroup (topnavbar style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the SegmentedTabs with multi-select ButtonGroup, matching the topnavbar Completed/Warning/Failed/Running pattern. State dots use the same palette as AlertStateChip (FIRING=error, ACKNOWLEDGED=warning, PENDING=muted, RESOLVED=success). Default selection is the three "open" states — Resolved is off by default and a single click surfaces closed alerts without navigating to /history. --- ui/src/pages/Alerts/AllAlertsPage.tsx | 35 +++++++++++++++------------ 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx index 7de47066..a398109a 100644 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ b/ui/src/pages/Alerts/AllAlertsPage.tsx @@ -2,9 +2,9 @@ import { useState } from 'react'; import { Link } from 'react-router'; import { Bell } from 'lucide-react'; import { - DataTable, EmptyState, SegmentedTabs, + ButtonGroup, DataTable, EmptyState, } from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; +import type { ButtonGroupItem, Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { SeverityBadge } from '../../components/SeverityBadge'; import { AlertStateChip } from '../../components/AlertStateChip'; @@ -20,17 +20,22 @@ import tableStyles from '../../styles/table-section.module.css'; type AlertState = NonNullable; -const STATE_FILTERS: Record = { - open: { label: 'Currently open', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED'] }, - firing: { label: 'Firing now', values: ['FIRING'] }, - acked: { label: 'Acknowledged', values: ['ACKNOWLEDGED'] }, - all: { label: 'All states', values: ['PENDING', 'FIRING', 'ACKNOWLEDGED', 'RESOLVED'] }, -}; +const STATE_ITEMS: ButtonGroupItem[] = [ + { value: 'FIRING', label: 'Firing', color: 'var(--error)' }, + { value: 'ACKNOWLEDGED', label: 'Acknowledged', color: 'var(--warning)' }, + { value: 'PENDING', label: 'Pending', color: 'var(--text-muted)' }, + { value: 'RESOLVED', label: 'Resolved', color: 'var(--success)' }, +]; + +const DEFAULT_OPEN_STATES = new Set(['PENDING', 'FIRING', 'ACKNOWLEDGED']); export default function AllAlertsPage() { - const [filterKey, setFilterKey] = useState('open'); - const filter = STATE_FILTERS[filterKey]; - const { data, isLoading, error } = useAlerts({ state: filter.values, limit: 200 }); + const [stateSel, setStateSel] = useState>(() => new Set(DEFAULT_OPEN_STATES)); + const stateValues: AlertState[] | undefined = stateSel.size === 0 + ? undefined + : [...stateSel] as AlertState[]; + + const { data, isLoading, error } = useAlerts({ state: stateValues, limit: 200 }); const markRead = useMarkAlertRead(); const rows = data ?? []; @@ -77,10 +82,10 @@ export default function AllAlertsPage() { {rows.length} matching your filter
- ({ value, label: f.label }))} - active={filterKey} - onChange={setFilterKey} +
From 23d02ba6a046c32ec955066faf90061145fc54a2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:10:43 +0200 Subject: [PATCH 22/49] refactor(ui/alerts): tighter inbox action bar, history uses global time range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inbox: replace 4 parallel outlined buttons with 2 context-aware ones. When nothing is selected → "Acknowledge all firing" (primary) + "Mark all read" (secondary). When rows are selected → the same slots become "Acknowledge N" + "Mark N read" with counts inlined. Primary variant gives the foreground action proper visual weight; secondary is the supporting action. No more visually-identical disabled buttons cluttering the bar. History: drop the local DateRangePicker. The page now reads `timeRange` from `useGlobalFilters()` so the top-bar TimeRangeDropdown (1h / 3h / 6h / Today / 24h / 7d / custom) is the single source of truth, consistent with every other time-scoped page in the app. --- ui/src/pages/Alerts/HistoryPage.tsx | 16 ++----- ui/src/pages/Alerts/InboxPage.tsx | 73 ++++++++++++++++------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx index 59bda291..a7e22922 100644 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ b/ui/src/pages/Alerts/HistoryPage.tsx @@ -1,8 +1,7 @@ -import { useState } from 'react'; import { Link } from 'react-router'; import { History } from 'lucide-react'; import { - DataTable, EmptyState, DateRangePicker, + DataTable, EmptyState, useGlobalFilters, } from '@cameleer/design-system'; import type { Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; @@ -29,18 +28,16 @@ function formatDuration(from?: string | null, to?: string | null): string { } export default function HistoryPage() { - const [dateRange, setDateRange] = useState({ - start: new Date(Date.now() - 7 * 24 * 3600_000), - end: new Date(), - }); + const { timeRange } = useGlobalFilters(); - // useAlerts doesn't accept a time range today; filter client-side. + // useAlerts doesn't accept a time range today; filter client-side + // against the global TimeRangeDropdown in the top bar. const { data, isLoading, error } = useAlerts({ state: 'RESOLVED', limit: 200 }); const filtered = (data ?? []).filter((a) => { if (!a.firedAt) return false; const t = new Date(a.firedAt).getTime(); - return t >= dateRange.start.getTime() && t <= dateRange.end.getTime(); + return t >= timeRange.start.getTime() && t <= timeRange.end.getTime(); }); const columns: Column[] = [ @@ -95,9 +92,6 @@ export default function HistoryPage() { : `${filtered.length} resolved alert${filtered.length === 1 ? '' : 's'} in range`} -
- -
{filtered.length === 0 ? ( diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 0ac3a1ff..b7760bdf 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -200,38 +200,47 @@ export default function InboxPage() { {allSelected ? 'Deselect all' : `Select all${rows.length ? ` (${rows.length})` : ''}`} - - - - + {selectedIds.length > 0 ? ( + <> + + + + ) : ( + <> + + + + )} {rows.length === 0 ? ( From 414f7204bfc0c5274c46d11c640ae4f727d72437 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:52:08 +0200 Subject: [PATCH 23/49] feat(alerting): AGENT_LIFECYCLE condition kind with per-subject fire mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows alert rules to fire on agent-lifecycle events — REGISTERED, RE_REGISTERED, DEREGISTERED, WENT_STALE, WENT_DEAD, RECOVERED — rather than only on current state. Each matching `(agent, eventType, timestamp)` becomes its own ackable AlertInstance, so outages on distinct agents are independently routable. Core: - New `ConditionKind.AGENT_LIFECYCLE` + `AgentLifecycleCondition` record (scope, eventTypes, withinSeconds). Compact ctor rejects empty eventTypes and withinSeconds<1. - Strict allowlist enum `AgentLifecycleEventType` (six entries matching the server-emitted types in `AgentRegistrationController` and `AgentLifecycleMonitor`). Custom agent-emitted event types tracked in backlog issue #145. - `AgentEventRepository.findInWindow(env, appSlug, agentId, eventTypes, from, to, limit)` — new read path ordered `(timestamp ASC, insert_id ASC)` used by the evaluator. Implemented on `ClickHouseAgentEventRepository` with tenant + env filter mandatory. App: - `AgentLifecycleEvaluator` queries events in the last `withinSeconds` window and returns `EvalResult.Batch` with one `Firing` per row. Every Firing carries a canonical `_subjectFingerprint` of `"::"` in context plus `agent` / `event` subtrees for Mustache templating. - `NotificationContextBuilder` gains an `AGENT_LIFECYCLE` branch that exposes `{{agent.id}}`, `{{agent.app}}`, `{{event.type}}`, `{{event.timestamp}}`, `{{event.detail}}`. - Validation is delegated to the record compact ctor + enum at Jackson deserialization time — matches the existing policy of keeping controller validators focused on env-scoped / SQL-injection concerns. Schema: - V16 migration generalises the V15 per-exchange discriminator on `alert_instances_open_rule_uq` to prefer `_subjectFingerprint` with a fallback to the legacy `exchange.id` expression. Scalar kinds still resolve to `''` and keep one-open-per-rule. Duplicate-key path in `PostgresAlertInstanceRepository.save` is unchanged — the index is the deduper. UI: - New `AgentLifecycleForm.tsx` wizard form with multi-select chips for the six allowed event types + `withinSeconds` input. Wired into `ConditionStep`, `form-state` (validation + defaults: WENT_DEAD, 300 s), and `enums.ts` options. Tests in `enums.test.ts` pin the new option array. - `alert-variables.ts` registers `{{agent.app}}`, `{{event.type}}`, `{{event.timestamp}}`, `{{event.detail}}` leaves for the new kind, and extends `agent.id`'s availability list to include `AGENT_LIFECYCLE`. Tests (all passing): - 5 new JSON-roundtrip cases on `AlertConditionJsonTest` (positive + empty/zero/unknown-type rejection). - 5 new evaluator unit tests on `AgentLifecycleEvaluatorTest` (empty window, multi-agent fingerprint shape, scope forwarding, missing env). - `NotificationContextBuilderTest` switch now covers the new kind. - 119 alerting unit tests + 71 UI tests green. Docs: `.claude/rules/{core,app,ui}` and CLAUDE.md migration list updated. --- .claude/rules/app-classes.md | 4 +- .claude/rules/core-classes.md | 2 +- .claude/rules/ui.md | 2 +- CLAUDE.md | 4 +- .../eval/AgentLifecycleEvaluator.java | 95 +++++++++++++ .../notify/NotificationContextBuilder.java | 4 + .../ClickHouseAgentEventRepository.java | 53 +++++++ ...6__alert_instances_subject_fingerprint.sql | 27 ++++ .../eval/AgentLifecycleEvaluatorTest.java | 130 ++++++++++++++++++ .../NotificationContextBuilderTest.java | 4 + .../core/agent/AgentEventRepository.java | 16 +++ .../alerting/AgentLifecycleCondition.java | 34 +++++ .../alerting/AgentLifecycleEventType.java | 20 +++ .../server/core/alerting/AlertCondition.java | 4 +- .../server/core/alerting/ConditionKind.java | 10 +- .../core/alerting/AlertConditionJsonTest.java | 46 +++++++ ui/src/api/openapi.json | 2 +- ui/src/api/schema.d.ts | 32 +++-- .../MustacheEditor/alert-variables.ts | 12 +- .../pages/Alerts/RuleEditor/ConditionStep.tsx | 9 ++ .../condition-forms/AgentLifecycleForm.tsx | 72 ++++++++++ ui/src/pages/Alerts/RuleEditor/form-state.ts | 7 + ui/src/pages/Alerts/enums.test.ts | 13 ++ ui/src/pages/Alerts/enums.ts | 19 +++ 24 files changed, 601 insertions(+), 20 deletions(-) create mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluator.java create mode 100644 cameleer-server-app/src/main/resources/db/migration/V16__alert_instances_subject_fingerprint.sql create mode 100644 cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluatorTest.java create mode 100644 cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleCondition.java create mode 100644 cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleEventType.java create mode 100644 ui/src/pages/Alerts/RuleEditor/condition-forms/AgentLifecycleForm.tsx diff --git a/.claude/rules/app-classes.md b/.claude/rules/app-classes.md index 25257395..e04365e0 100644 --- a/.claude/rules/app-classes.md +++ b/.claude/rules/app-classes.md @@ -65,8 +65,8 @@ Env-scoped read-path controllers (`AlertController`, `AlertRuleController`, `Ale - `AgentEventsController` — GET `/api/v1/environments/{envSlug}/agents/events` (lifecycle events; cursor-paginated, returns `{ data, nextCursor, hasMore }`; order `(timestamp DESC, insert_id DESC)`; cursor is base64url of `"{timestampIso}|{insert_id_uuid}"` — `insert_id` is a stable UUID column used as a same-millisecond tiebreak). - `AgentMetricsController` — GET `/api/v1/environments/{envSlug}/agents/{agentId}/metrics` (JVM/Camel metrics). Rejects cross-env agents (404) as defence-in-depth. - `DiagramRenderController` — GET `/api/v1/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram` (env-scoped lookup). Also GET `/api/v1/diagrams/{contentHash}/render` (flat — content hashes are globally unique). -- `AlertRuleController` — `/api/v1/environments/{envSlug}/alerts/rules`. GET list / POST create / GET `{id}` / PUT `{id}` / DELETE `{id}` / POST `{id}/enable` / POST `{id}/disable` / POST `{id}/render-preview` / POST `{id}/test-evaluate`. OPERATOR+ for mutations, VIEWER+ for reads. CRITICAL: attribute keys in `ExchangeMatchCondition.filter.attributes` are validated at rule-save time against `^[a-zA-Z0-9._-]+$` — they are later inlined into ClickHouse SQL. Webhook validation: verifies `outboundConnectionId` exists and `isAllowedInEnvironment`. Null notification templates default to `""` (NOT NULL constraint). Audit: `ALERT_RULE_CHANGE`. -- `AlertController` — `/api/v1/environments/{envSlug}/alerts`. GET list (inbox filtered by userId/groupIds/roleNames via `InAppInboxQuery`) / GET `/unread-count` / GET `{id}` / POST `{id}/ack` / POST `{id}/read` / POST `/bulk-read`. VIEWER+ for all. Inbox SQL: `? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?` — requires at least one matching target (no broadcast concept). +- `AlertRuleController` — `/api/v1/environments/{envSlug}/alerts/rules`. GET list / POST create / GET `{id}` / PUT `{id}` / DELETE `{id}` / POST `{id}/enable` / POST `{id}/disable` / POST `{id}/render-preview` / POST `{id}/test-evaluate`. OPERATOR+ for mutations, VIEWER+ for reads. CRITICAL: attribute keys in `ExchangeMatchCondition.filter.attributes` are validated at rule-save time against `^[a-zA-Z0-9._-]+$` — they are later inlined into ClickHouse SQL. `AgentLifecycleCondition` is allowlist-only — the `AgentLifecycleEventType` enum (REGISTERED / RE_REGISTERED / DEREGISTERED / WENT_STALE / WENT_DEAD / RECOVERED) plus the record compact ctor (non-empty `eventTypes`, `withinSeconds ≥ 1`) do the validation; custom agent-emitted event types are tracked in backlog issue #145. Webhook validation: verifies `outboundConnectionId` exists and `isAllowedInEnvironment`. Null notification templates default to `""` (NOT NULL constraint). Audit: `ALERT_RULE_CHANGE`. +- `AlertController` — `/api/v1/environments/{envSlug}/alerts`. GET list (inbox filtered by userId/groupIds/roleNames via `InAppInboxQuery`; optional multi-value `state` + `severity` query params push filtering into PostgreSQL via `listForInbox` with `state::text = ANY(?)` / `severity::text = ANY(?)`) / GET `/unread-count` / GET `{id}` / POST `{id}/ack` / POST `{id}/read` / POST `/bulk-read`. VIEWER+ for all. Inbox SQL: `? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?` — requires at least one matching target (no broadcast concept). - `AlertSilenceController` — `/api/v1/environments/{envSlug}/alerts/silences`. GET list / POST create / DELETE `{id}`. 422 if `endsAt <= startsAt`. OPERATOR+ for mutations, VIEWER+ for list. Audit: `ALERT_SILENCE_CHANGE`. - `AlertNotificationController` — Dual-path (no class-level prefix). GET `/api/v1/environments/{envSlug}/alerts/{alertId}/notifications` (VIEWER+); POST `/api/v1/alerts/notifications/{id}/retry` (OPERATOR+, flat — notification IDs globally unique). Retry resets attempts to 0 and sets `nextAttemptAt = now`. diff --git a/.claude/rules/core-classes.md b/.claude/rules/core-classes.md index c8dbf304..193e6832 100644 --- a/.claude/rules/core-classes.md +++ b/.claude/rules/core-classes.md @@ -17,7 +17,7 @@ paths: - `CommandType` — enum for command types (config-update, deep-trace, replay, route-control, etc.) - `CommandStatus` — enum for command acknowledgement states - `CommandReply` — record: command execution result from agent -- `AgentEventRecord`, `AgentEventRepository` — event persistence. `AgentEventRepository.queryPage(...)` is cursor-paginated (`AgentEventPage{data, nextCursor, hasMore}`); the legacy non-paginated `query(...)` path is gone. +- `AgentEventRecord`, `AgentEventRepository` — event persistence. `AgentEventRepository.queryPage(...)` is cursor-paginated (`AgentEventPage{data, nextCursor, hasMore}`); the legacy non-paginated `query(...)` path is gone. `AgentEventRepository.findInWindow(env, appSlug, agentId, eventTypes, from, to, limit)` returns matching events ordered by `(timestamp ASC, insert_id ASC)` — consumed by `AgentLifecycleEvaluator`. - `AgentEventPage` — record: `(List data, String nextCursor, boolean hasMore)` returned by `AgentEventRepository.queryPage` - `AgentEventListener` — callback interface for agent events - `RouteStateRegistry` — tracks per-agent route states diff --git a/.claude/rules/ui.md b/.claude/rules/ui.md index f673d9ac..16044b03 100644 --- a/.claude/rules/ui.md +++ b/.claude/rules/ui.md @@ -43,7 +43,7 @@ The UI has 4 main tabs: **Exchanges**, **Dashboard**, **Runtime**, **Deployments - `AllAlertsPage.tsx` — env-wide list with state-chip filter. - `HistoryPage.tsx` — RESOLVED alerts. - `RulesListPage.tsx` — CRUD + enable/disable toggle + env-promotion dropdown (pure UI prefill, no new endpoint). - - `RuleEditor/RuleEditorWizard.tsx` — 5-step wizard (Scope / Condition / Trigger / Notify / Review). `form-state.ts` is the single source of truth (`initialForm` / `toRequest` / `validateStep`). Six condition-form subcomponents under `RuleEditor/condition-forms/`. + - `RuleEditor/RuleEditorWizard.tsx` — 5-step wizard (Scope / Condition / Trigger / Notify / Review). `form-state.ts` is the single source of truth (`initialForm` / `toRequest` / `validateStep`). Seven condition-form subcomponents under `RuleEditor/condition-forms/` — including `AgentLifecycleForm.tsx` (multi-select event-type chips for the six-entry `AgentLifecycleEventType` allowlist + lookback-window input). - `SilencesPage.tsx` — matcher-based create + end-early. - `AlertRow.tsx` shared list row; `alerts-page.module.css` shared styling. - **Components**: diff --git a/CLAUDE.md b/CLAUDE.md index be7b1d8b..d1f05a8c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,6 +71,8 @@ PostgreSQL (Flyway): `cameleer-server-app/src/main/resources/db/migration/` - V12 — Alerting tables (alert_rules, alert_rule_targets, alert_instances, alert_notifications, alert_reads, alert_silences) - V13 — alert_instances open-rule unique index (alert_instances_open_rule_uq partial index on rule_id WHERE state IN PENDING/FIRING/ACKNOWLEDGED) - V14 — Repair EXCHANGE_MATCH alert_rules persisted with fireMode=null (sets fireMode=PER_EXCHANGE + perExchangeLingerSeconds=300); paired with stricter `ExchangeMatchCondition` ctor that now rejects null fireMode. +- V15 — Discriminate open-instance uniqueness by `context->'exchange'->>'id'` so EXCHANGE_MATCH/PER_EXCHANGE emits one alert_instance per matching exchange; scalar kinds resolve to `''` and keep one-open-per-rule. +- V16 — Generalise the V15 discriminator to prefer `context->>'_subjectFingerprint'` (falls back to the V15 `exchange.id` expression for legacy rows). Enables AGENT_LIFECYCLE to emit one alert_instance per `(agent, eventType, timestamp)` via a canonical fingerprint in the evaluator firing's context. ClickHouse: `cameleer-server-app/src/main/resources/clickhouse/init.sql` (run idempotently on startup) @@ -98,7 +100,7 @@ When adding, removing, or renaming classes, controllers, endpoints, UI component # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-server** (8527 symbols, 22174 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. +This project is indexed by GitNexus as **cameleer-server** (8603 symbols, 22281 relationships, 300 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. > If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluator.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluator.java new file mode 100644 index 00000000..9eb15d6e --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluator.java @@ -0,0 +1,95 @@ +package com.cameleer.server.app.alerting.eval; + +import com.cameleer.server.core.agent.AgentEventRecord; +import com.cameleer.server.core.agent.AgentEventRepository; +import com.cameleer.server.core.alerting.AgentLifecycleCondition; +import com.cameleer.server.core.alerting.AgentLifecycleEventType; +import com.cameleer.server.core.alerting.AlertRule; +import com.cameleer.server.core.alerting.AlertScope; +import com.cameleer.server.core.alerting.ConditionKind; +import com.cameleer.server.core.runtime.EnvironmentRepository; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * Evaluator for {@link AgentLifecycleCondition}. + *

+ * Each matching row in {@code agent_events} produces its own {@link EvalResult.Firing} + * in an {@link EvalResult.Batch}, so every {@code (agent, eventType, timestamp)} + * tuple gets its own {@code AlertInstance} — operationally distinct outages / + * restarts / shutdowns are independently ackable. Deduplication across ticks + * is enforced by {@code alert_instances_open_rule_uq} via the canonical + * {@code _subjectFingerprint} key in the instance context (see V16 migration). + */ +@Component +public class AgentLifecycleEvaluator implements ConditionEvaluator { + + /** Hard cap on rows returned per tick — prevents a flood of stale events from overwhelming the job. */ + private static final int MAX_EVENTS_PER_TICK = 500; + + private final AgentEventRepository eventRepo; + private final EnvironmentRepository envRepo; + + public AgentLifecycleEvaluator(AgentEventRepository eventRepo, EnvironmentRepository envRepo) { + this.eventRepo = eventRepo; + this.envRepo = envRepo; + } + + @Override + public ConditionKind kind() { return ConditionKind.AGENT_LIFECYCLE; } + + @Override + public EvalResult evaluate(AgentLifecycleCondition c, AlertRule rule, EvalContext ctx) { + String envSlug = envRepo.findById(rule.environmentId()) + .map(e -> e.slug()) + .orElse(null); + if (envSlug == null) return EvalResult.Clear.INSTANCE; + + AlertScope scope = c.scope(); + String appSlug = scope != null ? scope.appSlug() : null; + String agentId = scope != null ? scope.agentId() : null; + + List typeNames = c.eventTypes().stream() + .map(AgentLifecycleEventType::name) + .toList(); + + Instant from = ctx.now().minusSeconds(c.withinSeconds()); + Instant to = ctx.now(); + + List matches = eventRepo.findInWindow( + envSlug, appSlug, agentId, typeNames, from, to, MAX_EVENTS_PER_TICK); + + if (matches.isEmpty()) return new EvalResult.Batch(List.of()); + + List firings = new ArrayList<>(matches.size()); + for (AgentEventRecord ev : matches) { + firings.add(toFiring(ev)); + } + return new EvalResult.Batch(firings); + } + + private static EvalResult.Firing toFiring(AgentEventRecord ev) { + String fingerprint = (ev.instanceId() == null ? "" : ev.instanceId()) + + ":" + (ev.eventType() == null ? "" : ev.eventType()) + + ":" + (ev.timestamp() == null ? "0" : Long.toString(ev.timestamp().toEpochMilli())); + + Map context = new LinkedHashMap<>(); + context.put("agent", Map.of( + "id", ev.instanceId() == null ? "" : ev.instanceId(), + "app", ev.applicationId() == null ? "" : ev.applicationId() + )); + context.put("event", Map.of( + "type", ev.eventType() == null ? "" : ev.eventType(), + "timestamp", ev.timestamp() == null ? "" : ev.timestamp().toString(), + "detail", ev.detail() == null ? "" : ev.detail() + )); + context.put("_subjectFingerprint", fingerprint); + + return new EvalResult.Firing(1.0, null, context); + } +} diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilder.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilder.java index 41f0ce31..50e4db43 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilder.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilder.java @@ -64,6 +64,10 @@ public class NotificationContextBuilder { ctx.put("agent", subtree(instance, "agent.id", "agent.name", "agent.state")); ctx.put("app", subtree(instance, "app.slug", "app.id")); } + case AGENT_LIFECYCLE -> { + ctx.put("agent", subtree(instance, "agent.id", "agent.app")); + ctx.put("event", subtree(instance, "event.type", "event.timestamp", "event.detail")); + } case DEPLOYMENT_STATE -> { ctx.put("deployment", subtree(instance, "deployment.id", "deployment.status")); ctx.put("app", subtree(instance, "app.slug", "app.id")); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java index 817cf2a1..fe4c25c6 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/storage/ClickHouseAgentEventRepository.java @@ -106,4 +106,57 @@ public class ClickHouseAgentEventRepository implements AgentEventRepository { return new AgentEventPage(results, nextCursor, hasMore); } + + @Override + public List findInWindow(String environment, + String applicationId, + String instanceId, + List eventTypes, + Instant fromInclusive, + Instant toExclusive, + int limit) { + if (eventTypes == null || eventTypes.isEmpty()) { + throw new IllegalArgumentException("eventTypes must not be empty"); + } + if (fromInclusive == null || toExclusive == null) { + throw new IllegalArgumentException("from/to must not be null"); + } + + // `event_type IN (?, ?, …)` — one placeholder per type. + String placeholders = String.join(",", java.util.Collections.nCopies(eventTypes.size(), "?")); + var sql = new StringBuilder(SELECT_BASE); + var params = new ArrayList(); + params.add(tenantId); + + if (environment != null) { + sql.append(" AND environment = ?"); + params.add(environment); + } + if (applicationId != null) { + sql.append(" AND application_id = ?"); + params.add(applicationId); + } + if (instanceId != null) { + sql.append(" AND instance_id = ?"); + params.add(instanceId); + } + sql.append(" AND event_type IN (").append(placeholders).append(")"); + params.addAll(eventTypes); + sql.append(" AND timestamp >= ? AND timestamp < ?"); + params.add(Timestamp.from(fromInclusive)); + params.add(Timestamp.from(toExclusive)); + sql.append(" ORDER BY timestamp ASC, insert_id ASC LIMIT ?"); + params.add(limit); + + return jdbc.query(sql.toString(), + (rs, rowNum) -> new AgentEventRecord( + rs.getLong("id"), + rs.getString("instance_id"), + rs.getString("application_id"), + rs.getString("event_type"), + rs.getString("detail"), + rs.getTimestamp("timestamp").toInstant() + ), + params.toArray()); + } } diff --git a/cameleer-server-app/src/main/resources/db/migration/V16__alert_instances_subject_fingerprint.sql b/cameleer-server-app/src/main/resources/db/migration/V16__alert_instances_subject_fingerprint.sql new file mode 100644 index 00000000..c9e27777 --- /dev/null +++ b/cameleer-server-app/src/main/resources/db/migration/V16__alert_instances_subject_fingerprint.sql @@ -0,0 +1,27 @@ +-- V16 — Generalise open-alert_instance uniqueness via `_subjectFingerprint`. +-- +-- V15 discriminated open instances by `context->'exchange'->>'id'` so that +-- EXCHANGE_MATCH / PER_EXCHANGE could emit one instance per exchange. The new +-- AGENT_LIFECYCLE / PER_AGENT condition has the same shape but a different +-- subject key (agentId + eventType + eventTs). Rather than bolt condition-kind +-- knowledge into the index, we introduce a canonical `_subjectFingerprint` +-- field in `context` that every "per-subject" evaluator writes. The index +-- prefers it over the legacy exchange.id discriminator. +-- +-- Precedence in the COALESCE: +-- 1. context->>'_subjectFingerprint' — explicit per-subject key (new) +-- 2. context->'exchange'->>'id' — legacy EXCHANGE_MATCH instances (pre-V16) +-- 3. '' — scalar condition kinds (one open per rule) +-- +-- Existing open PER_EXCHANGE instances keep working because they never set +-- `_subjectFingerprint` but do carry `context.exchange.id`, so the index +-- still discriminates them correctly. +DROP INDEX IF EXISTS alert_instances_open_rule_uq; + +CREATE UNIQUE INDEX alert_instances_open_rule_uq + ON alert_instances (rule_id, (COALESCE( + context->>'_subjectFingerprint', + context->'exchange'->>'id', + ''))) + WHERE rule_id IS NOT NULL + AND state IN ('PENDING','FIRING','ACKNOWLEDGED'); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluatorTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluatorTest.java new file mode 100644 index 00000000..a043c0e5 --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AgentLifecycleEvaluatorTest.java @@ -0,0 +1,130 @@ +package com.cameleer.server.app.alerting.eval; + +import com.cameleer.server.core.agent.AgentEventRecord; +import com.cameleer.server.core.agent.AgentEventRepository; +import com.cameleer.server.core.alerting.*; +import com.cameleer.server.core.runtime.Environment; +import com.cameleer.server.core.runtime.EnvironmentRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class AgentLifecycleEvaluatorTest { + + private AgentEventRepository events; + private EnvironmentRepository envRepo; + private AgentLifecycleEvaluator eval; + + private static final UUID ENV_ID = UUID.fromString("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"); + private static final UUID RULE_ID = UUID.fromString("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"); + private static final String ENV_SLUG = "prod"; + private static final Instant NOW = Instant.parse("2026-04-19T10:00:00Z"); + + @BeforeEach + void setUp() { + events = mock(AgentEventRepository.class); + envRepo = mock(EnvironmentRepository.class); + when(envRepo.findById(ENV_ID)).thenReturn(Optional.of( + new Environment(ENV_ID, ENV_SLUG, "Prod", true, true, Map.of(), 5, Instant.EPOCH))); + eval = new AgentLifecycleEvaluator(events, envRepo); + } + + private AlertRule ruleWith(AlertCondition condition) { + return new AlertRule(RULE_ID, ENV_ID, "lifecycle test", null, + AlertSeverity.CRITICAL, true, condition.kind(), condition, + 60, 0, 0, null, null, List.of(), List.of(), + null, null, null, Map.of(), null, null, null, null); + } + + private EvalContext ctx() { return new EvalContext("default", NOW, new TickCache()); } + + @Test + void kindIsAgentLifecycle() { + assertThat(eval.kind()).isEqualTo(ConditionKind.AGENT_LIFECYCLE); + } + + @Test + void emptyWindowYieldsEmptyBatch() { + var condition = new AgentLifecycleCondition( + new AlertScope(null, null, null), + List.of(AgentLifecycleEventType.WENT_DEAD), + 300); + when(events.findInWindow(eq(ENV_SLUG), any(), any(), any(), any(), any(), anyInt())) + .thenReturn(List.of()); + + EvalResult r = eval.evaluate(condition, ruleWith(condition), ctx()); + assertThat(r).isInstanceOf(EvalResult.Batch.class); + assertThat(((EvalResult.Batch) r).firings()).isEmpty(); + } + + @Test + void emitsOneFiringPerEventWithFingerprint() { + Instant ts1 = NOW.minusSeconds(30); + Instant ts2 = NOW.minusSeconds(10); + when(events.findInWindow(eq(ENV_SLUG), any(), any(), any(), any(), any(), anyInt())) + .thenReturn(List.of( + new AgentEventRecord(0, "agent-A", "orders", "WENT_DEAD", "A went dead", ts1), + new AgentEventRecord(0, "agent-B", "orders", "WENT_DEAD", "B went dead", ts2) + )); + + var condition = new AgentLifecycleCondition( + new AlertScope(null, null, null), + List.of(AgentLifecycleEventType.WENT_DEAD), 60); + + EvalResult r = eval.evaluate(condition, ruleWith(condition), ctx()); + var batch = (EvalResult.Batch) r; + assertThat(batch.firings()).hasSize(2); + + var f0 = batch.firings().get(0); + assertThat(f0.context()).containsKey("_subjectFingerprint"); + assertThat((String) f0.context().get("_subjectFingerprint")) + .isEqualTo("agent-A:WENT_DEAD:" + ts1.toEpochMilli()); + @SuppressWarnings("unchecked") + Map agent0 = (Map) f0.context().get("agent"); + assertThat(agent0).containsEntry("id", "agent-A").containsEntry("app", "orders"); + @SuppressWarnings("unchecked") + Map event0 = (Map) f0.context().get("event"); + assertThat(event0).containsEntry("type", "WENT_DEAD"); + + var f1 = batch.firings().get(1); + assertThat((String) f1.context().get("_subjectFingerprint")) + .isEqualTo("agent-B:WENT_DEAD:" + ts2.toEpochMilli()); + } + + @Test + void forwardsScopeFiltersToRepo() { + when(events.findInWindow(eq(ENV_SLUG), eq("orders"), eq("agent-A"), any(), any(), any(), anyInt())) + .thenReturn(List.of()); + var condition = new AgentLifecycleCondition( + new AlertScope("orders", null, "agent-A"), + List.of(AgentLifecycleEventType.REGISTERED), 120); + eval.evaluate(condition, ruleWith(condition), ctx()); + // Mockito `when` matches — verifying no mismatch is enough; stub returns [] + } + + @Test + void clearsWhenEnvIsMissing() { + // envRepo returns empty → should Clear, not throw. + EnvironmentRepository emptyEnvRepo = mock(EnvironmentRepository.class); + when(emptyEnvRepo.findById(ENV_ID)).thenReturn(Optional.empty()); + AgentLifecycleEvaluator localEval = new AgentLifecycleEvaluator(events, emptyEnvRepo); + + var condition = new AgentLifecycleCondition( + new AlertScope(null, null, null), + List.of(AgentLifecycleEventType.WENT_DEAD), 60); + EvalResult r = localEval.evaluate(condition, ruleWith(condition), ctx()); + assertThat(r).isEqualTo(EvalResult.Clear.INSTANCE); + } +} diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java index a046922c..9d33073e 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java @@ -43,6 +43,10 @@ class NotificationContextBuilderTest { case AGENT_STATE -> new AgentStateCondition( new AlertScope(null, null, null), "DEAD", 0); + case AGENT_LIFECYCLE -> new AgentLifecycleCondition( + new AlertScope(null, null, null), + List.of(AgentLifecycleEventType.WENT_DEAD), + 60); case DEPLOYMENT_STATE -> new DeploymentStateCondition( new AlertScope("my-app", null, null), List.of("FAILED")); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentEventRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentEventRepository.java index 63c09b42..aa710577 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentEventRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentEventRepository.java @@ -1,6 +1,7 @@ package com.cameleer.server.core.agent; import java.time.Instant; +import java.util.List; public interface AgentEventRepository { @@ -13,4 +14,19 @@ public interface AgentEventRepository { */ AgentEventPage queryPage(String applicationId, String instanceId, String environment, Instant from, Instant to, String cursor, int limit); + + /** + * Inclusive-exclusive window query ordered by (timestamp ASC, instance_id ASC) + * used by the AGENT_LIFECYCLE alert evaluator. {@code eventTypes} is required + * and must be non-empty; the implementation filters via {@code event_type IN (...)}. + * Scope filters ({@code applicationId}, {@code instanceId}) are optional. The + * returned list is capped at {@code limit} rows. + */ + List findInWindow(String environment, + String applicationId, + String instanceId, + List eventTypes, + Instant fromInclusive, + Instant toExclusive, + int limit); } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleCondition.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleCondition.java new file mode 100644 index 00000000..fa28805a --- /dev/null +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleCondition.java @@ -0,0 +1,34 @@ +package com.cameleer.server.core.alerting; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Fires one {@code AlertInstance} per matching {@code agent_events} row in the + * lookback window. Per-subject fire mode (see + * {@link AgentLifecycleEventType}) — each {@code (agent, eventType, timestamp)} + * tuple is independently ackable, driven by a canonical + * {@code _subjectFingerprint} in the instance context and the partial unique + * index on {@code alert_instances}. + */ +public record AgentLifecycleCondition( + AlertScope scope, + List eventTypes, + int withinSeconds +) implements AlertCondition { + + public AgentLifecycleCondition { + if (eventTypes == null || eventTypes.isEmpty()) { + throw new IllegalArgumentException("eventTypes must not be empty"); + } + if (withinSeconds < 1) { + throw new IllegalArgumentException("withinSeconds must be >= 1"); + } + eventTypes = List.copyOf(eventTypes); + } + + @Override + @JsonProperty(value = "kind", access = JsonProperty.Access.READ_ONLY) + public ConditionKind kind() { return ConditionKind.AGENT_LIFECYCLE; } +} diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleEventType.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleEventType.java new file mode 100644 index 00000000..857c6ff8 --- /dev/null +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AgentLifecycleEventType.java @@ -0,0 +1,20 @@ +package com.cameleer.server.core.alerting; + +/** + * Allowlist of agent-lifecycle event types that may appear in an + * {@link AgentLifecycleCondition}. The set matches exactly the events the + * server writes to {@code agent_events} — registration-controller emits + * REGISTERED / RE_REGISTERED / DEREGISTERED, the lifecycle monitor emits + * WENT_STALE / WENT_DEAD / RECOVERED. + *

+ * Custom agent-emitted event types (via {@code POST /api/v1/data/events}) + * are intentionally excluded — see backlog issue #145. + */ +public enum AgentLifecycleEventType { + REGISTERED, + RE_REGISTERED, + DEREGISTERED, + WENT_STALE, + WENT_DEAD, + RECOVERED +} diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertCondition.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertCondition.java index 008fd78a..e562dac7 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertCondition.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertCondition.java @@ -9,13 +9,15 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo; @JsonSubTypes.Type(value = RouteMetricCondition.class, name = "ROUTE_METRIC"), @JsonSubTypes.Type(value = ExchangeMatchCondition.class, name = "EXCHANGE_MATCH"), @JsonSubTypes.Type(value = AgentStateCondition.class, name = "AGENT_STATE"), + @JsonSubTypes.Type(value = AgentLifecycleCondition.class, name = "AGENT_LIFECYCLE"), @JsonSubTypes.Type(value = DeploymentStateCondition.class, name = "DEPLOYMENT_STATE"), @JsonSubTypes.Type(value = LogPatternCondition.class, name = "LOG_PATTERN"), @JsonSubTypes.Type(value = JvmMetricCondition.class, name = "JVM_METRIC") }) public sealed interface AlertCondition permits RouteMetricCondition, ExchangeMatchCondition, AgentStateCondition, - DeploymentStateCondition, LogPatternCondition, JvmMetricCondition { + AgentLifecycleCondition, DeploymentStateCondition, LogPatternCondition, + JvmMetricCondition { @JsonProperty("kind") ConditionKind kind(); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/ConditionKind.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/ConditionKind.java index b53585ce..17611a3a 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/ConditionKind.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/ConditionKind.java @@ -1,3 +1,11 @@ package com.cameleer.server.core.alerting; -public enum ConditionKind { ROUTE_METRIC, EXCHANGE_MATCH, AGENT_STATE, DEPLOYMENT_STATE, LOG_PATTERN, JVM_METRIC } +public enum ConditionKind { + ROUTE_METRIC, + EXCHANGE_MATCH, + AGENT_STATE, + AGENT_LIFECYCLE, + DEPLOYMENT_STATE, + LOG_PATTERN, + JVM_METRIC +} diff --git a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertConditionJsonTest.java b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertConditionJsonTest.java index 8ecf85f0..950720a0 100644 --- a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertConditionJsonTest.java +++ b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertConditionJsonTest.java @@ -101,4 +101,50 @@ class AlertConditionJsonTest { AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class); assertThat(parsed).isInstanceOf(JvmMetricCondition.class); } + + @Test + void roundtripAgentLifecycle() throws Exception { + var c = new AgentLifecycleCondition( + new AlertScope("orders", null, null), + List.of(AgentLifecycleEventType.WENT_DEAD, AgentLifecycleEventType.DEREGISTERED), + 300); + AlertCondition parsed = om.readValue(om.writeValueAsString((AlertCondition) c), AlertCondition.class); + assertThat(parsed).isInstanceOf(AgentLifecycleCondition.class); + var alc = (AgentLifecycleCondition) parsed; + assertThat(alc.eventTypes()).containsExactly( + AgentLifecycleEventType.WENT_DEAD, AgentLifecycleEventType.DEREGISTERED); + assertThat(alc.withinSeconds()).isEqualTo(300); + assertThat(alc.kind()).isEqualTo(ConditionKind.AGENT_LIFECYCLE); + } + + @Test + void agentLifecycleRejectsEmptyEventTypes() { + assertThatThrownBy(() -> new AgentLifecycleCondition( + new AlertScope(null, null, null), List.of(), 60)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("eventTypes"); + } + + @Test + void agentLifecycleRejectsZeroWindow() { + assertThatThrownBy(() -> new AgentLifecycleCondition( + new AlertScope(null, null, null), + List.of(AgentLifecycleEventType.WENT_DEAD), 0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("withinSeconds"); + } + + @Test + void agentLifecycleRejectsUnknownEventTypeOnDeserialization() { + String json = """ + { + "kind": "AGENT_LIFECYCLE", + "scope": {}, + "eventTypes": ["REGISTERED", "BOGUS_EVENT"], + "withinSeconds": 60 + } + """; + assertThatThrownBy(() -> om.readValue(json, AlertCondition.class)) + .hasMessageContaining("BOGUS_EVENT"); + } } diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index e4999b15..d28f7b11 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReadRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"state","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]}}},{"name":"severity","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["CRITICAL","WARNING","INFO"]}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkReadRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReadRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"state","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]}}},{"name":"severity","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["CRITICAL","WARNING","INFO"]}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentLifecycleCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"eventTypes":{"type":"array","items":{"type":"string","enum":["REGISTERED","RE_REGISTERED","DEREGISTERED","WENT_STALE","WENT_DEAD","RECOVERED"]}},"withinSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkReadRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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 diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index 55270541..d49d32f8 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -2221,6 +2221,16 @@ export interface components { /** Format: date-time */ createdAt?: string; }; + AgentLifecycleCondition: { + kind: "AgentLifecycleCondition"; + } & (Omit & { + scope?: components["schemas"]["AlertScope"]; + eventTypes?: ("REGISTERED" | "RE_REGISTERED" | "DEREGISTERED" | "WENT_STALE" | "WENT_DEAD" | "RECOVERED")[]; + /** Format: int32 */ + withinSeconds?: number; + /** @enum {string} */ + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + }); AgentStateCondition: { kind: "AgentStateCondition"; } & (Omit & { @@ -2229,11 +2239,11 @@ export interface components { /** Format: int32 */ forSeconds?: number; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); AlertCondition: { /** @enum {string} */ - kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }; AlertRuleRequest: { name?: string; @@ -2241,8 +2251,8 @@ export interface components { /** @enum {string} */ severity: "CRITICAL" | "WARNING" | "INFO"; /** @enum {string} */ - conditionKind: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; - condition: components["schemas"]["AgentStateCondition"] | components["schemas"]["DeploymentStateCondition"] | components["schemas"]["ExchangeMatchCondition"] | components["schemas"]["JvmMetricCondition"] | components["schemas"]["LogPatternCondition"] | components["schemas"]["RouteMetricCondition"]; + conditionKind: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + condition: components["schemas"]["AgentLifecycleCondition"] | components["schemas"]["AgentStateCondition"] | components["schemas"]["DeploymentStateCondition"] | components["schemas"]["ExchangeMatchCondition"] | components["schemas"]["JvmMetricCondition"] | components["schemas"]["LogPatternCondition"] | components["schemas"]["RouteMetricCondition"]; /** Format: int32 */ evaluationIntervalSeconds?: number; /** Format: int32 */ @@ -2274,7 +2284,7 @@ export interface components { scope?: components["schemas"]["AlertScope"]; states?: string[]; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); ExchangeFilter: { status?: string; @@ -2296,7 +2306,7 @@ export interface components { /** Format: int32 */ perExchangeLingerSeconds?: number; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); JvmMetricCondition: { kind: "JvmMetricCondition"; @@ -2312,7 +2322,7 @@ export interface components { /** Format: int32 */ windowSeconds?: number; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); LogPatternCondition: { kind: "LogPatternCondition"; @@ -2325,7 +2335,7 @@ export interface components { /** Format: int32 */ windowSeconds?: number; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); RouteMetricCondition: { kind: "RouteMetricCondition"; @@ -2340,7 +2350,7 @@ export interface components { /** Format: int32 */ windowSeconds?: number; /** @enum {string} */ - readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + readonly kind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; }); WebhookBindingRequest: { /** Format: uuid */ @@ -2361,8 +2371,8 @@ export interface components { severity?: "CRITICAL" | "WARNING" | "INFO"; enabled?: boolean; /** @enum {string} */ - conditionKind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; - condition?: components["schemas"]["AgentStateCondition"] | components["schemas"]["DeploymentStateCondition"] | components["schemas"]["ExchangeMatchCondition"] | components["schemas"]["JvmMetricCondition"] | components["schemas"]["LogPatternCondition"] | components["schemas"]["RouteMetricCondition"]; + conditionKind?: "ROUTE_METRIC" | "EXCHANGE_MATCH" | "AGENT_STATE" | "AGENT_LIFECYCLE" | "DEPLOYMENT_STATE" | "LOG_PATTERN" | "JVM_METRIC"; + condition?: components["schemas"]["AgentLifecycleCondition"] | components["schemas"]["AgentStateCondition"] | components["schemas"]["DeploymentStateCondition"] | components["schemas"]["ExchangeMatchCondition"] | components["schemas"]["JvmMetricCondition"] | components["schemas"]["LogPatternCondition"] | components["schemas"]["RouteMetricCondition"]; /** Format: int32 */ evaluationIntervalSeconds?: number; /** Format: int32 */ diff --git a/ui/src/components/MustacheEditor/alert-variables.ts b/ui/src/components/MustacheEditor/alert-variables.ts index 5886495b..634656de 100644 --- a/ui/src/components/MustacheEditor/alert-variables.ts +++ b/ui/src/components/MustacheEditor/alert-variables.ts @@ -42,6 +42,16 @@ export const ALERT_VARIABLES: AlertVariable[] = [ { path: 'app.id', type: 'uuid', description: 'App UUID', sampleValue: '33333333-...', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH', 'AGENT_STATE', 'DEPLOYMENT_STATE', 'LOG_PATTERN', 'JVM_METRIC'], mayBeNull: true }, + // AGENT_LIFECYCLE — agent + event subtree (distinct from AGENT_STATE's agent.* leaves) + { path: 'agent.app', type: 'string', description: 'Agent app slug', sampleValue: 'orders', + availableForKinds: ['AGENT_LIFECYCLE'] }, + { path: 'event.type', type: 'string', description: 'Lifecycle event type', sampleValue: 'WENT_DEAD', + availableForKinds: ['AGENT_LIFECYCLE'] }, + { path: 'event.timestamp', type: 'Instant', description: 'When the event happened', sampleValue: '2026-04-20T14:33:10Z', + availableForKinds: ['AGENT_LIFECYCLE'] }, + { path: 'event.detail', type: 'string', description: 'Free-text event detail', sampleValue: 'orders-0 STALE -> DEAD', + availableForKinds: ['AGENT_LIFECYCLE'], mayBeNull: true }, + // ROUTE_METRIC + EXCHANGE_MATCH share route.* { path: 'route.id', type: 'string', description: 'Route ID', sampleValue: 'route-1', availableForKinds: ['ROUTE_METRIC', 'EXCHANGE_MATCH'] }, @@ -56,7 +66,7 @@ export const ALERT_VARIABLES: AlertVariable[] = [ // AGENT_STATE + JVM_METRIC share agent.id/name; AGENT_STATE adds agent.state { path: 'agent.id', type: 'string', description: 'Agent instance ID', sampleValue: 'prod-orders-0', - availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] }, + availableForKinds: ['AGENT_STATE', 'AGENT_LIFECYCLE', 'JVM_METRIC'] }, { path: 'agent.name', type: 'string', description: 'Agent display name', sampleValue: 'orders-0', availableForKinds: ['AGENT_STATE', 'JVM_METRIC'] }, { path: 'agent.state', type: 'string', description: 'Agent state', sampleValue: 'DEAD', diff --git a/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx b/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx index 2d5c21f6..12f17881 100644 --- a/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx +++ b/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx @@ -3,6 +3,7 @@ import type { FormState } from './form-state'; import { RouteMetricForm } from './condition-forms/RouteMetricForm'; import { ExchangeMatchForm } from './condition-forms/ExchangeMatchForm'; import { AgentStateForm } from './condition-forms/AgentStateForm'; +import { AgentLifecycleForm } from './condition-forms/AgentLifecycleForm'; import { DeploymentStateForm } from './condition-forms/DeploymentStateForm'; import { LogPatternForm } from './condition-forms/LogPatternForm'; import { JvmMetricForm } from './condition-forms/JvmMetricForm'; @@ -23,6 +24,13 @@ export function ConditionStep({ form, setForm }: { form: FormState; setForm: (f: base.perExchangeLingerSeconds = 300; base.filter = {}; } + if (kind === 'AGENT_LIFECYCLE') { + // Sensible defaults so a rule can be saved without touching every sub-field. + // WENT_DEAD is the most "alert-worthy" event out of the six; a 5-minute + // window matches the registry's STALE→DEAD cadence + slack for tick jitter. + base.eventTypes = ['WENT_DEAD']; + base.withinSeconds = 300; + } setForm({ ...form, conditionKind: kind, @@ -42,6 +50,7 @@ export function ConditionStep({ form, setForm }: { form: FormState; setForm: (f: {form.conditionKind === 'ROUTE_METRIC' && } {form.conditionKind === 'EXCHANGE_MATCH' && } {form.conditionKind === 'AGENT_STATE' && } + {form.conditionKind === 'AGENT_LIFECYCLE' && } {form.conditionKind === 'DEPLOYMENT_STATE' && } {form.conditionKind === 'LOG_PATTERN' && } {form.conditionKind === 'JVM_METRIC' && } diff --git a/ui/src/pages/Alerts/RuleEditor/condition-forms/AgentLifecycleForm.tsx b/ui/src/pages/Alerts/RuleEditor/condition-forms/AgentLifecycleForm.tsx new file mode 100644 index 00000000..6074575e --- /dev/null +++ b/ui/src/pages/Alerts/RuleEditor/condition-forms/AgentLifecycleForm.tsx @@ -0,0 +1,72 @@ +import { FormField, Input } from '@cameleer/design-system'; +import type { FormState } from '../form-state'; +import { + AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS, + type AgentLifecycleEventType, +} from '../../enums'; + +/** + * Form for `AGENT_LIFECYCLE` conditions. Users pick one or more event types + * (allowlist only) and a lookback window in seconds. The evaluator queries + * `agent_events` with those filters; each matching row produces its own + * {@code AlertInstance}. + */ +export function AgentLifecycleForm({ form, setForm }: { form: FormState; setForm: (f: FormState) => void }) { + const c = form.condition as Record; + const selected = new Set( + Array.isArray(c.eventTypes) ? (c.eventTypes as AgentLifecycleEventType[]) : [], + ); + + const patch = (p: Record) => + setForm({ + ...form, + condition: { ...(form.condition as Record), ...p } as FormState['condition'], + }); + + const toggle = (t: AgentLifecycleEventType) => { + const next = new Set(selected); + if (next.has(t)) next.delete(t); else next.add(t); + patch({ eventTypes: [...next] }); + }; + + return ( + <> + +

+ {AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS.map((opt) => { + const active = selected.has(opt.value); + return ( + + ); + })} +
+ + + patch({ withinSeconds: Number(e.target.value) })} + /> + + + ); +} diff --git a/ui/src/pages/Alerts/RuleEditor/form-state.ts b/ui/src/pages/Alerts/RuleEditor/form-state.ts index 120c70af..18f25cb5 100644 --- a/ui/src/pages/Alerts/RuleEditor/form-state.ts +++ b/ui/src/pages/Alerts/RuleEditor/form-state.ts @@ -160,6 +160,13 @@ export function validateStep(step: WizardStep, f: FormState): string[] { if (c.windowSeconds == null) errs.push('Window (seconds) is required for COUNT_IN_WINDOW.'); } } + if (f.conditionKind === 'AGENT_LIFECYCLE') { + const c = f.condition as Record; + const types = Array.isArray(c.eventTypes) ? (c.eventTypes as string[]) : []; + if (types.length === 0) errs.push('Pick at least one event type.'); + const within = c.withinSeconds as number | undefined; + if (within == null || within < 1) errs.push('Lookback window must be \u2265 1 second.'); + } } if (step === 'trigger') { if (f.evaluationIntervalSeconds < 5) errs.push('Evaluation interval must be \u2265 5 s.'); diff --git a/ui/src/pages/Alerts/enums.test.ts b/ui/src/pages/Alerts/enums.test.ts index 0e5f2584..7b12c906 100644 --- a/ui/src/pages/Alerts/enums.test.ts +++ b/ui/src/pages/Alerts/enums.test.ts @@ -7,6 +7,7 @@ import { JVM_AGGREGATION_OPTIONS, EXCHANGE_FIRE_MODE_OPTIONS, TARGET_KIND_OPTIONS, + AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS, } from './enums'; /** @@ -25,12 +26,24 @@ describe('alerts/enums option arrays', () => { { value: 'ROUTE_METRIC', label: 'Route metric (error rate, latency, throughput)' }, { value: 'EXCHANGE_MATCH', label: 'Exchange match (specific failures)' }, { value: 'AGENT_STATE', label: 'Agent state (DEAD / STALE)' }, + { value: 'AGENT_LIFECYCLE', label: 'Agent lifecycle (register / restart / stale / dead)' }, { value: 'DEPLOYMENT_STATE', label: 'Deployment state (FAILED / DEGRADED)' }, { value: 'LOG_PATTERN', label: 'Log pattern (count of matching logs)' }, { value: 'JVM_METRIC', label: 'JVM metric (heap, GC, inflight)' }, ]); }); + it('AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS', () => { + expect(AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS).toEqual([ + { value: 'WENT_STALE', label: 'Went stale (heartbeat missed)' }, + { value: 'WENT_DEAD', label: 'Went dead (extended silence)' }, + { value: 'RECOVERED', label: 'Recovered (stale → live)' }, + { value: 'REGISTERED', label: 'Registered (first check-in)' }, + { value: 'RE_REGISTERED', label: 'Re-registered (app restart)' }, + { value: 'DEREGISTERED', label: 'Deregistered (graceful shutdown)' }, + ]); + }); + it('SEVERITY_OPTIONS', () => { expect(SEVERITY_OPTIONS).toEqual([ { value: 'CRITICAL', label: 'Critical' }, diff --git a/ui/src/pages/Alerts/enums.ts b/ui/src/pages/Alerts/enums.ts index 585141ae..16a7c759 100644 --- a/ui/src/pages/Alerts/enums.ts +++ b/ui/src/pages/Alerts/enums.ts @@ -44,6 +44,13 @@ export type RouteMetric = 'ERROR_RATE' | 'AVG_DURATION_MS' | 'P99_LATENCY_M export type Comparator = 'GT' | 'GTE' | 'LT' | 'LTE' | 'EQ'; export type JvmAggregation = 'MAX' | 'MIN' | 'AVG' | 'LATEST'; export type ExchangeFireMode = 'PER_EXCHANGE' | 'COUNT_IN_WINDOW'; +export type AgentLifecycleEventType = + | 'REGISTERED' + | 'RE_REGISTERED' + | 'DEREGISTERED' + | 'WENT_STALE' + | 'WENT_DEAD' + | 'RECOVERED'; export interface Option { value: T; label: string } @@ -73,6 +80,7 @@ const CONDITION_KIND_LABELS: Record = { ROUTE_METRIC: 'Route metric (error rate, latency, throughput)', EXCHANGE_MATCH: 'Exchange match (specific failures)', AGENT_STATE: 'Agent state (DEAD / STALE)', + AGENT_LIFECYCLE: 'Agent lifecycle (register / restart / stale / dead)', DEPLOYMENT_STATE: 'Deployment state (FAILED / DEGRADED)', LOG_PATTERN: 'Log pattern (count of matching logs)', JVM_METRIC: 'JVM metric (heap, GC, inflight)', @@ -114,6 +122,15 @@ const EXCHANGE_FIRE_MODE_LABELS: Record = { COUNT_IN_WINDOW: 'Threshold: N matches in window', }; +const AGENT_LIFECYCLE_EVENT_TYPE_LABELS: Record = { + WENT_STALE: 'Went stale (heartbeat missed)', + WENT_DEAD: 'Went dead (extended silence)', + RECOVERED: 'Recovered (stale → live)', + REGISTERED: 'Registered (first check-in)', + RE_REGISTERED: 'Re-registered (app restart)', + DEREGISTERED: 'Deregistered (graceful shutdown)', +}; + const TARGET_KIND_LABELS: Record = { USER: 'User', GROUP: 'Group', @@ -147,3 +164,5 @@ export const COMPARATOR_OPTIONS: Option[] = toOptions export const JVM_AGGREGATION_OPTIONS: Option[] = toOptions(JVM_AGGREGATION_LABELS, JVM_AGGREGATION_HIDDEN); export const EXCHANGE_FIRE_MODE_OPTIONS: Option[] = toOptions(EXCHANGE_FIRE_MODE_LABELS); export const TARGET_KIND_OPTIONS: Option[] = toOptions(TARGET_KIND_LABELS); +export const AGENT_LIFECYCLE_EVENT_TYPE_OPTIONS: Option[] = + toOptions(AGENT_LIFECYCLE_EVENT_TYPE_LABELS); From c0b8c9a1ad47817a62747580179841cf4247d4b6 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:45:04 +0200 Subject: [PATCH 24/49] =?UTF-8?q?docs(alerts):=20spec=20=E2=80=94=20inbox?= =?UTF-8?q?=20redesign=20(single=20filterable=20inbox)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Collapse /alerts/inbox, /alerts/all, /alerts/history into a single filterable inbox. Drop ACKNOWLEDGED from AlertState; add read_at and deleted_at as orthogonal timestamp flags. Retire per-user alert_reads tracking. Add Silence-rule and Delete row/bulk actions. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...2026-04-21-alerts-inbox-redesign-design.md | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-21-alerts-inbox-redesign-design.md diff --git a/docs/superpowers/specs/2026-04-21-alerts-inbox-redesign-design.md b/docs/superpowers/specs/2026-04-21-alerts-inbox-redesign-design.md new file mode 100644 index 00000000..9457e2fc --- /dev/null +++ b/docs/superpowers/specs/2026-04-21-alerts-inbox-redesign-design.md @@ -0,0 +1,202 @@ +# Alerts Inbox Redesign — Design + +**Status:** Approved for planning +**Date:** 2026-04-21 +**Author:** Hendrik + Claude + +## Goal + +Collapse the three alert list pages (`/alerts/inbox`, `/alerts/all`, `/alerts/history`) into a single filterable inbox. Retire per-user read tracking: `ack`, `read`, and `delete` become global timestamp flags on the alert instance. Add row/bulk actions for **Silence rule…** and **Delete** directly from the list. + +## Motivation + +Today the three pages all hit the same user-scoped `InAppInboxQuery.listInbox`, so "All" is misleading (it's not env-wide) and "History" is just "Inbox with status=RESOLVED". The user asked for a single clean inbox with richer filters and list-level actions, and explicitly granted simplification of the read/ack/delete tracking model: one action is visible to everyone, no per-user state. + +## Scope + +**In:** +- Data model: drop `ACKNOWLEDGED` from `AlertState`, add `read_at` + `deleted_at` columns, drop `alert_reads` table, rework V13 open-rule index predicate. +- Backend: `AlertController` gains `acked`/`read` filter params, new `DELETE /alerts/{id}`, new `POST /alerts/bulk-delete`, new `POST /alerts/bulk-ack`. `/read` + `/bulk-read` rewire to update `alert_instances.read_at`. +- Data migration (V17): existing `ACKNOWLEDGED` rows → `state='FIRING'` (ack_time preserved). +- UI: rebuild `InboxPage` filter bar, add Silence/Delete row + bulk actions. Delete `AllAlertsPage.tsx` + `HistoryPage.tsx`. Sidebar trims to Inbox · Rules · Silences. +- Tests + rules-file updates. + +**Out:** +- No redirects from `/alerts/all` or `/alerts/history` — clean break per project's no-backwards-compat policy. Stale URLs 404. +- No per-instance silence (different from rule-silence). Silence row action always silences the rule that produced the alert. +- No "mark unread". Read is a one-way flag. +- No per-user actor tracking for `read`/`deleted`. `acked_by` stays (already exists, useful in UI), but only because it's already wired. + +## Architecture + +### Data model (`alert_instances`) + +``` +state enum: PENDING · FIRING · RESOLVED (was: + ACKNOWLEDGED) +acked_at TIMESTAMPTZ NULL (existing, semantics unchanged) +acked_by TEXT NULL → users(user_id) (existing, retained for UI) +read_at TIMESTAMPTZ NULL (NEW, global) +deleted_at TIMESTAMPTZ NULL (NEW, soft delete) +``` + +**Orthogonality:** `state` describes the alert's lifecycle (is the underlying condition still met?). `acked_at` / `read_at` / `deleted_at` describe what humans have done to the notification. A FIRING alert can be acked (= "someone's on it") while remaining FIRING until the condition clears. + +**V13 open-rule unique index predicate** (preserved as the evaluator's dedup key) changes from: +```sql +WHERE state IN ('PENDING','FIRING','ACKNOWLEDGED') +``` +to: +```sql +WHERE state IN ('PENDING','FIRING') AND deleted_at IS NULL +``` +Ack no longer "closes" the open window — a rule that's still matching stays de-duped against the open instance whether acked or not. Deleting soft-deletes the row and opens a new slot so the rule can fire again fresh if the condition re-triggers. + +**`alert_reads` table:** dropped entirely. No FK references elsewhere. + +### Postgres enum removal + +Postgres doesn't support removing a value from an enum type. Migration path: + +```sql +-- V17 +-- 1. Coerce existing ACKNOWLEDGED rows → FIRING (ack_time already set) +UPDATE alert_instances SET state = 'FIRING' WHERE state = 'ACKNOWLEDGED'; + +-- 2. Swap to a new enum type without ACKNOWLEDGED +CREATE TYPE alert_state_enum_v2 AS ENUM ('PENDING','FIRING','RESOLVED'); +ALTER TABLE alert_instances + ALTER COLUMN state TYPE alert_state_enum_v2 + USING state::text::alert_state_enum_v2; +DROP TYPE alert_state_enum; +ALTER TYPE alert_state_enum_v2 RENAME TO alert_state_enum; + +-- 3. New columns +ALTER TABLE alert_instances + ADD COLUMN read_at timestamptz NULL, + ADD COLUMN deleted_at timestamptz NULL; +CREATE INDEX alert_instances_read_idx ON alert_instances (environment_id, read_at) WHERE read_at IS NULL AND deleted_at IS NULL; +CREATE INDEX alert_instances_deleted_idx ON alert_instances (deleted_at) WHERE deleted_at IS NOT NULL; + +-- 4. Rework V13/V15/V16 open-rule unique index with the new predicate +DROP INDEX IF EXISTS alert_instances_open_rule_uq; +CREATE UNIQUE INDEX alert_instances_open_rule_uq + ON alert_instances (rule_id, (COALESCE( + context->>'_subjectFingerprint', + context->'exchange'->>'id', + ''))) + WHERE rule_id IS NOT NULL + AND state IN ('PENDING','FIRING') + AND deleted_at IS NULL; + +-- 5. Drop alert_reads +DROP TABLE alert_reads; +``` + +### Backend — `AlertController` + +| Method | Path | Body/Query | RBAC | Effect | +|---|---|---|---|---| +| GET | `/alerts` | `state, severity, acked, read, limit` | VIEWER+ | Inbox list, always `deleted_at IS NULL`. `state` no longer accepts `ACKNOWLEDGED`. `acked` / `read` are tri-state: **omitted** = no filter; `=true` = only acked/read; `=false` = only unacked/unread. UI defaults to `acked=false&read=false` via the "Hide acked" + "Hide read" toggles. | +| GET | `/alerts/unread-count` | — | VIEWER+ | Counts `read_at IS NULL AND deleted_at IS NULL` + user-visibility predicate. | +| GET | `/alerts/{id}` | — | VIEWER+ | Detail. Returns 404 if `deleted_at IS NOT NULL`. | +| POST | `/alerts/{id}/ack` | — | VIEWER+ | Sets `acked_at=now, acked_by=user`. No state change. | +| POST | `/alerts/{id}/read` | — | VIEWER+ | Sets `read_at=now` if null. Idempotent. | +| POST | `/alerts/bulk-read` | `{ instanceIds: [...] }` | VIEWER+ | UPDATE `alert_instances SET read_at=now()` for all ids in env. | +| POST | `/alerts/bulk-ack` | `{ instanceIds: [...] }` | **NEW** VIEWER+ | Parallel to bulk-read. | +| DELETE | `/alerts/{id}` | — | **NEW** OPERATOR+ | Sets `deleted_at=now`. Returns 204. | +| POST | `/alerts/bulk-delete` | `{ instanceIds: [...] }` | **NEW** OPERATOR+ | Bulk soft-delete in env. | + +Removed: +- `AlertReadRepository` bean + `alert_reads` usage — `read_at`/`bulk-read` now update `alert_instances` directly. +- `ACKNOWLEDGED` handling in all backend code paths. + +`InAppInboxQuery.countUnread` rewires to a single SQL count on `alert_instances` with `read_at IS NULL AND deleted_at IS NULL` + target-visibility predicate. + +### Backend — evaluator + notifier + +- `AlertInstanceRepository.findOpenByRule(ruleId, subjectFingerprint)` already exists; its predicate now matches the new index (`state IN ('PENDING','FIRING') AND deleted_at IS NULL`). +- All test fixtures that assert `state=ACKNOWLEDGED` → assert `acked_at IS NOT NULL`. +- Notification pipeline (`AlertNotifier`) already fires on state transitions; no change — ack no longer being a state means one fewer state-change branch to handle. + +### Silence from list — no new endpoint + +UI row-action calls existing `POST /alerts/silences` with `{ matcher: { ruleId: }, startsAt: now, endsAt: now + duration, reason: "Silenced from inbox" }`. The duration picker is a small menu: `1h / 8h / 24h / Custom…`. "Custom" routes to `/alerts/silences` (the existing SilencesPage form) with the `ruleId` pre-filled via URL search param. + +### UI — `InboxPage.tsx` + +**Filter bar (topnavbar-style, left-to-right):** + +| Filter | Values | Default | +|---|---|---| +| Severity (ButtonGroup multi) | CRITICAL · WARNING · INFO | none (= no filter) | +| Status (ButtonGroup multi) | PENDING · FIRING · RESOLVED | FIRING selected | +| Hide acked (Toggle) | on/off | **on** | +| Hide read (Toggle) | on/off | **on** | + +Default state: "Show me firing things nobody's touched." Matches the "what needs attention" mental model. + +**Row actions column** (right-aligned, shown on hover or always for the touched row): +- `Acknowledge` (when `acked_at IS NULL`) +- `Mark read` (when `read_at IS NULL`) +- `Silence rule…` (opens quick menu: `1h / 8h / 24h / Custom…`) +- `Delete` (trash icon, OPERATOR+ only). Soft-delete. Undo toast for 5s invalidates the mutation. + +**Bulk toolbar** (shown when selection > 0, above table): +- `Acknowledge N` (filters to unacked) +- `Mark N read` (filters to unread) +- `Silence rules` (silences every unique ruleId in selection — duration menu) +- `Delete N` (OPERATOR+) — opens confirmation modal: "Delete N alerts? This affects all users." + +**Deleted/dropped files:** +- `ui/src/pages/Alerts/AllAlertsPage.tsx` — removed +- `ui/src/pages/Alerts/HistoryPage.tsx` — removed +- `/alerts/all` and `/alerts/history` route definitions in `router.tsx` — removed + +**Sidebar:** +`buildAlertsTreeNodes` in `ui/src/components/sidebar-utils.ts` trims to: Inbox · Rules · Silences. "Inbox" stays as the first child; selecting `/alerts` (bare) goes to inbox. + +**CMD-K:** `buildAlertSearchData` still registers `alert` and `alertRule` categories, but the alert-category deep links all point to `/alerts/inbox/{id}` (single detail route). + +### Tests + +Backend: +- `AlertControllerTest` — new `acked` + `read` filter cases, new DELETE + bulk-delete + bulk-ack tests, 404 on soft-deleted instance. +- `PostgresAlertInstanceRepositoryTest` — `markRead`/`bulkMarkRead`/`softDelete`/`bulkSoftDelete` SQL, index predicate correctness. +- V17 migration test: seed an `ACKNOWLEDGED` row pre-migration, verify post-migration state and index. +- Every test using `AlertState.ACKNOWLEDGED` — removed or switched to `acked_at IS NOT NULL` assertion. +- `AlertNotifierTest` — confirm no regression on notification emission paths. + +UI: +- `InboxPage.test.tsx` — filter toggles, select-all, row actions, bulk actions, optimistic delete + undo. +- `enums.test.ts` snapshot — `AlertState` drops ACKNOWLEDGED, new filter option arrays added. +- Silence duration menu component test. + +### Docs / rules updates + +- `.claude/rules/app-classes.md`: + - `AlertController` endpoint list updated (new DELETE, bulk-delete, bulk-ack; `acked`/`read` filter params; `ACKNOWLEDGED` removed from allowed state). + - Drop `AlertReadRepository` from `security/` or repository listings. +- `.claude/rules/ui.md`: + - Alerts section: remove "All" and "History" pages, drop their routes. Rewrite Inbox description to new filter bar + actions. + - Note: unread-count bell now global. +- `.claude/rules/core-classes.md`: + - `AlertState` enum values reduced to three. + - Note `alert_reads` table is retired. +- `CLAUDE.md`: + - New migration entry: `V17 — Alerts: drop ACKNOWLEDGED state, add read_at/deleted_at, drop alert_reads, rework open-rule index.` + +## Risk / open concerns + +1. **Enum-type swap on a populated table.** `ALTER COLUMN TYPE … USING cast::text::enum_v2` rewrites every row. `alert_instances` is expected to remain bounded (RESOLVED rows age out via retention), but on large installs this should run during a low-traffic window. Migration is idempotent. +2. **Concurrent ack/delete races.** Both are simple column updates with `WHERE id=? AND deleted_at IS NULL`; last-write wins is acceptable per the "no individual tracking" decision. +3. **Notification context mustache variables.** No change — `alert.state` shape is unchanged; templates referencing `state=ACKNOWLEDGED` are user-authored and will start producing no matches after the migration, which is intentional. Add a release note. +4. **CMD-K deep links** to deleted alert ids return 404 now (they did before for missing, now also for soft-deleted). Acceptable. + +## Acceptance + +- Single inbox at `/alerts/inbox` with four filter dimensions wired end-to-end. +- Silence-rule menu works from row + bulk. +- Soft-delete works from row + bulk, with OPERATOR+ guard and undo toast for single. +- Unread count bell reflects global `read_at IS NULL`. +- All existing backend/UI tests green; new test coverage as listed above. +- V17 up-migrates ACKNOWLEDGED rows cleanly; reviewer can verify with a seeded pre-migration snapshot. From 70bf59daca8c866bad36fbc8c3e728cec36fa189 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:56:53 +0200 Subject: [PATCH 25/49] =?UTF-8?q?docs(alerts):=20implementation=20plan=20?= =?UTF-8?q?=E2=80=94=20inbox=20redesign=20(16=20tasks)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 16 TDD tasks covering V17 migration (drop ACKNOWLEDGED + add read_at/deleted_at + drop alert_reads + rework open-rule index), backend repo/controller/endpoints including /restore for undo-toast backing, OpenAPI regen, UI rebuild (single filterable inbox, row/bulk actions, silence-rule quick menu, SilencesPage ?ruleId= prefill), concrete test bodies, and rules/CLAUDE.md updates. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../plans/2026-04-21-alerts-inbox-redesign.md | 1615 +++++++++++++++++ 1 file changed, 1615 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-21-alerts-inbox-redesign.md diff --git a/docs/superpowers/plans/2026-04-21-alerts-inbox-redesign.md b/docs/superpowers/plans/2026-04-21-alerts-inbox-redesign.md new file mode 100644 index 00000000..8bc3d3b7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-21-alerts-inbox-redesign.md @@ -0,0 +1,1615 @@ +# Alerts Inbox Redesign Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Collapse the Inbox / All / History alert pages into a single filterable inbox; make `read`/`ack`/`delete` global timestamp flags; add Silence-rule and Delete row/bulk actions. + +**Architecture:** Drop `ACKNOWLEDGED` from `AlertState` (orthogonal `acked_at` flag already exists); add `read_at` + `deleted_at` columns; drop the `alert_reads` table; rework V13 open-rule unique index predicate to `state IN ('PENDING','FIRING') AND deleted_at IS NULL`. Rewire `/read`, `/bulk-read`, `countUnread` to update `alert_instances` directly. Add `DELETE /alerts/{id}`, `POST /alerts/{id}/restore` (undo-toast backing), `POST /alerts/bulk-delete`, `POST /alerts/bulk-ack`. Rebuild `InboxPage.tsx` with a 4-filter bar; delete `AllAlertsPage` + `HistoryPage`; trim sidebar. + +**Tech Stack:** Java 17 / Spring Boot 3.4.3 / Flyway / PostgreSQL 16 / TypeScript / React 18 / React Router v6 / TanStack Query / `@cameleer/design-system`. + +**Spec:** `docs/superpowers/specs/2026-04-21-alerts-inbox-redesign-design.md` + +--- + +## File structure + +**Backend (core):** +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java` — drop `ACKNOWLEDGED` +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java` — add `readAt`, `deletedAt` +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java` — add `acked`/`read` filter params, `markRead`/`bulkMarkRead`/`softDelete`/`bulkSoftDelete`, `countUnread(envId, userId, groupIds, roleNames)` (replaces old single-target countUnreadBySeverityForUser) +- Delete: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java` + +**Backend (app):** +- Create: `cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java` — drop `ACKNOWLEDGED` case +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java` — DELETE, bulk-delete, bulk-ack, acked/read filter params, rewire `/read` + `/bulk-read` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java` — add filter params, countUnread rewire +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java` — add `readAt` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java` — drop `alertReadRepository` bean +- Delete: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java` + +**UI:** +- Regenerate: `ui/src/api/schema.d.ts`, `ui/src/api/openapi.json` +- Modify: `ui/src/api/queries/alerts.ts` — new mutations, new filter params +- Delete: `ui/src/pages/Alerts/AllAlertsPage.tsx`, `ui/src/pages/Alerts/HistoryPage.tsx` +- Modify: `ui/src/router.tsx` — drop `/alerts/all`, `/alerts/history` +- Modify: `ui/src/components/sidebar-utils.ts` — trim `buildAlertsTreeNodes` +- Modify: `ui/src/pages/Alerts/InboxPage.tsx` — new filter bar + actions +- Create: `ui/src/pages/Alerts/SilenceRuleMenu.tsx` — small duration-picker menu +- Modify: `ui/src/pages/Alerts/SilencesPage.tsx` — read `?ruleId=` URL param + +**Tests:** +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java` +- Create: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java` +- Create: `ui/src/pages/Alerts/InboxPage.test.tsx` + +**Docs/rules:** +- Modify: `.claude/rules/app-classes.md`, `.claude/rules/ui.md`, `.claude/rules/core-classes.md` +- Modify: `CLAUDE.md` + +--- + +## Working rules (every task) + +1. **Run migration integration tests via Testcontainers** — `mvn -pl cameleer-server-app -Dtest= test`. The suite already bootstraps Postgres via `@SpringBootTest`. +2. **Commit per task.** Commit message format: `feat(alerts): ` / `refactor(alerts): ` / `test(alerts): `. End with the Claude co-author trailer. +3. **After any backend REST change**, regenerate OpenAPI in one step: `cd ui && (curl -sS http://localhost:8081/api/v1/api-docs > src/api/openapi.json) && npx openapi-typescript src/api/openapi.json -o src/api/schema.d.ts`. This requires the backend running. +4. **Run `gitnexus_impact` before editing any symbol touched by ≥2 callers**, per project rule. Report blast radius. Run `gitnexus_detect_changes` pre-commit. +5. **Don't batch.** One task = one commit. Mark TODO items complete as you go. + +--- + +## Task 1: V17 migration — drop ACKNOWLEDGED, add read_at + deleted_at, drop alert_reads, rework open-rule index + +**Files:** +- Create: `cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql` +- Create: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java` + +- [ ] **Step 1: Write the migration SQL** + +```sql +-- V17 — Alerts: drop ACKNOWLEDGED state, add read_at/deleted_at, drop alert_reads, +-- rework open-rule unique index predicate to survive ack (acked no longer "closed"). + +-- 1. Coerce ACKNOWLEDGED rows → FIRING (acked_at already set on these rows) +UPDATE alert_instances SET state = 'FIRING' WHERE state = 'ACKNOWLEDGED'; + +-- 2. Swap alert_state_enum to remove ACKNOWLEDGED (Postgres can't drop enum values in place) +CREATE TYPE alert_state_enum_v2 AS ENUM ('PENDING','FIRING','RESOLVED'); +ALTER TABLE alert_instances + ALTER COLUMN state TYPE alert_state_enum_v2 + USING state::text::alert_state_enum_v2; +DROP TYPE alert_state_enum; +ALTER TYPE alert_state_enum_v2 RENAME TO alert_state_enum; + +-- 3. New orthogonal flag columns +ALTER TABLE alert_instances + ADD COLUMN read_at timestamptz NULL, + ADD COLUMN deleted_at timestamptz NULL; + +CREATE INDEX alert_instances_unread_idx + ON alert_instances (environment_id, read_at) + WHERE read_at IS NULL AND deleted_at IS NULL; + +CREATE INDEX alert_instances_deleted_idx + ON alert_instances (deleted_at) + WHERE deleted_at IS NOT NULL; + +-- 4. Rework the V13/V15/V16 open-rule uniqueness index: +-- - drop ACKNOWLEDGED from the predicate (ack no longer "closes") +-- - add "AND deleted_at IS NULL" so a soft-deleted row frees the slot +DROP INDEX IF EXISTS alert_instances_open_rule_uq; +CREATE UNIQUE INDEX alert_instances_open_rule_uq + ON alert_instances (rule_id, (COALESCE( + context->>'_subjectFingerprint', + context->'exchange'->>'id', + ''))) + WHERE rule_id IS NOT NULL + AND state IN ('PENDING','FIRING') + AND deleted_at IS NULL; + +-- 5. Drop the per-user reads table — read is now global on alert_instances.read_at +DROP TABLE alert_reads; +``` + +- [ ] **Step 2: Write the failing migration IT** + +```java +package com.cameleer.server.app.alerting.storage; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@ActiveProfiles("test") +class V17MigrationIT { + + @Autowired JdbcTemplate jdbc; + + @Test + void alert_state_enum_drops_acknowledged() { + var values = jdbc.queryForList(""" + SELECT unnest(enum_range(NULL::alert_state_enum))::text AS v + """, String.class); + assertThat(values).containsExactlyInAnyOrder("PENDING", "FIRING", "RESOLVED"); + } + + @Test + void read_at_and_deleted_at_columns_exist() { + var cols = jdbc.queryForList(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'alert_instances' + AND column_name IN ('read_at','deleted_at') + """, String.class); + assertThat(cols).containsExactlyInAnyOrder("read_at", "deleted_at"); + } + + @Test + void alert_reads_table_is_gone() { + Integer count = jdbc.queryForObject(""" + SELECT COUNT(*)::int FROM information_schema.tables + WHERE table_name = 'alert_reads' + """, Integer.class); + assertThat(count).isZero(); + } + + @Test + void open_rule_index_predicate_is_reworked() { + String def = jdbc.queryForObject(""" + SELECT pg_get_indexdef(indexrelid) + FROM pg_index + JOIN pg_class ON pg_class.oid = pg_index.indexrelid + WHERE pg_class.relname = 'alert_instances_open_rule_uq' + """, String.class); + assertThat(def).contains("state = ANY (ARRAY['PENDING'::alert_state_enum, 'FIRING'::alert_state_enum])"); + assertThat(def).contains("deleted_at IS NULL"); + } +} +``` + +- [ ] **Step 3: Run the tests — expect 4 failures** (migration not applied yet) + +Run: `mvn -pl cameleer-server-app -Dtest=V17MigrationIT test` +Expected: 4 failures — enum still has ACKNOWLEDGED, columns don't exist, alert_reads still exists, index predicate wrong. + +- [ ] **Step 4: Run the full suite to verify clean up-migration** — `mvn -pl cameleer-server-app verify -DskipITs=false`. Expected: V17 test class green; many existing tests RED because they reference `ACKNOWLEDGED`. That's fine — they get fixed in subsequent tasks. + +- [ ] **Step 5: Commit** + +```bash +git add cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql \ + cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java +git commit -m "feat(alerts): V17 migration — drop ACKNOWLEDGED, add read_at + deleted_at" +``` + +--- + +## Task 2: Drop ACKNOWLEDGED from AlertState + add read_at/deleted_at to AlertInstance + +**Files:** +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java` +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java` + +- [ ] **Step 1: Update `AlertState.java`** to exactly: + +```java +package com.cameleer.server.core.alerting; + +public enum AlertState { PENDING, FIRING, RESOLVED } +``` + +- [ ] **Step 2: Add `readAt` + `deletedAt` to `AlertInstance`** — insert the fields after `lastNotifiedAt` (keep insertion order stable for DTOs), update compact ctor if needed, add witherr helpers `withReadAt`, `withDeletedAt`: + +```java +public record AlertInstance( + UUID id, + UUID ruleId, + Map ruleSnapshot, + UUID environmentId, + AlertState state, + AlertSeverity severity, + Instant firedAt, + Instant ackedAt, + String ackedBy, + Instant resolvedAt, + Instant lastNotifiedAt, + Instant readAt, // NEW — global "someone has seen this" + Instant deletedAt, // NEW — soft delete + boolean silenced, + Double currentValue, + Double threshold, + Map context, + String title, + String message, + List targetUserIds, + List targetGroupIds, + List targetRoleNames) { + // ... existing compact ctor unchanged (list/map copies) ... + + public AlertInstance withReadAt(Instant i) { /* new record with readAt=i */ } + public AlertInstance withDeletedAt(Instant i) { /* new record with deletedAt=i */ } + // ... update every existing wither to include readAt/deletedAt in positional args ... +} +``` + +All existing `withXxx` helpers must be updated to pass `readAt, deletedAt` positionally to the new-record construction. This is mechanical. Any call sites in the codebase that construct `AlertInstance` directly need `null, null` inserted at those positions. + +- [ ] **Step 3: Build the module** — `mvn -pl cameleer-server-core compile`. Fix any compile errors in callers of the constructor (seed data / tests). Expected caller: `AlertStateTransitions.newInstance` — add `null, null` for readAt/deletedAt. + +- [ ] **Step 4: Run the core unit tests** — `mvn -pl cameleer-server-core test`. Expected: any direct `AlertState.ACKNOWLEDGED` references in `AlertScopeTest` → drop them or replace with `FIRING`. + +- [ ] **Step 5: Commit** + +```bash +git add cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java \ + cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java \ + cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java \ + cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java +git commit -m "refactor(alerts): drop ACKNOWLEDGED from AlertState, add readAt/deletedAt to AlertInstance" +``` + +--- + +## Task 3: AlertStateTransitions — drop ACKNOWLEDGED branch + +**Files:** +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java` + +- [ ] **Step 1: Write the failing test case** — append to `AlertStateTransitionsTest.java`: + +```java +@Test +void firing_with_ack_stays_firing_on_next_firing_tick() { + // Pre-redesign this was the "ACKNOWLEDGED stays ACK" case. Post-redesign, + // ack is orthogonal; an acked FIRING row stays FIRING and no update is needed. + AlertInstance current = newInstance(AlertState.FIRING) + .withAck("alice", Instant.parse("2026-04-21T10:00:00Z")); + Optional out = AlertStateTransitions.apply( + current, new EvalResult.Firing(1.0, null, Map.of()), rule, NOW); + assertThat(out).isEmpty(); +} +``` + +(Use existing `newInstance` helper and `rule` field in the test class.) + +- [ ] **Step 2: Run test — expect FAIL with `NoSuchFieldError: ACKNOWLEDGED`** (switch-case references removed enum value). Run: `mvn -pl cameleer-server-app -Dtest=AlertStateTransitionsTest test`. + +- [ ] **Step 3: Update `AlertStateTransitions.onFiring`** — replace the switch: + +```java +return switch (current.state()) { + case PENDING -> { + Instant promoteAt = current.firedAt().plusSeconds(rule.forDurationSeconds()); + if (!promoteAt.isAfter(now)) { + yield Optional.of(current + .withState(AlertState.FIRING) + .withFiredAt(now)); + } + yield Optional.empty(); + } + case FIRING -> Optional.empty(); + case RESOLVED -> Optional.empty(); +}; +``` + +Also update `onClear` comment to remove the `/ ACKNOWLEDGED` mention. + +- [ ] **Step 4: Run test — expect PASS** + any existing test in this class that asserted the ACKNOWLEDGED branch needs to be dropped or converted. The old "ack stays ack" case becomes the new "ack-with-fire stays firing (no update)" case covered above. + +- [ ] **Step 5: Run the full alerting test suite** — `mvn -pl cameleer-server-app -Dtest='*Alert*' test`. Many still RED — fixed in later tasks. + +- [ ] **Step 6: Commit** + +```bash +git add cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java \ + cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java +git commit -m "refactor(alerts): state machine — acked is orthogonal, no transition on ack" +``` + +--- + +## Task 4: AlertInstanceRepository interface — filter params + new methods, drop AlertReadRepository + +**Files:** +- Modify: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java` +- Delete: `cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java` + +- [ ] **Step 1: Rewrite `AlertInstanceRepository`** to add filter params + new methods: + +```java +package com.cameleer.server.core.alerting; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public interface AlertInstanceRepository { + AlertInstance save(AlertInstance instance); + Optional findById(UUID id); + + /** Open instance for a rule: state IN ('PENDING','FIRING') AND deleted_at IS NULL. */ + Optional findOpenForRule(UUID ruleId); + + /** Unfiltered inbox listing — convenience overload. */ + default List listForInbox(UUID environmentId, + List userGroupIdFilter, + String userId, + List userRoleNames, + int limit) { + return listForInbox(environmentId, userGroupIdFilter, userId, userRoleNames, + null, null, null, null, limit); + } + + /** + * Inbox listing with optional filters. {@code null} or empty lists mean no filter. + * {@code acked} and {@code read} are tri-state: {@code null} = no filter, + * {@code TRUE} = only acked/read, {@code FALSE} = only unacked/unread. + * Always excludes soft-deleted rows ({@code deleted_at IS NOT NULL}). + */ + List listForInbox(UUID environmentId, + List userGroupIdFilter, + String userId, + List userRoleNames, + List states, + List severities, + Boolean acked, + Boolean read, + int limit); + + /** + * Count unread alert instances visible to the user, grouped by severity. + * Visibility: targets user directly, or via one of the given groups/roles. + * "Unread" = {@code read_at IS NULL AND deleted_at IS NULL}. + */ + Map countUnreadBySeverity(UUID environmentId, + String userId, + List groupIds, + List roleNames); + + void ack(UUID id, String userId, Instant when); + void resolve(UUID id, Instant when); + void markSilenced(UUID id, boolean silenced); + void deleteResolvedBefore(Instant cutoff); + + /** Set {@code read_at = when} if currently null. Idempotent. */ + void markRead(UUID id, Instant when); + /** Bulk variant — single UPDATE. */ + void bulkMarkRead(List ids, Instant when); + + /** Set {@code deleted_at = when} if currently null. Idempotent. */ + void softDelete(UUID id, Instant when); + /** Bulk variant — single UPDATE. */ + void bulkSoftDelete(List ids, Instant when); + + /** Clear {@code deleted_at}. Undo for soft-delete. Idempotent. */ + void restore(UUID id); + + /** Bulk ack — single UPDATE. Each row gets {@code acked_at=when, acked_by=userId} if unacked. */ + void bulkAck(List ids, String userId, Instant when); + + List listFiringDueForReNotify(Instant now); +} +``` + +**Breaking changes vs prior signature:** +- `listForInbox` gains `acked` and `read` params (tri-state Boolean). +- `countUnreadBySeverityForUser(envId, userId)` → `countUnreadBySeverity(envId, userId, groupIds, roleNames)`. Renamed + extended scope. +- New: `markRead`, `bulkMarkRead`, `softDelete`, `bulkSoftDelete`, `bulkAck`. + +- [ ] **Step 2: Delete `AlertReadRepository.java`** — `rm cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java`. Read/ack/delete all live on `AlertInstanceRepository` now. + +- [ ] **Step 3: Compile — expect caller breakage** — `mvn -pl cameleer-server-core compile`. Then `mvn -pl cameleer-server-app compile` will surface every broken call site. Known breakages (fixed in later tasks, do **not** patch them here): + - `PostgresAlertInstanceRepository` (Task 5) + - `AlertController` (Task 6) + - `InAppInboxQuery` (Task 6) + - `AlertingBeanConfig` (Task 5) + - `PostgresAlertReadRepository` (Task 5) + +- [ ] **Step 4: Commit** + +```bash +git add cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +git rm cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java +git commit -m "feat(alerts): core repo — filter params + markRead/softDelete/bulkAck; drop AlertReadRepository" +``` + +--- + +## Task 5: PostgresAlertInstanceRepository — save/rowMapper, new methods, listForInbox filters, countUnread + +**Files:** +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java` +- Delete: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java` + +- [ ] **Step 1: Update `save` SQL** — add `read_at`, `deleted_at` to INSERT column list, values list, `ON CONFLICT DO UPDATE` list. Add `Timestamp.from(i.readAt())`, `Timestamp.from(i.deletedAt())` to the `jdbc.update(...)` args (via the existing `ts()` helper that null-safes). Follow the same pattern as `ackedAt`. + +- [ ] **Step 2: Update `findOpenForRule` predicate**: + +```java +var list = jdbc.query(""" + SELECT * FROM alert_instances + WHERE rule_id = ? + AND state IN ('PENDING','FIRING') + AND deleted_at IS NULL + LIMIT 1 + """, rowMapper(), ruleId); +``` + +- [ ] **Step 3: Update `ack` — no state change anymore**: + +```java +@Override +public void ack(UUID id, String userId, Instant when) { + jdbc.update(""" + UPDATE alert_instances + SET acked_at = ?, acked_by = ? + WHERE id = ? AND acked_at IS NULL AND deleted_at IS NULL + """, Timestamp.from(when), userId, id); +} +``` + +- [ ] **Step 4: Implement `listForInbox` with tri-state filters** — add to existing dynamic SQL: + +```java +// after severity filter block: +if (acked != null) { + sql.append(acked ? " AND acked_at IS NOT NULL" : " AND acked_at IS NULL"); +} +if (read != null) { + sql.append(read ? " AND read_at IS NOT NULL" : " AND read_at IS NULL"); +} +sql.append(" AND deleted_at IS NULL"); +sql.append(" ORDER BY fired_at DESC LIMIT ?"); +``` + +Signature matches the interface (`Boolean acked`, `Boolean read`). + +- [ ] **Step 5: Rewrite `countUnreadBySeverity`** — removes the `alert_reads` join: + +```java +@Override +public Map countUnreadBySeverity(UUID environmentId, + String userId, + List groupIds, + List roleNames) { + Array groupArray = toUuidArrayFromStrings(groupIds); + Array roleArray = toTextArray(roleNames); + String sql = """ + SELECT severity::text AS severity, COUNT(*) AS cnt + FROM alert_instances + WHERE environment_id = ? + AND read_at IS NULL + AND deleted_at IS NULL + AND ( + ? = ANY(target_user_ids) + OR target_group_ids && ? + OR target_role_names && ? + ) + GROUP BY severity + """; + EnumMap counts = new EnumMap<>(AlertSeverity.class); + for (AlertSeverity s : AlertSeverity.values()) counts.put(s, 0L); + jdbc.query(sql, rs -> counts.put( + AlertSeverity.valueOf(rs.getString("severity")), rs.getLong("cnt") + ), environmentId, userId, groupArray, roleArray); + return counts; +} +``` + +- [ ] **Step 6: Implement new mutation methods**: + +```java +@Override +public void markRead(UUID id, Instant when) { + jdbc.update("UPDATE alert_instances SET read_at = ? WHERE id = ? AND read_at IS NULL", + Timestamp.from(when), id); +} + +@Override +public void bulkMarkRead(List ids, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET read_at = ? + WHERE id = ANY(?) AND read_at IS NULL + """, Timestamp.from(when), idArray); +} + +@Override +public void softDelete(UUID id, Instant when) { + jdbc.update("UPDATE alert_instances SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL", + Timestamp.from(when), id); +} + +@Override +public void bulkSoftDelete(List ids, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET deleted_at = ? + WHERE id = ANY(?) AND deleted_at IS NULL + """, Timestamp.from(when), idArray); +} + +@Override +public void restore(UUID id) { + jdbc.update("UPDATE alert_instances SET deleted_at = NULL WHERE id = ?", id); +} + +@Override +public void bulkAck(List ids, String userId, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET acked_at = ?, acked_by = ? + WHERE id = ANY(?) AND acked_at IS NULL AND deleted_at IS NULL + """, Timestamp.from(when), userId, idArray); +} +``` + +- [ ] **Step 7: Update `rowMapper`** — read `read_at` + `deleted_at`: + +```java +Timestamp readAt = rs.getTimestamp("read_at"); +Timestamp deletedAt = rs.getTimestamp("deleted_at"); +// ... and pass: +readAt == null ? null : readAt.toInstant(), +deletedAt == null ? null : deletedAt.toInstant(), +``` + +Insert at the position matching the record's field order (after `lastNotifiedAt`, before `silenced`). + +- [ ] **Step 8: Delete `PostgresAlertReadRepository.java`** + remove its bean from `AlertingBeanConfig`: + +```bash +git rm cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java +``` + +In `AlertingBeanConfig.java` remove: +```java +@Bean +public AlertReadRepository alertReadRepository(JdbcTemplate jdbc) { ... } +``` +Remove the `AlertReadRepository` import too. + +- [ ] **Step 9: Update `PostgresAlertInstanceRepositoryIT`** — add tests: + +```java +@Test +void markRead_is_idempotent_and_sets_read_at() { + var inst = insertFreshFiring(); + repo.markRead(inst.id(), Instant.parse("2026-04-21T10:00:00Z")); + repo.markRead(inst.id(), Instant.parse("2026-04-21T11:00:00Z")); // idempotent — no-op + var loaded = repo.findById(inst.id()).orElseThrow(); + assertThat(loaded.readAt()).isEqualTo(Instant.parse("2026-04-21T10:00:00Z")); +} + +@Test +void softDelete_excludes_from_listForInbox() { + var inst = insertFreshFiring(); + repo.softDelete(inst.id(), Instant.parse("2026-04-21T10:00:00Z")); + var rows = repo.listForInbox(ENV_ID, List.of(), USER_ID, List.of(), + null, null, null, null, 100); + assertThat(rows).extracting(AlertInstance::id).doesNotContain(inst.id()); +} + +@Test +void findOpenForRule_returns_acked_firing() { + var inst = insertFreshFiring(); + repo.ack(inst.id(), USER_ID, Instant.parse("2026-04-21T10:00:00Z")); + var open = repo.findOpenForRule(inst.ruleId()); + assertThat(open).isPresent(); // ack no longer closes the open slot +} + +@Test +void findOpenForRule_skips_soft_deleted() { + var inst = insertFreshFiring(); + repo.softDelete(inst.id(), Instant.now()); + assertThat(repo.findOpenForRule(inst.ruleId())).isEmpty(); +} + +@Test +void bulk_ack_only_touches_unacked_rows() { + var a = insertFreshFiring(); + var b = insertFreshFiring(); + repo.ack(a.id(), "alice", Instant.parse("2026-04-21T09:00:00Z")); + repo.bulkAck(List.of(a.id(), b.id()), "bob", Instant.parse("2026-04-21T10:00:00Z")); + assertThat(repo.findById(a.id()).orElseThrow().ackedBy()).isEqualTo("alice"); + assertThat(repo.findById(b.id()).orElseThrow().ackedBy()).isEqualTo("bob"); +} + +@Test +void listForInbox_acked_false_hides_acked_rows() { + var a = insertFreshFiring(); + var b = insertFreshFiring(); + repo.ack(a.id(), "alice", Instant.now()); + var rows = repo.listForInbox(ENV_ID, List.of(), USER_ID, List.of(), + null, null, /*acked*/ false, null, 100); + assertThat(rows).extracting(AlertInstance::id).containsExactly(b.id()); +} +``` + +(Use the test class's existing `insertFreshFiring()` helper or add a small factory. `ENV_ID` / `USER_ID` are existing fixture constants.) + +Also update any existing test asserting `state == ACKNOWLEDGED` after `ack()` — instead assert `ackedAt != null && state == FIRING`. + +- [ ] **Step 10: Run the repo IT** — `mvn -pl cameleer-server-app -Dtest=PostgresAlertInstanceRepositoryIT test`. Expected: all green. + +- [ ] **Step 11: Commit** + +```bash +git add cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java \ + cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java \ + cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +git rm cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java +git commit -m "feat(alerts): Postgres repo — markRead/softDelete/bulkAck, acked/read filters, countUnread via read_at" +``` + +--- + +## Task 6: InAppInboxQuery + AlertController — new endpoints, filter params, rewire read + +**Files:** +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java` +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java` +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java` + +- [ ] **Step 1: Update `AlertDto`** — add `readAt`: + +```java +public record AlertDto( + UUID id, + UUID ruleId, + UUID environmentId, + AlertState state, + AlertSeverity severity, + String title, + String message, + Instant firedAt, + Instant ackedAt, + String ackedBy, + Instant resolvedAt, + Instant readAt, // NEW + boolean silenced, + Double currentValue, + Double threshold, + Map context +) { + public static AlertDto from(AlertInstance i) { + return new AlertDto( + i.id(), i.ruleId(), i.environmentId(), i.state(), i.severity(), + i.title(), i.message(), i.firedAt(), i.ackedAt(), i.ackedBy(), + i.resolvedAt(), i.readAt(), i.silenced(), + i.currentValue(), i.threshold(), i.context()); + } +} +``` + +`deletedAt` is intentionally absent — soft-deleted rows never reach the wire. + +- [ ] **Step 2: Update `InAppInboxQuery`** — new filter signature, rewired countUnread: + +```java +public List listInbox(UUID envId, + String userId, + List states, + List severities, + Boolean acked, + Boolean read, + int limit) { + List groupIds = resolveGroupIds(userId); + List roleNames = resolveRoleNames(userId); + return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, + states, severities, acked, read, limit); +} + +public UnreadCountResponse countUnread(UUID envId, String userId) { + Key key = new Key(envId, userId); + Instant now = Instant.now(clock); + Entry cached = memo.get(key); + if (cached != null && now.isBefore(cached.expiresAt())) return cached.response(); + List groupIds = resolveGroupIds(userId); + List roleNames = resolveRoleNames(userId); + Map bySeverity = + instanceRepo.countUnreadBySeverity(envId, userId, groupIds, roleNames); + UnreadCountResponse response = UnreadCountResponse.from(bySeverity); + memo.put(key, new Entry(response, now.plusMillis(MEMO_TTL_MS))); + return response; +} +``` + +Drop the deprecated 4-arg `listInbox` overload — the controller will use the full form. + +- [ ] **Step 3: Update `AlertController`** — add DELETE, bulk-delete, bulk-ack, acked/read filter params, rewire read/bulk-read: + +```java +@GetMapping +public List list( + @EnvPath Environment env, + @RequestParam(defaultValue = "50") int limit, + @RequestParam(required = false) List state, + @RequestParam(required = false) List severity, + @RequestParam(required = false) Boolean acked, + @RequestParam(required = false) Boolean read) { + String userId = currentUserId(); + int effectiveLimit = Math.min(limit, 200); + return inboxQuery.listInbox(env.id(), userId, state, severity, acked, read, effectiveLimit) + .stream().map(AlertDto::from).toList(); +} + +@PostMapping("/{id}/read") +public void read(@EnvPath Environment env, @PathVariable UUID id) { + requireLiveInstance(id, env.id()); + instanceRepo.markRead(id, Instant.now()); +} + +@PostMapping("/bulk-read") +public void bulkRead(@EnvPath Environment env, @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvIds(req.instanceIds(), env.id()); + if (!filtered.isEmpty()) instanceRepo.bulkMarkRead(filtered, Instant.now()); +} + +@PostMapping("/bulk-ack") +@PreAuthorize("hasAnyRole('VIEWER','OPERATOR','ADMIN')") +public void bulkAck(@EnvPath Environment env, @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvIds(req.instanceIds(), env.id()); + if (!filtered.isEmpty()) instanceRepo.bulkAck(filtered, currentUserId(), Instant.now()); +} + +@DeleteMapping("/{id}") +@PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") +public ResponseEntity delete(@EnvPath Environment env, @PathVariable UUID id) { + requireLiveInstance(id, env.id()); + instanceRepo.softDelete(id, Instant.now()); + return ResponseEntity.noContent().build(); +} + +@PostMapping("/bulk-delete") +@PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") +public void bulkDelete(@EnvPath Environment env, @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvIds(req.instanceIds(), env.id()); + if (!filtered.isEmpty()) instanceRepo.bulkSoftDelete(filtered, Instant.now()); +} + +@PostMapping("/{id}/restore") +@PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") +public ResponseEntity restore(@EnvPath Environment env, @PathVariable UUID id) { + // Must find the row regardless of deleted_at (requireLiveInstance would 404 on a deleted row). + AlertInstance inst = instanceRepo.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found")); + if (!inst.environmentId().equals(env.id())) + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found in env"); + instanceRepo.restore(id); + return ResponseEntity.noContent().build(); +} + +// Helpers: +private AlertInstance requireLiveInstance(UUID id, UUID envId) { + AlertInstance i = instanceRepo.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found")); + if (!i.environmentId().equals(envId) || i.deletedAt() != null) + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found in env"); + return i; +} + +private List inEnvIds(List ids, UUID envId) { + return ids.stream() + .filter(id -> instanceRepo.findById(id) + .map(i -> i.environmentId().equals(envId) && i.deletedAt() == null) + .orElse(false)) + .toList(); +} +``` + +Rename `BulkReadRequest` → `BulkIdsRequest` (the body shape `{ instanceIds: [...] }` is identical for read/ack/delete; one DTO covers all three). Or keep `BulkReadRequest` and add `BulkAckRequest` + `BulkDeleteRequest` as sealed aliases — pick **rename to `BulkIdsRequest`** for DRY. Update references accordingly. + +Drop the `AlertReadRepository` field + constructor param. + +- [ ] **Step 4: Update `AlertControllerIT`** — add cases: + +```java +@Test +void bulkDelete_softDeletes_matching_rows_in_env_only() { /* two rows in env, one out; bulk-delete both; only in-env row is deleted */ } + +@Test +void list_respects_read_filter() { /* three rows, two unread; ?read=false returns two */ } + +@Test +void list_respects_acked_filter() { /* similar */ } + +@Test +void delete_non_operator_returns_403() { /* VIEWER role → 403 */ } + +@Test +void get_returns_404_for_soft_deleted() { /* delete then GET → 404 */ } +``` + +Use existing test harness (`@WebMvcTest` or full `@SpringBootTest`, whichever the current IT uses). + +Also fix every existing IT assertion that expects `state=ACKNOWLEDGED` — switch to `ackedAt != null`. + +- [ ] **Step 5: Run** — `mvn -pl cameleer-server-app -Dtest=AlertControllerIT test`. Expected: green. + +- [ ] **Step 6: Commit** + +```bash +git add cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java \ + cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java \ + cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java \ + cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkIdsRequest.java \ + cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +git rm cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkReadRequest.java +git commit -m "feat(alerts): controller — DELETE + bulk-delete/bulk-ack, acked/read filters, read via instance" +``` + +--- + +## Task 7: Fix remaining backend tests + SecurityConfig matchers + +**Files:** +- Modify: `cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java` (RBAC matchers if needed) +- Modify: `cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java` +- Modify: any other test referencing `ACKNOWLEDGED` / `AlertReadRepository` +- Modify: Grep-find all: `grep -rn "ACKNOWLEDGED\|AlertReadRepository\|alert_reads" cameleer-server-app/src cameleer-server-core/src` + +- [ ] **Step 1: List remaining ACKNOWLEDGED references** + +Run: `grep -rn "ACKNOWLEDGED\|AlertReadRepository\|alert_reads" cameleer-server-app/src cameleer-server-core/src` +Expected: only test + doc fixture references now. Patch each assertion from `state==ACKNOWLEDGED` → `ackedAt != null`. + +- [ ] **Step 2: Update `SecurityConfig` matchers** — add matchers for the new endpoints: + +```java +// Ack/read/bulk-read/bulk-ack — VIEWER+ (matches existing) +.requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/*/ack", "/api/v1/environments/*/alerts/*/read", + "/api/v1/environments/*/alerts/bulk-read", "/api/v1/environments/*/alerts/bulk-ack") + .hasAnyRole("VIEWER","OPERATOR","ADMIN") +// Delete/bulk-delete — OPERATOR+ +.requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/*") + .hasAnyRole("OPERATOR","ADMIN") +.requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/bulk-delete") + .hasAnyRole("OPERATOR","ADMIN") +``` + +Verify the existing config — the `@PreAuthorize` annotations on the controller methods cover this, but add URL matchers if the existing config relies on them (inspect before copy-paste). + +- [ ] **Step 3: Update `AlertingFullLifecycleIT`** — any flow that did `ack → state=ACKNOWLEDGED → resolve` now skips the state change on ack. Patch asserts. + +- [ ] **Step 4: Run the whole alerting suite** — `mvn -pl cameleer-server-app -Dtest='*Alert*' test`. Expected: green. + +- [ ] **Step 5: Full build** — `mvn clean verify`. Expected: green. + +- [ ] **Step 6: Commit** + +```bash +git add -A +git commit -m "test(alerts): migrate fixtures off ACKNOWLEDGED + alert_reads" +``` + +--- + +## Task 8: Regenerate OpenAPI schema + +**Files:** +- Regenerate: `ui/src/api/schema.d.ts`, `ui/src/api/openapi.json` + +- [ ] **Step 1: Start backend** — `java -jar cameleer-server-app/target/cameleer-server-app-1.0-SNAPSHOT.jar` in a separate terminal, or via Docker per your local workflow. Wait for `:8081` to accept. + +- [ ] **Step 2: Regenerate schema** + +```bash +cd ui +curl -sS http://localhost:8081/api/v1/api-docs > src/api/openapi.json +npx openapi-typescript src/api/openapi.json -o src/api/schema.d.ts +``` + +- [ ] **Step 3: Verify new endpoints appear** — `grep -E "bulk-delete|bulk-ack|\"delete\"" src/api/schema.d.ts | head -20`. Expected: DELETE on `/alerts/{id}`, POST on bulk-delete + bulk-ack. `AlertDto` includes `readAt`. `/alerts` GET has `acked`/`read` query params. + +- [ ] **Step 4: Surface TS breakages** — `cd ui && npx tsc --noEmit`. Expected: errors in `alerts.ts`, `InboxPage.tsx`, `AllAlertsPage.tsx`, `HistoryPage.tsx` — all fixed in subsequent tasks. Do **not** patch here. + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/api/openapi.json ui/src/api/schema.d.ts +git commit -m "chore(ui): regenerate OpenAPI schema for alerts inbox redesign" +``` + +--- + +## Task 9: UI alerts.ts — new mutations + useAlerts filter params + +**Files:** +- Modify: `ui/src/api/queries/alerts.ts` + +- [ ] **Step 1: Update `AlertsFilter` + `useAlerts`** — add `acked?: boolean`, `read?: boolean`: + +```ts +export interface AlertsFilter { + state?: AlertState | AlertState[]; + severity?: AlertSeverity | AlertSeverity[]; + acked?: boolean; + read?: boolean; + ruleId?: string; + limit?: number; +} + +// in useAlerts queryFn, after severity handling: +if (filter.acked !== undefined) query.acked = filter.acked; +if (filter.read !== undefined) query.read = filter.read; + +// queryKey must include acked/read: +queryKey: ['alerts', env, 'list', fetchLimit, stateKey, severityKey, filter.acked ?? null, filter.read ?? null], +``` + +- [ ] **Step 2: Rewire `useBulkReadAlerts` body** — body shape is identical (`{ instanceIds }`), but fix the TS reference if the generated type renamed it to `BulkIdsRequest`. + +- [ ] **Step 3: Add four new mutations**: + +```ts +export function useBulkAckAlerts() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (ids: string[]) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/bulk-ack', + { params: { path: { envSlug: env } }, body: { instanceIds: ids } } as any); + if (error) throw error; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['alerts', env] }), + }); +} + +export function useDeleteAlert() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.DELETE( + '/environments/{envSlug}/alerts/{id}', + { params: { path: { envSlug: env, id } } } as any); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} + +export function useBulkDeleteAlerts() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (ids: string[]) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/bulk-delete', + { params: { path: { envSlug: env } }, body: { instanceIds: ids } } as any); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} + +export function useRestoreAlert() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/{id}/restore', + { params: { path: { envSlug: env, id } } } as any); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} +``` + +- [ ] **Step 4: `npx tsc --noEmit`** — expected: only `AllAlertsPage.tsx`, `HistoryPage.tsx`, `InboxPage.tsx` errors remain. Those pages are rewritten/deleted in the next tasks. + +- [ ] **Step 5: Commit** + +```bash +git add ui/src/api/queries/alerts.ts +git commit -m "feat(ui/alerts): hooks for bulk-ack, delete, bulk-delete; acked/read filter params" +``` + +--- + +## Task 10: Remove AllAlertsPage, HistoryPage, router routes, sidebar trim + +**Files:** +- Delete: `ui/src/pages/Alerts/AllAlertsPage.tsx` +- Delete: `ui/src/pages/Alerts/HistoryPage.tsx` +- Modify: `ui/src/router.tsx` +- Modify: `ui/src/components/sidebar-utils.ts` + +- [ ] **Step 1: Delete the two pages** + +```bash +git rm ui/src/pages/Alerts/AllAlertsPage.tsx ui/src/pages/Alerts/HistoryPage.tsx +``` + +- [ ] **Step 2: Trim `router.tsx`** — drop lines 27, 28 (lazy imports) and the two route entries at lines 87, 88 (`/alerts/all`, `/alerts/history`). Keep the `/alerts` → `/alerts/inbox` redirect on line 85 (sidebar click lands here). + +- [ ] **Step 3: Trim `buildAlertsTreeNodes`** in `ui/src/components/sidebar-utils.ts`: + +```ts +export function buildAlertsTreeNodes(): SidebarTreeNode[] { + const icon = (el: ReactNode) => el; + return [ + { id: 'alerts-inbox', label: 'Inbox', path: '/alerts/inbox', icon: icon(createElement(Inbox, { size: 14 })) }, + { id: 'alerts-rules', label: 'Rules', path: '/alerts/rules', icon: icon(createElement(AlertTriangle, { size: 14 })) }, + { id: 'alerts-silences', label: 'Silences', path: '/alerts/silences', icon: icon(createElement(BellOff, { size: 14 })) }, + ]; +} +``` + +Remove `List`, `ScrollText` from the `lucide-react` import (if no other user). + +- [ ] **Step 4: Check CMD-K** — `grep -n "alerts/all\|alerts/history" ui/src`. Expected: no hits. `buildAlertSearchData` in `LayoutShell.tsx` deep-links to `/alerts/inbox/{id}` regardless. + +- [ ] **Step 5: Run typecheck** — `cd ui && npx tsc --noEmit`. Expected: only `InboxPage.tsx` errors left (next task). + +- [ ] **Step 6: Commit** + +```bash +git add ui/src/router.tsx ui/src/components/sidebar-utils.ts +git rm ui/src/pages/Alerts/AllAlertsPage.tsx ui/src/pages/Alerts/HistoryPage.tsx +git commit -m "refactor(ui/alerts): single inbox — remove AllAlerts + History pages, trim sidebar" +``` + +--- + +## Task 11: Create Silence-rule quick menu component + +**Files:** +- Create: `ui/src/pages/Alerts/SilenceRuleMenu.tsx` + +- [ ] **Step 1: Write the component** — a small popover/menu with three preset buttons + a "Custom…" link: + +```tsx +import { useState, useRef } from 'react'; +import { useNavigate } from 'react-router'; +import { Button, Popover } from '@cameleer/design-system'; +import { BellOff } from 'lucide-react'; +import { useCreateSilence } from '../../api/queries/alertSilences'; +import { useToast } from '@cameleer/design-system'; + +const PRESETS: Array<{ label: string; hours: number }> = [ + { label: '1 hour', hours: 1 }, + { label: '8 hours', hours: 8 }, + { label: '24 hours', hours: 24 }, +]; + +interface Props { + ruleId: string; + ruleTitle?: string; + onDone?: () => void; + variant?: 'row' | 'bulk'; +} + +export function SilenceRuleMenu({ ruleId, ruleTitle, onDone, variant = 'row' }: Props) { + const [open, setOpen] = useState(false); + const anchor = useRef(null); + const create = useCreateSilence(); + const navigate = useNavigate(); + const { toast } = useToast(); + + const createWithDuration = async (hours: number) => { + const now = new Date(); + const endsAt = new Date(now.getTime() + hours * 3600_000); + try { + await create.mutateAsync({ + matcher: { ruleId }, + reason: `Silenced from inbox${ruleTitle ? ` (${ruleTitle})` : ''}`, + startsAt: now.toISOString(), + endsAt: endsAt.toISOString(), + }); + toast({ title: `Silenced for ${hours}h`, variant: 'success' }); + setOpen(false); + onDone?.(); + } catch (e) { + toast({ title: 'Silence failed', description: String(e), variant: 'error' }); + } + }; + + return ( + <> + + setOpen(false)} anchor={anchor}> +
+ {PRESETS.map((p) => ( + + ))} + +
+
+ + ); +} +``` + +If the DS doesn't export `Popover`, fall back to the existing dropdown/menu pattern used elsewhere — search `ui/src` for "Popover\|DropdownMenu\|PopoverContent" and match an established pattern. + +- [ ] **Step 2: `npx tsc --noEmit`** — expected clean. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/SilenceRuleMenu.tsx +git commit -m "feat(ui/alerts): silence-rule quick menu (1h/8h/24h/custom)" +``` + +--- + +## Task 12: Rewrite InboxPage — filter bar, row actions, bulk toolbar + +**Files:** +- Modify: `ui/src/pages/Alerts/InboxPage.tsx` + +- [ ] **Step 1: Rewrite `InboxPage.tsx`** — full replacement. Key shape: + +```tsx +// Filter state: +const [severitySel, setSeveritySel] = useState>(new Set()); +const [stateSel, setStateSel] = useState>(new Set(['FIRING'])); // default FIRING only +const [hideAcked, setHideAcked] = useState(true); +const [hideRead, setHideRead] = useState(true); + +const { data } = useAlerts({ + severity: severitySel.size ? ([...severitySel] as AlertSeverity[]) : undefined, + state: stateSel.size ? ([...stateSel] as AlertState[]) : undefined, + acked: hideAcked ? false : undefined, + read: hideRead ? false : undefined, + limit: 200, +}); +``` + +**Filter-bar JSX** (in the page header actions column): + +```tsx +
+ + + + +
+``` + +`STATE_ITEMS` omits `ACKNOWLEDGED`: + +```tsx +const STATE_ITEMS: ButtonGroupItem[] = [ + { value: 'PENDING', label: 'Pending', color: 'var(--text-muted)' }, + { value: 'FIRING', label: 'Firing', color: 'var(--error)' }, + { value: 'RESOLVED', label: 'Resolved', color: 'var(--success)' }, +]; +``` + +**Row actions column** (replace the existing `ack` column): + +```tsx +{ + key: 'actions', header: '', width: '240px', + render: (_, row) => ( +
+ {row.ackedAt == null && ( + + )} + {row.readAt == null && ( + + )} + {row.ruleId && ( + + )} + {canDelete && ( + + )} +
+ ), +}, +``` + +`canDelete` gated via `useAuthStore()` role check: `roles.includes('OPERATOR') || roles.includes('ADMIN')`. + +**Single delete with undo toast** (5s) — backed by `useRestoreAlert`: + +```tsx +const del = useDeleteAlert(); +const restore = useRestoreAlert(); + +const onDeleteOne = async (id: string) => { + try { + await del.mutateAsync(id); + toast({ + title: 'Deleted', + variant: 'success', + duration: 5000, + action: { + label: 'Undo', + onClick: () => restore.mutateAsync(id).then( + () => toast({ title: 'Restored', variant: 'success' }), + (e) => toast({ title: 'Undo failed', description: String(e), variant: 'error' }) + ), + }, + }); + } catch (e) { + toast({ title: 'Delete failed', description: String(e), variant: 'error' }); + } +}; +``` + +If the DS `toast` API doesn't support an `action` slot, render a secondary `` inside the toast `description` ReactNode — check `@cameleer/design-system` toast signature before choosing. + +**Bulk toolbar** (the existing `filterBar` section): + +```tsx +{selectedIds.length > 0 ? ( + <> + + + + {canDelete && ( + + )} + +) : ( /* existing "Acknowledge all firing" + "Mark all read" */ )} +``` + +**Bulk-delete confirmation modal:** + +```tsx + setDeletePending(null)} + onConfirm={async () => { + if (!deletePending) return; + await bulkDelete.mutateAsync(deletePending); + toast({ title: `Deleted ${deletePending.length}`, variant: 'success' }); + setDeletePending(null); + setSelected(new Set()); + }} + title="Delete alerts?" + message={`Delete ${deletePending?.length ?? 0} alerts? This affects all users.`} + confirmText="Delete" + variant="danger" +/> +``` + +`SilenceRulesForSelection` is a helper that walks `rows` filtered to `selectedIds`, collects unique `ruleId`s, and renders a menu that creates one silence per ruleId with the chosen duration — composed from `SilenceRuleMenu` or a similar inline control. + +- [ ] **Step 2: Manual TS check + dev run** — `cd ui && npx tsc --noEmit`, expected green. Run `npm run dev`; navigate to `/alerts/inbox`, verify: + - Filter toggles work (changing hides/reveals rows) + - Ack button disappears after ack; row stays visible (hide-acked OFF) + - Silence menu opens, "1 hour" creates a silence and shows success toast + - Delete button (as OPERATOR) soft-deletes; row disappears; bell count updates + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/InboxPage.tsx +git commit -m "feat(ui/alerts): single inbox — filter bar, silence/delete row actions, bulk toolbar" +``` + +--- + +## Task 13: SilencesPage — prefill `?ruleId=` from URL + +**Files:** +- Modify: `ui/src/pages/Alerts/SilencesPage.tsx` + +- [ ] **Step 1: Add `useSearchParams` import + effect** + +```tsx +import { useSearchParams } from 'react-router'; +// inside component: +const [searchParams] = useSearchParams(); +useEffect(() => { + const r = searchParams.get('ruleId'); + if (r) setMatcherRuleId(r); +}, [searchParams]); +``` + +That's the entire change. `matcherRuleId` already feeds the create-silence form. + +- [ ] **Step 2: Manual test** — navigate to `/alerts/silences?ruleId=abc-123`; the Rule ID field is prefilled. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/SilencesPage.tsx +git commit -m "feat(ui/alerts): SilencesPage — prefill Rule ID from ?ruleId= query param" +``` + +--- + +## Task 14: UI InboxPage tests + +**Files:** +- Create: `ui/src/pages/Alerts/InboxPage.test.tsx` + +- [ ] **Step 1: Write Vitest + RTL tests** with concrete bodies. Mocks hoisted at top so hook calls return deterministic data. + +```tsx +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter } from 'react-router'; +import InboxPage from './InboxPage'; +import type { AlertDto } from '../../api/queries/alerts'; + +const alertsMock = vi.fn(); +const deleteMock = vi.fn().mockResolvedValue(undefined); +const bulkDelete = vi.fn().mockResolvedValue(undefined); +const ackMock = vi.fn().mockResolvedValue(undefined); +const markReadMock = vi.fn(); +const authRolesMock = vi.fn<[], string[]>(); + +vi.mock('../../api/queries/alerts', () => ({ + useAlerts: (...args: unknown[]) => ({ data: alertsMock(...args), isLoading: false, error: null }), + useAckAlert: () => ({ mutateAsync: ackMock, isPending: false }), + useMarkAlertRead: () => ({ mutate: markReadMock }), + useBulkReadAlerts: () => ({ mutateAsync: vi.fn(), isPending: false }), + useBulkAckAlerts: () => ({ mutateAsync: vi.fn(), isPending: false }), + useDeleteAlert: () => ({ mutateAsync: deleteMock, isPending: false }), + useBulkDeleteAlerts:() => ({ mutateAsync: bulkDelete, isPending: false }), + useRestoreAlert: () => ({ mutateAsync: vi.fn() }), +})); + +vi.mock('../../auth/auth-store', () => ({ + useAuthStore: () => ({ roles: authRolesMock() }), +})); + +function mount() { + const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + return render( + + + + + , + ); +} + +const ROW_FIRING: AlertDto = { + id: '11111111-1111-1111-1111-111111111111', + ruleId: 'rrrrrrrr-rrrr-rrrr-rrrr-rrrrrrrrrrrr', + state: 'FIRING', severity: 'CRITICAL', + title: 'Order pipeline down', message: 'msg', + firedAt: '2026-04-21T10:00:00Z', + ackedAt: null, ackedBy: null, resolvedAt: null, readAt: null, + silenced: false, currentValue: null, threshold: null, + environmentId: 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', + context: {}, +}; +const ROW_ACKED: AlertDto = { ...ROW_FIRING, id: '22222222-2222-2222-2222-222222222222', ackedAt: '2026-04-21T10:05:00Z', ackedBy: 'alice' }; + +beforeEach(() => { + vi.clearAllMocks(); + authRolesMock.mockReturnValue(['OPERATOR']); + alertsMock.mockReturnValue([ROW_FIRING]); +}); + +describe('InboxPage', () => { + it('calls useAlerts with default filters: state=[FIRING], acked=false, read=false', () => { + mount(); + expect(alertsMock).toHaveBeenCalledWith(expect.objectContaining({ + state: ['FIRING'], + acked: false, + read: false, + })); + }); + + it('unchecking "Hide acked" removes the acked filter', async () => { + mount(); + await userEvent.click(screen.getByRole('checkbox', { name: /hide acked/i })); + const lastCall = alertsMock.mock.calls.at(-1)?.[0]; + expect(lastCall).not.toHaveProperty('acked'); + }); + + it('shows Acknowledge button only on rows where ackedAt is null', () => { + alertsMock.mockReturnValue([ROW_FIRING, ROW_ACKED]); + mount(); + expect(screen.getAllByRole('button', { name: /acknowledge/i })).toHaveLength(1); + }); + + it('opens bulk-delete confirmation with the correct count', async () => { + alertsMock.mockReturnValue([ROW_FIRING, ROW_ACKED]); + mount(); + // Select both rows + for (const cb of screen.getAllByRole('checkbox', { name: /^select/i })) { + await userEvent.click(cb); + } + await userEvent.click(screen.getByRole('button', { name: /delete 2/i })); + expect(screen.getByRole('dialog')).toHaveTextContent(/delete 2 alerts/i); + }); + + it('hides Delete buttons when user lacks OPERATOR role', () => { + authRolesMock.mockReturnValue(['VIEWER']); + mount(); + expect(screen.queryByRole('button', { name: /delete/i })).toBeNull(); + }); + + it('clicking row Delete invokes useDeleteAlert and shows an Undo toast', async () => { + mount(); + await userEvent.click(within(screen.getAllByRole('row')[1]).getByRole('button', { name: /delete/i })); + expect(deleteMock).toHaveBeenCalledWith(ROW_FIRING.id); + expect(await screen.findByText(/deleted/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /undo/i })).toBeInTheDocument(); + }); +}); +``` + +Adjust selectors if design-system renders checkboxes/buttons with different accessible names — prefer role+name queries over test-ids. + +- [ ] **Step 2: Run** — `cd ui && npx vitest run src/pages/Alerts/InboxPage.test.tsx`. Green. + +- [ ] **Step 3: Commit** + +```bash +git add ui/src/pages/Alerts/InboxPage.test.tsx +git commit -m "test(ui/alerts): InboxPage filter + action coverage" +``` + +--- + +## Task 15: Rules docs + CLAUDE.md + +**Files:** +- Modify: `.claude/rules/app-classes.md` — AlertController endpoint table +- Modify: `.claude/rules/core-classes.md` — AlertState enum, drop AlertReadRepository mention +- Modify: `.claude/rules/ui.md` — Alerts section +- Modify: `CLAUDE.md` — V17 migration entry + +- [ ] **Step 1: Update `.claude/rules/app-classes.md`** — find the `AlertController` bullet and replace: + +``` +- `AlertController` — `/api/v1/environments/{envSlug}/alerts`. GET list (inbox filtered by userId/groupIds/roleNames via `InAppInboxQuery`; query params: multi-value `state` (PENDING|FIRING|RESOLVED) + `severity`, tri-state `acked`/`read`; always `deleted_at IS NULL`) / GET `/unread-count` / GET `{id}` (404 if deleted) / POST `{id}/ack` (sets acked_at only, no state change) / POST `{id}/read` (sets read_at) / POST `/bulk-read` / POST `/bulk-ack` (VIEWER+) / DELETE `{id}` (OPERATOR+, soft-delete) / POST `/bulk-delete` (OPERATOR+). VIEWER+ for reads and ack/read. Inbox SQL: `? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?` — requires at least one matching target. +``` + +Also drop any `AlertReadRepository` bean mention from the `config/` or `storage/` sub-bullet. + +- [ ] **Step 2: Update `.claude/rules/core-classes.md`** — `AlertState` enum: + +``` +- `AlertState` — enum: PENDING, FIRING, RESOLVED. Ack is orthogonal (`acked_at` on `alert_instances`), not a state. +- `AlertInstanceRepository` — interface: save/findById/findOpenForRule (state IN ('PENDING','FIRING') AND deleted_at IS NULL); `listForInbox(env, groups, user, roles, states, severities, acked, read, limit)`; `countUnreadBySeverity(env, user, groups, roles)`; `ack`/`resolve`/`markSilenced`/`markRead`/`bulkMarkRead`/`softDelete`/`bulkSoftDelete`/`bulkAck`/`deleteResolvedBefore`/`listFiringDueForReNotify`. +``` + +Remove the `AlertReadRepository` bullet entirely. + +- [ ] **Step 3: Update `.claude/rules/ui.md`** — Alerts section: + +``` +- **Sidebar section** (`buildAlertsTreeNodes` in `ui/src/components/sidebar-utils.ts`) — Inbox, Rules, Silences. +- **Routes** in `ui/src/router.tsx`: `/alerts` (redirect to inbox), `/alerts/inbox`, `/alerts/rules`, `/alerts/rules/new`, `/alerts/rules/:id`, `/alerts/silences`. +- **Pages** under `ui/src/pages/Alerts/`: + - `InboxPage.tsx` — single filterable inbox. Filters: severity (multi), state (PENDING/FIRING/RESOLVED, default FIRING), Hide acked (default on), Hide read (default on). Row actions: Acknowledge, Mark read, Silence rule… (quick menu: 1h/8h/24h/Custom), Delete (OPERATOR+, soft-delete). Bulk toolbar: Ack N · Mark N read · Silence rules · Delete N (confirmation modal). + - `RulesListPage.tsx`, `RuleEditor/RuleEditorWizard.tsx` (unchanged). + - `SilencesPage.tsx` — matcher-based create + end-early. Reads `?ruleId=` search param to prefill the Rule ID field (used by InboxPage "Silence rule… → Custom"). + - `SilenceRuleMenu.tsx` (new) — popover duration picker used by row/bulk silence action. +``` + +- [ ] **Step 4: Update `CLAUDE.md`** — append to the V16 line: + +``` +- V17 — Alerts: drop ACKNOWLEDGED from AlertState (ack is now orthogonal via acked_at), add read_at + deleted_at (global, no per-user tracking), drop alert_reads table, rework V13/V15/V16 open-rule unique index predicate to `state IN ('PENDING','FIRING') AND deleted_at IS NULL`. +``` + +- [ ] **Step 5: Commit** + +```bash +git add .claude/rules/app-classes.md .claude/rules/core-classes.md .claude/rules/ui.md CLAUDE.md +git commit -m "docs(alerts): rules + CLAUDE.md — inbox redesign, V17 migration" +``` + +--- + +## Task 16: Final verification + +- [ ] **Step 1: `mvn clean verify`** — full backend build + ITs. Expected: green. +- [ ] **Step 2: `cd ui && npm run build`** — TS strict + bundle. Expected: green. +- [ ] **Step 3: `cd ui && npx vitest run`** — all UI tests. Expected: green. +- [ ] **Step 4: Grep sanity** — `grep -rn "ACKNOWLEDGED\|AlertReadRepository\|alert_reads" cameleer-server-app cameleer-server-core ui/src`. Expected: only this plan file + spec. +- [ ] **Step 5: Manual smoke**: + - `/alerts/inbox` default view shows only unread-firing alerts + - Toggle "Hide acked" OFF — acked rows appear + - Ack a row — it stays visible while "Hide acked" is OFF; disappears when re-toggled ON + - Click "Silence rule… → 1 hour" on a row — success toast, new silence visible on `/alerts/silences` + - Bell badge decreases after marking rows as read + - As VIEWER, Delete button is absent + - As OPERATOR, Delete button works, soft-deletes, confirmation modal on bulk +- [ ] **Step 6: Index refresh** — `npx gitnexus analyze --embeddings` to keep the code graph fresh for future sessions. + +--- + +## Self-review notes + +Covered spec sections: +- ✅ Data model changes → Task 1, 2 +- ✅ V17 migration (enum swap + columns + drop alert_reads + rework index) → Task 1 +- ✅ `AlertStateTransitions` ACKNOWLEDGED removal → Task 3 +- ✅ `AlertInstanceRepository` interface changes → Task 4 +- ✅ `PostgresAlertInstanceRepository` save/rowMapper/findOpenForRule/ack/listForInbox/countUnread/new mutations → Task 5 +- ✅ Drop `AlertReadRepository` + impl + bean → Task 4, 5 +- ✅ `AlertController` DELETE/bulk-delete/bulk-ack + acked/read filters + rewire read → Task 6 +- ✅ `InAppInboxQuery` new signature + countUnread → Task 6 +- ✅ `AlertDto.readAt` → Task 6 +- ✅ Test-fixture migration off ACKNOWLEDGED → Task 7 +- ✅ `SecurityConfig` matchers for new endpoints → Task 7 +- ✅ OpenAPI regen → Task 8 +- ✅ UI hooks: `useDeleteAlert`, `useBulkDeleteAlerts`, `useBulkAckAlerts`, `useAlerts` filter params → Task 9 +- ✅ Delete `AllAlertsPage` + `HistoryPage` + router + sidebar → Task 10 +- ✅ `SilenceRuleMenu` component → Task 11 +- ✅ `InboxPage` rewrite → Task 12 +- ✅ `SilencesPage` `?ruleId=` prefill → Task 13 +- ✅ UI tests → Task 14 +- ✅ Rules + `CLAUDE.md` → Task 15 +- ✅ Final verification → Task 16 From e95c21d0cb66bd6159baa41d5f8400938d5e75f3 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:04:09 +0200 Subject: [PATCH 26/49] =?UTF-8?q?feat(alerts):=20V17=20migration=20?= =?UTF-8?q?=E2=80=94=20drop=20ACKNOWLEDGED,=20add=20read=5Fat=20+=20delete?= =?UTF-8?q?d=5Fat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../V17__alerts_drop_acknowledged.sql | 53 +++++++++++++++++++ .../app/alerting/storage/V17MigrationIT.java | 48 +++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql create mode 100644 cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java diff --git a/cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql b/cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql new file mode 100644 index 00000000..35a9e1dd --- /dev/null +++ b/cameleer-server-app/src/main/resources/db/migration/V17__alerts_drop_acknowledged.sql @@ -0,0 +1,53 @@ +-- V17 — Alerts: drop ACKNOWLEDGED state, add read_at/deleted_at, drop alert_reads, +-- rework open-rule unique index predicate to survive ack (acked no longer "closed"). + +-- 1. Coerce ACKNOWLEDGED rows → FIRING (acked_at already set on these rows) +UPDATE alert_instances SET state = 'FIRING' WHERE state = 'ACKNOWLEDGED'; + +-- 2. Swap alert_state_enum to remove ACKNOWLEDGED (Postgres can't drop enum values in place) +-- First drop all indexes that reference alert_state_enum so ALTER COLUMN can proceed. +DROP INDEX IF EXISTS alert_instances_open_rule_uq; +DROP INDEX IF EXISTS alert_instances_inbox_idx; +DROP INDEX IF EXISTS alert_instances_open_rule_idx; +DROP INDEX IF EXISTS alert_instances_resolved_idx; + +CREATE TYPE alert_state_enum_v2 AS ENUM ('PENDING','FIRING','RESOLVED'); +ALTER TABLE alert_instances + ALTER COLUMN state TYPE alert_state_enum_v2 + USING state::text::alert_state_enum_v2; +DROP TYPE alert_state_enum; +ALTER TYPE alert_state_enum_v2 RENAME TO alert_state_enum; + +-- Recreate the non-unique indexes that were dropped above +CREATE INDEX alert_instances_inbox_idx ON alert_instances (environment_id, state, fired_at DESC); +CREATE INDEX alert_instances_open_rule_idx ON alert_instances (rule_id, state) WHERE rule_id IS NOT NULL; +CREATE INDEX alert_instances_resolved_idx ON alert_instances (resolved_at) WHERE state = 'RESOLVED'; + +-- 3. New orthogonal flag columns +ALTER TABLE alert_instances + ADD COLUMN read_at timestamptz NULL, + ADD COLUMN deleted_at timestamptz NULL; + +CREATE INDEX alert_instances_unread_idx + ON alert_instances (environment_id, read_at) + WHERE read_at IS NULL AND deleted_at IS NULL; + +CREATE INDEX alert_instances_deleted_idx + ON alert_instances (deleted_at) + WHERE deleted_at IS NOT NULL; + +-- 4. Rework the V13/V15/V16 open-rule uniqueness index: +-- - drop ACKNOWLEDGED from the predicate (ack no longer "closes") +-- - add "AND deleted_at IS NULL" so a soft-deleted row frees the slot +DROP INDEX IF EXISTS alert_instances_open_rule_uq; +CREATE UNIQUE INDEX alert_instances_open_rule_uq + ON alert_instances (rule_id, (COALESCE( + context->>'_subjectFingerprint', + context->'exchange'->>'id', + ''))) + WHERE rule_id IS NOT NULL + AND state IN ('PENDING','FIRING') + AND deleted_at IS NULL; + +-- 5. Drop the per-user reads table — read is now global on alert_instances.read_at +DROP TABLE alert_reads; diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java new file mode 100644 index 00000000..cfd0965e --- /dev/null +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V17MigrationIT.java @@ -0,0 +1,48 @@ +package com.cameleer.server.app.alerting.storage; + +import com.cameleer.server.app.AbstractPostgresIT; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class V17MigrationIT extends AbstractPostgresIT { + + @Test + void alert_state_enum_drops_acknowledged() { + var values = jdbcTemplate.queryForList(""" + SELECT unnest(enum_range(NULL::alert_state_enum))::text AS v + """, String.class); + assertThat(values).containsExactlyInAnyOrder("PENDING", "FIRING", "RESOLVED"); + } + + @Test + void read_at_and_deleted_at_columns_exist() { + var cols = jdbcTemplate.queryForList(""" + SELECT column_name FROM information_schema.columns + WHERE table_name = 'alert_instances' + AND column_name IN ('read_at','deleted_at') + """, String.class); + assertThat(cols).containsExactlyInAnyOrder("read_at", "deleted_at"); + } + + @Test + void alert_reads_table_is_gone() { + Integer count = jdbcTemplate.queryForObject(""" + SELECT COUNT(*)::int FROM information_schema.tables + WHERE table_name = 'alert_reads' + """, Integer.class); + assertThat(count).isZero(); + } + + @Test + void open_rule_index_predicate_is_reworked() { + String def = jdbcTemplate.queryForObject(""" + SELECT pg_get_indexdef(indexrelid) + FROM pg_index + JOIN pg_class ON pg_class.oid = pg_index.indexrelid + WHERE pg_class.relname = 'alert_instances_open_rule_uq' + """, String.class); + assertThat(def).contains("state = ANY (ARRAY['PENDING'::alert_state_enum, 'FIRING'::alert_state_enum])"); + assertThat(def).contains("deleted_at IS NULL"); + } +} From 82e82350f99a93f32ffd0c2bb85ee3d14889dc11 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:12:37 +0200 Subject: [PATCH 27/49] refactor(alerts): drop ACKNOWLEDGED from AlertState, add readAt/deletedAt to AlertInstance - AlertState: remove ACKNOWLEDGED case (V17 migration already dropped it from DB enum) - AlertInstance: insert readAt + deletedAt Instant fields after lastNotifiedAt; add withReadAt/withDeletedAt withers; update all existing withers to pass both fields positionally - AlertStateTransitions: add null,null for readAt/deletedAt in newInstance ctor call; collapse FIRING,ACKNOWLEDGED switch arm to just FIRING - AlertScopeTest: update AlertState.values() assertion to 3 values; fix stale ConditionKind.hasSize(6) to 7 (JVM_METRIC was added earlier) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../alerting/eval/AlertStateTransitions.java | 10 +++--- .../server/core/alerting/AlertInstance.java | 34 ++++++++++++++----- .../server/core/alerting/AlertState.java | 2 +- .../server/core/alerting/AlertScopeTest.java | 4 +-- 4 files changed, 34 insertions(+), 16 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java index 1e0297f0..22596e32 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/eval/AlertStateTransitions.java @@ -28,7 +28,7 @@ public final class AlertStateTransitions { /** * Apply an EvalResult to the current open AlertInstance. * - * @param current the open instance for this rule (PENDING / FIRING / ACKNOWLEDGED), or null if none + * @param current the open instance for this rule (PENDING / FIRING), or null if none * @param result the evaluator outcome * @param rule the rule being evaluated * @param now wall-clock instant for the current tick @@ -50,7 +50,7 @@ public final class AlertStateTransitions { private static Optional onClear(AlertInstance current, Instant now) { if (current == null) return Optional.empty(); // no open instance — no-op if (current.state() == AlertState.RESOLVED) return Optional.empty(); // already resolved - // Any open state (PENDING / FIRING / ACKNOWLEDGED) → RESOLVED + // Any open state (PENDING / FIRING) → RESOLVED return Optional.of(current .withState(AlertState.RESOLVED) .withResolvedAt(now)); @@ -84,8 +84,8 @@ public final class AlertStateTransitions { // Still within forDuration — stay PENDING, nothing to persist yield Optional.empty(); } - // FIRING / ACKNOWLEDGED — re-notification cadence handled by the dispatcher - case FIRING, ACKNOWLEDGED -> Optional.empty(); + // FIRING — re-notification cadence handled by the dispatcher + case FIRING -> Optional.empty(); // RESOLVED should never appear as the "current open" instance, but guard anyway case RESOLVED -> Optional.empty(); }; @@ -126,6 +126,8 @@ public final class AlertStateTransitions { null, // ackedBy null, // resolvedAt null, // lastNotifiedAt + null, // readAt + null, // deletedAt false, // silenced f.currentValue(), f.threshold(), diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java index cdc1822b..c2fcfd09 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstance.java @@ -17,6 +17,8 @@ public record AlertInstance( String ackedBy, Instant resolvedAt, Instant lastNotifiedAt, + Instant readAt, // NEW — global "someone has seen this" + Instant deletedAt, // NEW — soft delete boolean silenced, Double currentValue, Double threshold, @@ -39,63 +41,77 @@ public record AlertInstance( public AlertInstance withState(AlertState s) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - s, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + s, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withFiredAt(Instant i) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, i, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, i, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withResolvedAt(Instant i) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, i, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, i, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withAck(String ackedBy, Instant ackedAt) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withSilenced(boolean silenced) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withTitleMessage(String title, String message) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withLastNotifiedAt(Instant instant) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, instant, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, instant, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withContext(Map context) { return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } public AlertInstance withRuleSnapshot(Map snapshot) { return new AlertInstance(id, ruleId, snapshot, environmentId, - state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, silenced, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, deletedAt, silenced, + currentValue, threshold, context, title, message, + targetUserIds, targetGroupIds, targetRoleNames); + } + + public AlertInstance withReadAt(Instant i) { + return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, i, deletedAt, silenced, + currentValue, threshold, context, title, message, + targetUserIds, targetGroupIds, targetRoleNames); + } + + public AlertInstance withDeletedAt(Instant i) { + return new AlertInstance(id, ruleId, ruleSnapshot, environmentId, + state, severity, firedAt, ackedAt, ackedBy, resolvedAt, lastNotifiedAt, readAt, i, silenced, currentValue, threshold, context, title, message, targetUserIds, targetGroupIds, targetRoleNames); } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java index d42d7e03..f3f35feb 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertState.java @@ -1,3 +1,3 @@ package com.cameleer.server.core.alerting; -public enum AlertState { PENDING, FIRING, ACKNOWLEDGED, RESOLVED } +public enum AlertState { PENDING, FIRING, RESOLVED } diff --git a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java index 5713a18a..497a6e22 100644 --- a/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java +++ b/cameleer-server-core/src/test/java/com/cameleer/server/core/alerting/AlertScopeTest.java @@ -23,8 +23,8 @@ class AlertScopeTest { assertThat(AlertSeverity.values()).containsExactly( AlertSeverity.CRITICAL, AlertSeverity.WARNING, AlertSeverity.INFO); assertThat(AlertState.values()).containsExactly( - AlertState.PENDING, AlertState.FIRING, AlertState.ACKNOWLEDGED, AlertState.RESOLVED); - assertThat(ConditionKind.values()).hasSize(6); + AlertState.PENDING, AlertState.FIRING, AlertState.RESOLVED); + assertThat(ConditionKind.values()).hasSize(7); assertThat(TargetKind.values()).containsExactly( TargetKind.USER, TargetKind.GROUP, TargetKind.ROLE); assertThat(NotificationStatus.values()).containsExactly( From 5b1b3f215a95a9c635b3fa694d316a9da640d065 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:28:31 +0200 Subject: [PATCH 28/49] =?UTF-8?q?test(alerts):=20state=20machine=20?= =?UTF-8?q?=E2=80=94=20ack=20is=20orthogonal,=20does=20not=20transition=20?= =?UTF-8?q?FIRING?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AlertStateTransitionsTest: add null,null for readAt/deletedAt in openInstance helper; replace firingWhenAcknowledgedIsNoOp with firing_with_ack_stays_firing_on_next_firing_tick; convert ackedInstanceClearsToResolved to use FIRING+withAck; update section comment. - PostgresAlertInstanceRepository: stub null,null for readAt/deletedAt in rowMapper to unblock compilation (Task 4 will read the actual DB columns). - All other alerting test files: add null,null for readAt/deletedAt to AlertInstance ctor calls so the test source tree compiles; stub ACKNOWLEDGED JSON/state assertions with FIRING + TODO Task 4 comments. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PostgresAlertInstanceRepository.java | 2 ++ .../app/alerting/AlertingFullLifecycleIT.java | 4 ++-- .../alerting/controller/AlertControllerIT.java | 4 ++-- .../AlertNotificationControllerIT.java | 2 +- .../eval/AlertStateTransitionsTest.java | 17 +++++++++++------ .../notify/NotificationContextBuilderTest.java | 2 +- .../notify/NotificationDispatchJobIT.java | 2 +- .../notify/SilenceMatcherServiceTest.java | 6 +++--- .../alerting/notify/WebhookDispatcherIT.java | 2 +- .../PostgresAlertInstanceRepositoryIT.java | 6 +++--- 10 files changed, 27 insertions(+), 20 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index c2cfdd60..1243499d 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -235,6 +235,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository rs.getString("acked_by"), resolvedAt == null ? null : resolvedAt.toInstant(), lastNotifiedAt == null ? null : lastNotifiedAt.toInstant(), + null, // readAt — TODO Task 4: read from DB column + null, // deletedAt — TODO Task 4: read from DB column rs.getBoolean("silenced"), currentValue, threshold, diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java index 2fa3c742..8919d86d 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java @@ -243,11 +243,11 @@ class AlertingFullLifecycleIT extends AbstractPostgresIT { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode body = objectMapper.readTree(resp.getBody()); - assertThat(body.path("state").asText()).isEqualTo("ACKNOWLEDGED"); + assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack no longer changes state // DB state AlertInstance updated = instanceRepo.findById(instanceId).orElseThrow(); - assertThat(updated.state()).isEqualTo(AlertState.ACKNOWLEDGED); + assertThat(updated.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack is orthogonal to state } @Test diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java index 4b866321..b6cf7611 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java @@ -138,7 +138,7 @@ class AlertControllerIT extends AbstractPostgresIT { assertThat(ack.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode body = objectMapper.readTree(ack.getBody()); - assertThat(body.path("state").asText()).isEqualTo("ACKNOWLEDGED"); + assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack is orthogonal to state } @Test @@ -192,7 +192,7 @@ class AlertControllerIT extends AbstractPostgresIT { AlertInstance instance = new AlertInstance( UUID.randomUUID(), null, null, envId, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, false, + Instant.now(), null, null, null, null, null, null, false, 42.0, 1000.0, null, "Test alert", "Something happened", List.of("test-operator"), List.of(), List.of()); return instanceRepo.save(instance); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java index 766af9db..6abbb100 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertNotificationControllerIT.java @@ -175,7 +175,7 @@ class AlertNotificationControllerIT extends AbstractPostgresIT { AlertInstance instance = new AlertInstance( UUID.randomUUID(), null, null, envId, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, false, + Instant.now(), null, null, null, null, null, null, false, 42.0, 1000.0, null, "Test alert", "Something happened", List.of(), List.of(), List.of("OPERATOR")); return instanceRepo.save(instance); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java index 29d07a81..17c3b6c7 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java @@ -34,7 +34,7 @@ class AlertStateTransitionsTest { return new AlertInstance( UUID.randomUUID(), UUID.randomUUID(), Map.of(), UUID.randomUUID(), state, AlertSeverity.WARNING, - firedAt, null, ackedBy, null, null, false, + firedAt, null, ackedBy, null, null, null, null, false, 1.0, null, Map.of(), "title", "msg", List.of(), List.of(), List.of()); } @@ -71,7 +71,8 @@ class AlertStateTransitionsTest { @Test void ackedInstanceClearsToResolved() { - var acked = openInstance(AlertState.ACKNOWLEDGED, NOW.minusSeconds(30), "alice"); + var acked = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null) + .withAck("alice", Instant.parse("2026-04-19T11:55:00Z")); var next = AlertStateTransitions.apply(acked, EvalResult.Clear.INSTANCE, ruleWith(0), NOW); assertThat(next).hasValueSatisfying(i -> { assertThat(i.state()).isEqualTo(AlertState.RESOLVED); @@ -131,7 +132,7 @@ class AlertStateTransitionsTest { } // ------------------------------------------------------------------------- - // Firing branch — already open FIRING / ACKNOWLEDGED + // Firing branch — already open FIRING (with or without ack) // ------------------------------------------------------------------------- @Test @@ -142,9 +143,13 @@ class AlertStateTransitionsTest { } @Test - void firingWhenAcknowledgedIsNoOp() { - var acked = openInstance(AlertState.ACKNOWLEDGED, NOW.minusSeconds(30), "alice"); - var next = AlertStateTransitions.apply(acked, FIRING_RESULT, ruleWith(0), NOW); + void firing_with_ack_stays_firing_on_next_firing_tick() { + // Pre-redesign this was the "ACKNOWLEDGED stays ACK" case. Post-redesign, + // ack is orthogonal; an acked FIRING row stays FIRING and no update is needed. + var current = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null) + .withAck("alice", Instant.parse("2026-04-21T10:00:00Z")); + var next = AlertStateTransitions.apply( + current, new EvalResult.Firing(1.0, null, Map.of()), ruleWith(0), NOW); assertThat(next).isEmpty(); } diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java index 9d33073e..e67c4834 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationContextBuilderTest.java @@ -75,7 +75,7 @@ class NotificationContextBuilderTest { INST_ID, RULE_ID, Map.of(), ENV_ID, AlertState.FIRING, AlertSeverity.CRITICAL, Instant.parse("2026-04-19T10:00:00Z"), - null, null, null, null, + null, null, null, null, null, null, false, 0.95, 0.1, ctx, "Alert fired", "Some message", List.of(), List.of(), List.of() diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationDispatchJobIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationDispatchJobIT.java index b843fc4b..9f96ee49 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationDispatchJobIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/NotificationDispatchJobIT.java @@ -89,7 +89,7 @@ class NotificationDispatchJobIT extends AbstractPostgresIT { instanceRepo.save(new AlertInstance( instanceId, ruleId, Map.of(), envId, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, false, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of(), "title", "msg", List.of(), List.of(), List.of())); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/SilenceMatcherServiceTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/SilenceMatcherServiceTest.java index aed812d7..bc86435a 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/SilenceMatcherServiceTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/SilenceMatcherServiceTest.java @@ -30,7 +30,7 @@ class SilenceMatcherServiceTest { return new AlertInstance( INST_ID, RULE_ID, Map.of(), ENV_ID, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, + Instant.now(), null, null, null, null, null, null, false, 1.5, 1.0, Map.of(), "title", "msg", List.of(), List.of(), List.of() @@ -85,7 +85,7 @@ class SilenceMatcherServiceTest { var inst = new AlertInstance( INST_ID, null, Map.of(), ENV_ID, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of(), "t", "m", List.of(), List.of(), List.of() @@ -99,7 +99,7 @@ class SilenceMatcherServiceTest { var inst = new AlertInstance( INST_ID, null, Map.of(), ENV_ID, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of(), "t", "m", List.of(), List.of(), List.of() diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/WebhookDispatcherIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/WebhookDispatcherIT.java index cd83c44e..b26a4616 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/WebhookDispatcherIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/WebhookDispatcherIT.java @@ -188,7 +188,7 @@ class WebhookDispatcherIT { return new AlertInstance( UUID.randomUUID(), UUID.randomUUID(), Map.of(), UUID.randomUUID(), AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, false, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of(), "Alert", "Message", List.of(), List.of(), List.of()); } diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 35997a9f..877cf26a 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -176,7 +176,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { repo.ack(inst.id(), userId, when); var found = repo.findById(inst.id()).orElseThrow(); - assertThat(found.state()).isEqualTo(AlertState.ACKNOWLEDGED); + assertThat(found.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack no longer changes state assertThat(found.ackedBy()).isEqualTo(userId); assertThat(found.ackedAt()).isNotNull(); } @@ -325,7 +325,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { return new AlertInstance( UUID.randomUUID(), ruleId, Map.of(), envId, AlertState.FIRING, severity, - Instant.now(), null, null, null, null, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of(), "title", "message", userIds, groupIds, roleNames); @@ -341,7 +341,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { return new AlertInstance( UUID.randomUUID(), ruleId, Map.of(), envId, AlertState.FIRING, AlertSeverity.WARNING, - Instant.now(), null, null, null, null, + Instant.now(), null, null, null, null, null, null, false, null, null, Map.of("exchange", Map.of("id", exchangeId)), "title", "message", From 6e8d890442748c37bbae6e68b184ec3616c9c8a1 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:36:02 +0200 Subject: [PATCH 29/49] fix(alerts): remove dead ACKNOWLEDGED enum SQL + TODO comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove SET state='ACKNOWLEDGED' from ack() and the ACKNOWLEDGED predicate from findOpenForRule — both would error after V17. The final ack() + open-rule semantics (idempotent guards, deleted_at) are owned by Task 5; this is just the minimum to stop runtime SQL errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../storage/PostgresAlertInstanceRepository.java | 12 ++++++------ .../server/app/alerting/AlertingFullLifecycleIT.java | 4 ++-- .../app/alerting/controller/AlertControllerIT.java | 2 +- .../app/alerting/eval/AlertStateTransitionsTest.java | 2 -- .../storage/PostgresAlertInstanceRepositoryIT.java | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index 1243499d..63bbb142 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -87,7 +87,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository var list = jdbc.query(""" SELECT * FROM alert_instances WHERE rule_id = ? - AND state IN ('PENDING','FIRING','ACKNOWLEDGED') + AND state IN ('PENDING','FIRING') + AND deleted_at IS NULL LIMIT 1 """, rowMapper(), ruleId); return list.isEmpty() ? Optional.empty() : Optional.of(list.get(0)); @@ -158,9 +159,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository public void ack(UUID id, String userId, Instant when) { jdbc.update(""" UPDATE alert_instances - SET state = 'ACKNOWLEDGED'::alert_state_enum, - acked_at = ?, acked_by = ? - WHERE id = ? + SET acked_at = ?, acked_by = ? + WHERE id = ? AND acked_at IS NULL AND deleted_at IS NULL """, Timestamp.from(when), userId, id); } @@ -235,8 +235,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository rs.getString("acked_by"), resolvedAt == null ? null : resolvedAt.toInstant(), lastNotifiedAt == null ? null : lastNotifiedAt.toInstant(), - null, // readAt — TODO Task 4: read from DB column - null, // deletedAt — TODO Task 4: read from DB column + null, + null, rs.getBoolean("silenced"), currentValue, threshold, diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java index 8919d86d..77e73b5e 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/AlertingFullLifecycleIT.java @@ -243,11 +243,11 @@ class AlertingFullLifecycleIT extends AbstractPostgresIT { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode body = objectMapper.readTree(resp.getBody()); - assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack no longer changes state + assertThat(body.path("state").asText()).isEqualTo("FIRING"); // DB state AlertInstance updated = instanceRepo.findById(instanceId).orElseThrow(); - assertThat(updated.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack is orthogonal to state + assertThat(updated.state()).isEqualTo(AlertState.FIRING); } @Test diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java index b6cf7611..cdfcf478 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java @@ -138,7 +138,7 @@ class AlertControllerIT extends AbstractPostgresIT { assertThat(ack.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode body = objectMapper.readTree(ack.getBody()); - assertThat(body.path("state").asText()).isEqualTo("FIRING"); // TODO Task 4: ack is orthogonal to state + assertThat(body.path("state").asText()).isEqualTo("FIRING"); } @Test diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java index 17c3b6c7..e64205a6 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/eval/AlertStateTransitionsTest.java @@ -144,8 +144,6 @@ class AlertStateTransitionsTest { @Test void firing_with_ack_stays_firing_on_next_firing_tick() { - // Pre-redesign this was the "ACKNOWLEDGED stays ACK" case. Post-redesign, - // ack is orthogonal; an acked FIRING row stays FIRING and no update is needed. var current = openInstance(AlertState.FIRING, NOW.minusSeconds(30), null) .withAck("alice", Instant.parse("2026-04-21T10:00:00Z")); var next = AlertStateTransitions.apply( diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 877cf26a..7abcbc2c 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -176,7 +176,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { repo.ack(inst.id(), userId, when); var found = repo.findById(inst.id()).orElseThrow(); - assertThat(found.state()).isEqualTo(AlertState.FIRING); // TODO Task 4: ack no longer changes state + assertThat(found.state()).isEqualTo(AlertState.FIRING); assertThat(found.ackedBy()).isEqualTo(userId); assertThat(found.ackedAt()).isNotNull(); } From 55b2a0045866a0b0b067e2faec098188867ba2eb Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:38:10 +0200 Subject: [PATCH 30/49] =?UTF-8?q?feat(alerts):=20core=20repo=20=E2=80=94?= =?UTF-8?q?=20filter=20params=20+=20markRead/softDelete/bulkAck/restore;?= =?UTF-8?q?=20drop=20AlertReadRepository?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - listForInbox gains tri-state acked/read filter params - countUnreadBySeverityForUser(envId, userId) → countUnreadBySeverity(envId, userId, groupIds, roleNames) - new methods: markRead, bulkMarkRead, softDelete, bulkSoftDelete, bulkAck, restore - delete AlertReadRepository — read is now global on alert_instances.read_at Co-Authored-By: Claude Opus 4.7 (1M context) --- .../alerting/AlertInstanceRepository.java | 53 +++++++++++++------ .../core/alerting/AlertReadRepository.java | 9 ---- 2 files changed, 37 insertions(+), 25 deletions(-) delete mode 100644 cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java index d2e22278..a01cbfec 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java @@ -7,26 +7,27 @@ import java.util.Optional; import java.util.UUID; public interface AlertInstanceRepository { - AlertInstance save(AlertInstance instance); // upsert by id + AlertInstance save(AlertInstance instance); Optional findById(UUID id); - Optional findOpenForRule(UUID ruleId); // state IN ('PENDING','FIRING','ACKNOWLEDGED') - /** - * Unfiltered inbox listing. Convenience overload that delegates to the filtered - * variant with {@code states}/{@code severities} set to {@code null} (no filter). - */ + /** Open instance for a rule: state IN ('PENDING','FIRING') AND deleted_at IS NULL. */ + Optional findOpenForRule(UUID ruleId); + + /** Unfiltered inbox listing — convenience overload. */ default List listForInbox(UUID environmentId, List userGroupIdFilter, String userId, List userRoleNames, int limit) { - return listForInbox(environmentId, userGroupIdFilter, userId, userRoleNames, null, null, limit); + return listForInbox(environmentId, userGroupIdFilter, userId, userRoleNames, + null, null, null, null, limit); } /** - * Inbox listing with optional state + severity filters. {@code null} or empty lists mean - * "no filter on that field". When both lists are non-empty the row must match at least one - * value from each list (AND between dimensions, OR within). + * Inbox listing with optional filters. {@code null} or empty lists mean no filter. + * {@code acked} and {@code read} are tri-state: {@code null} = no filter, + * {@code TRUE} = only acked/read, {@code FALSE} = only unacked/unread. + * Always excludes soft-deleted rows ({@code deleted_at IS NOT NULL}). */ List listForInbox(UUID environmentId, List userGroupIdFilter, @@ -34,20 +35,40 @@ public interface AlertInstanceRepository { List userRoleNames, List states, List severities, + Boolean acked, + Boolean read, int limit); /** - * Count unread alert instances for the user, grouped by severity. - *

- * Always returns a map with an entry for every {@link AlertSeverity} (value 0 if no rows), - * so callers never need null-checks. Total unread count is the sum of the values. + * Count unread alert instances visible to the user, grouped by severity. + * Visibility: targets user directly, or via one of the given groups/roles. + * "Unread" = {@code read_at IS NULL AND deleted_at IS NULL}. */ - Map countUnreadBySeverityForUser(UUID environmentId, String userId); + Map countUnreadBySeverity(UUID environmentId, + String userId, + List groupIds, + List roleNames); + void ack(UUID id, String userId, Instant when); void resolve(UUID id, Instant when); void markSilenced(UUID id, boolean silenced); void deleteResolvedBefore(Instant cutoff); - /** FIRING instances whose reNotify cadence has elapsed since last notification. */ + /** Set {@code read_at = when} if currently null. Idempotent. */ + void markRead(UUID id, Instant when); + /** Bulk variant — single UPDATE. */ + void bulkMarkRead(List ids, Instant when); + + /** Set {@code deleted_at = when} if currently null. Idempotent. */ + void softDelete(UUID id, Instant when); + /** Bulk variant — single UPDATE. */ + void bulkSoftDelete(List ids, Instant when); + + /** Clear {@code deleted_at}. Undo for soft-delete. Idempotent. */ + void restore(UUID id); + + /** Bulk ack — single UPDATE. Each row gets {@code acked_at=when, acked_by=userId} if unacked. */ + void bulkAck(List ids, String userId, Instant when); + List listFiringDueForReNotify(Instant now); } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java deleted file mode 100644 index a3cd08e4..00000000 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertReadRepository.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.cameleer.server.core.alerting; - -import java.util.List; -import java.util.UUID; - -public interface AlertReadRepository { - void markRead(String userId, UUID alertInstanceId); - void bulkMarkRead(String userId, List alertInstanceIds); -} From da2819332c239ff04e83376b4e5032a16cca103e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:56:06 +0200 Subject: [PATCH 31/49] =?UTF-8?q?feat(alerts):=20Postgres=20repo=20?= =?UTF-8?q?=E2=80=94=20read=5Fat/deleted=5Fat=20columns,=20filter=20params?= =?UTF-8?q?,=20new=20mutations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - save/rowMapper read+write read_at and deleted_at - listForInbox: tri-state acked/read filters; always excludes deleted - countUnreadBySeverity: rewire without alert_reads join, preserve zero-fill - new: markRead/bulkMarkRead/softDelete/bulkSoftDelete/bulkAck/restore - delete PostgresAlertReadRepository + its bean - restore zero-fill Javadoc on interface - mechanical compile-fixes in AlertController, InAppInboxQuery, AlertControllerIT, InAppInboxQueryTest; Task 6 owns the rewrite - PostgresAlertReadRepositoryIT stubbed @Disabled; Task 7 owns migration Co-Authored-By: Claude Opus 4.7 (1M context) --- .../alerting/config/AlertingBeanConfig.java | 10 +- .../alerting/controller/AlertController.java | 12 +- .../app/alerting/notify/InAppInboxQuery.java | 6 +- .../PostgresAlertInstanceRepository.java | 103 ++++++++++++--- .../storage/PostgresAlertReadRepository.java | 35 ----- .../controller/AlertControllerIT.java | 2 - .../alerting/notify/InAppInboxQueryTest.java | 46 ++++--- .../PostgresAlertInstanceRepositoryIT.java | 103 ++++++++++++--- .../PostgresAlertReadRepositoryIT.java | 122 ++---------------- .../alerting/AlertInstanceRepository.java | 2 + 10 files changed, 227 insertions(+), 214 deletions(-) delete mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java index 2902f3ae..b4cc1e6a 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/config/AlertingBeanConfig.java @@ -3,7 +3,10 @@ package com.cameleer.server.app.alerting.config; import com.cameleer.server.app.alerting.eval.PerKindCircuitBreaker; import com.cameleer.server.app.alerting.metrics.AlertingMetrics; import com.cameleer.server.app.alerting.storage.*; -import com.cameleer.server.core.alerting.*; +import com.cameleer.server.core.alerting.AlertInstanceRepository; +import com.cameleer.server.core.alerting.AlertNotificationRepository; +import com.cameleer.server.core.alerting.AlertRuleRepository; +import com.cameleer.server.core.alerting.AlertSilenceRepository; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,11 +44,6 @@ public class AlertingBeanConfig { return new PostgresAlertNotificationRepository(jdbc, om); } - @Bean - public AlertReadRepository alertReadRepository(JdbcTemplate jdbc) { - return new PostgresAlertReadRepository(jdbc); - } - @Bean public Clock alertingClock() { return Clock.systemDefaultZone(); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java index eb65b0d5..69c4fb32 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java @@ -7,7 +7,6 @@ import com.cameleer.server.app.alerting.notify.InAppInboxQuery; import com.cameleer.server.app.web.EnvPath; import com.cameleer.server.core.alerting.AlertInstance; import com.cameleer.server.core.alerting.AlertInstanceRepository; -import com.cameleer.server.core.alerting.AlertReadRepository; import com.cameleer.server.core.alerting.AlertSeverity; import com.cameleer.server.core.alerting.AlertState; import com.cameleer.server.core.runtime.Environment; @@ -43,14 +42,11 @@ public class AlertController { private final InAppInboxQuery inboxQuery; private final AlertInstanceRepository instanceRepo; - private final AlertReadRepository readRepo; public AlertController(InAppInboxQuery inboxQuery, - AlertInstanceRepository instanceRepo, - AlertReadRepository readRepo) { + AlertInstanceRepository instanceRepo) { this.inboxQuery = inboxQuery; this.instanceRepo = instanceRepo; - this.readRepo = readRepo; } @GetMapping @@ -89,14 +85,12 @@ public class AlertController { @PostMapping("/{id}/read") public void read(@EnvPath Environment env, @PathVariable UUID id) { requireInstance(id, env.id()); - String userId = currentUserId(); - readRepo.markRead(userId, id); + instanceRepo.markRead(id, Instant.now()); } @PostMapping("/bulk-read") public void bulkRead(@EnvPath Environment env, @Valid @RequestBody BulkReadRequest req) { - String userId = currentUserId(); // filter to only instances in this env List filtered = req.instanceIds().stream() .filter(instanceId -> instanceRepo.findById(instanceId) @@ -104,7 +98,7 @@ public class AlertController { .orElse(false)) .toList(); if (!filtered.isEmpty()) { - readRepo.bulkMarkRead(userId, filtered); + instanceRepo.bulkMarkRead(filtered, Instant.now()); } } diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java index 88f8906b..c3d0b497 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java @@ -70,7 +70,7 @@ public class InAppInboxQuery { int limit) { List groupIds = resolveGroupIds(userId); List roleNames = resolveRoleNames(userId); - return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, limit); + return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, null, null, limit); } /** @@ -85,7 +85,9 @@ public class InAppInboxQuery { if (cached != null && now.isBefore(cached.expiresAt())) { return cached.response(); } - Map bySeverity = instanceRepo.countUnreadBySeverityForUser(envId, userId); + List groupIds = resolveGroupIds(userId); + List roleNames = resolveRoleNames(userId); + Map bySeverity = instanceRepo.countUnreadBySeverity(envId, userId, groupIds, roleNames); UnreadCountResponse response = UnreadCountResponse.from(bySeverity); memo.put(key, new Entry(response, now.plusMillis(MEMO_TTL_MS))); return response; diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index 63bbb142..e76fab97 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -34,10 +34,12 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository INSERT INTO alert_instances ( id, rule_id, rule_snapshot, environment_id, state, severity, fired_at, acked_at, acked_by, resolved_at, last_notified_at, + read_at, deleted_at, silenced, current_value, threshold, context, title, message, target_user_ids, target_group_ids, target_role_names) VALUES (?, ?, ?::jsonb, ?, ?::alert_state_enum, ?::severity_enum, ?, ?, ?, ?, ?, + ?, ?, ?, ?, ?, ?::jsonb, ?, ?, ?, ?, ?) ON CONFLICT (id) DO UPDATE SET @@ -46,6 +48,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository acked_by = EXCLUDED.acked_by, resolved_at = EXCLUDED.resolved_at, last_notified_at = EXCLUDED.last_notified_at, + read_at = EXCLUDED.read_at, + deleted_at = EXCLUDED.deleted_at, silenced = EXCLUDED.silenced, current_value = EXCLUDED.current_value, threshold = EXCLUDED.threshold, @@ -66,6 +70,7 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository i.environmentId(), i.state().name(), i.severity().name(), ts(i.firedAt()), ts(i.ackedAt()), i.ackedBy(), ts(i.resolvedAt()), ts(i.lastNotifiedAt()), + ts(i.readAt()), ts(i.deletedAt()), i.silenced(), i.currentValue(), i.threshold(), writeJson(i.context()), i.title(), i.message(), userIds, groupIds, roleNames); @@ -101,8 +106,9 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository List userRoleNames, List states, List severities, + Boolean acked, + Boolean read, int limit) { - // Build arrays for group UUIDs and role names Array groupArray = toUuidArrayFromStrings(userGroupIdFilter); Array roleArray = toTextArray(userRoleNames); @@ -127,7 +133,13 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository sql.append(" AND severity::text = ANY(?)"); args.add(severityArray); } - + if (acked != null) { + sql.append(acked ? " AND acked_at IS NOT NULL" : " AND acked_at IS NULL"); + } + if (read != null) { + sql.append(read ? " AND read_at IS NOT NULL" : " AND read_at IS NULL"); + } + sql.append(" AND deleted_at IS NULL"); sql.append(" ORDER BY fired_at DESC LIMIT ?"); args.add(limit); @@ -135,23 +147,30 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository } @Override - public Map countUnreadBySeverityForUser(UUID environmentId, String userId) { + public Map countUnreadBySeverity(UUID environmentId, + String userId, + List groupIds, + List roleNames) { + Array groupArray = toUuidArrayFromStrings(groupIds); + Array roleArray = toTextArray(roleNames); String sql = """ - SELECT ai.severity::text AS severity, COUNT(*) AS cnt - FROM alert_instances ai - WHERE ai.environment_id = ? - AND ? = ANY(ai.target_user_ids) - AND NOT EXISTS ( - SELECT 1 FROM alert_reads ar - WHERE ar.user_id = ? AND ar.alert_instance_id = ai.id + SELECT severity::text AS severity, COUNT(*) AS cnt + FROM alert_instances + WHERE environment_id = ? + AND read_at IS NULL + AND deleted_at IS NULL + AND ( + ? = ANY(target_user_ids) + OR target_group_ids && ? + OR target_role_names && ? ) - GROUP BY ai.severity + GROUP BY severity """; EnumMap counts = new EnumMap<>(AlertSeverity.class); for (AlertSeverity s : AlertSeverity.values()) counts.put(s, 0L); - jdbc.query(sql, rs -> { - counts.put(AlertSeverity.valueOf(rs.getString("severity")), rs.getLong("cnt")); - }, environmentId, userId, userId); + jdbc.query(sql, (org.springframework.jdbc.core.RowCallbackHandler) rs -> counts.put( + AlertSeverity.valueOf(rs.getString("severity")), rs.getLong("cnt") + ), environmentId, userId, groupArray, roleArray); return counts; } @@ -164,6 +183,56 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository """, Timestamp.from(when), userId, id); } + @Override + public void markRead(UUID id, Instant when) { + jdbc.update("UPDATE alert_instances SET read_at = ? WHERE id = ? AND read_at IS NULL", + Timestamp.from(when), id); + } + + @Override + public void bulkMarkRead(List ids, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET read_at = ? + WHERE id = ANY(?) AND read_at IS NULL + """, Timestamp.from(when), idArray); + } + + @Override + public void softDelete(UUID id, Instant when) { + jdbc.update("UPDATE alert_instances SET deleted_at = ? WHERE id = ? AND deleted_at IS NULL", + Timestamp.from(when), id); + } + + @Override + public void bulkSoftDelete(List ids, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET deleted_at = ? + WHERE id = ANY(?) AND deleted_at IS NULL + """, Timestamp.from(when), idArray); + } + + @Override + public void restore(UUID id) { + jdbc.update("UPDATE alert_instances SET deleted_at = NULL WHERE id = ?", id); + } + + @Override + public void bulkAck(List ids, String userId, Instant when) { + if (ids == null || ids.isEmpty()) return; + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + jdbc.update(""" + UPDATE alert_instances SET acked_at = ?, acked_by = ? + WHERE id = ANY(?) AND acked_at IS NULL AND deleted_at IS NULL + """, Timestamp.from(when), userId, idArray); + } + @Override public void resolve(UUID id, Instant when) { jdbc.update(""" @@ -215,6 +284,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository Timestamp ackedAt = rs.getTimestamp("acked_at"); Timestamp resolvedAt = rs.getTimestamp("resolved_at"); Timestamp lastNotifiedAt = rs.getTimestamp("last_notified_at"); + Timestamp readAt = rs.getTimestamp("read_at"); + Timestamp deletedAt = rs.getTimestamp("deleted_at"); Object cvObj = rs.getObject("current_value"); Double currentValue = cvObj == null ? null : ((Number) cvObj).doubleValue(); @@ -235,8 +306,8 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository rs.getString("acked_by"), resolvedAt == null ? null : resolvedAt.toInstant(), lastNotifiedAt == null ? null : lastNotifiedAt.toInstant(), - null, - null, + readAt == null ? null : readAt.toInstant(), + deletedAt == null ? null : deletedAt.toInstant(), rs.getBoolean("silenced"), currentValue, threshold, diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java deleted file mode 100644 index fa6daab4..00000000 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepository.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.cameleer.server.app.alerting.storage; - -import com.cameleer.server.core.alerting.AlertReadRepository; -import org.springframework.jdbc.core.JdbcTemplate; - -import java.util.List; -import java.util.UUID; - -public class PostgresAlertReadRepository implements AlertReadRepository { - - private final JdbcTemplate jdbc; - - public PostgresAlertReadRepository(JdbcTemplate jdbc) { - this.jdbc = jdbc; - } - - @Override - public void markRead(String userId, UUID alertInstanceId) { - jdbc.update(""" - INSERT INTO alert_reads (user_id, alert_instance_id) - VALUES (?, ?) - ON CONFLICT (user_id, alert_instance_id) DO NOTHING - """, userId, alertInstanceId); - } - - @Override - public void bulkMarkRead(String userId, List alertInstanceIds) { - if (alertInstanceIds == null || alertInstanceIds.isEmpty()) { - return; - } - for (UUID id : alertInstanceIds) { - markRead(userId, id); - } - } -} diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java index cdfcf478..5502d669 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java @@ -5,7 +5,6 @@ import com.cameleer.server.app.TestSecurityHelper; import com.cameleer.server.app.search.ClickHouseLogStore; import com.cameleer.server.core.alerting.AlertInstance; import com.cameleer.server.core.alerting.AlertInstanceRepository; -import com.cameleer.server.core.alerting.AlertReadRepository; import com.cameleer.server.core.alerting.AlertSeverity; import com.cameleer.server.core.alerting.AlertState; import com.fasterxml.jackson.databind.JsonNode; @@ -35,7 +34,6 @@ class AlertControllerIT extends AbstractPostgresIT { @Autowired private ObjectMapper objectMapper; @Autowired private TestSecurityHelper securityHelper; @Autowired private AlertInstanceRepository instanceRepo; - @Autowired private AlertReadRepository readRepo; private String operatorJwt; private String viewerJwt; diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java index 24430e35..e0189c80 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java @@ -22,6 +22,8 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicLong; +import org.mockito.ArgumentMatchers; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; @@ -75,13 +77,13 @@ class InAppInboxQueryTest { .thenReturn(List.of(new RoleSummary(roleId, "OPERATOR", true, "direct"))); when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of(groupId.toString())), - eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), eq(20))) + eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), isNull(), isNull(), eq(20))) .thenReturn(List.of()); List result = query.listInbox(ENV_ID, USER_ID, 20); assertThat(result).isEmpty(); verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()), - USER_ID, List.of("OPERATOR"), null, null, 20); + USER_ID, List.of("OPERATOR"), null, null, null, null, 20); } @Test @@ -94,12 +96,12 @@ class InAppInboxQueryTest { List severities = List.of(AlertSeverity.CRITICAL, AlertSeverity.WARNING); when(instanceRepo.listForInbox(eq(ENV_ID), eq(List.of()), eq(USER_ID), eq(List.of()), - eq(states), eq(severities), eq(25))) + eq(states), eq(severities), isNull(), isNull(), eq(25))) .thenReturn(List.of()); query.listInbox(ENV_ID, USER_ID, states, severities, 25); verify(instanceRepo).listForInbox(ENV_ID, List.of(), USER_ID, List.of(), - states, severities, 25); + states, severities, null, null, 25); } // ------------------------------------------------------------------------- @@ -108,7 +110,8 @@ class InAppInboxQueryTest { @Test void countUnread_totalIsSumOfBySeverityValues() { - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(4L, 2L, 1L)); UnreadCountResponse response = query.countUnread(ENV_ID, USER_ID); @@ -123,7 +126,8 @@ class InAppInboxQueryTest { @Test void countUnread_fillsMissingSeveritiesWithZero() { // Repository returns only CRITICAL — WARNING/INFO must default to 0. - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(Map.of(AlertSeverity.CRITICAL, 3L)); UnreadCountResponse response = query.countUnread(ENV_ID, USER_ID); @@ -141,7 +145,8 @@ class InAppInboxQueryTest { @Test void countUnread_secondCallWithin5sUsesCache() { - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(1L, 2L, 2L)); UnreadCountResponse first = query.countUnread(ENV_ID, USER_ID); @@ -150,12 +155,14 @@ class InAppInboxQueryTest { assertThat(first.total()).isEqualTo(5L); assertThat(second.total()).isEqualTo(5L); - verify(instanceRepo, times(1)).countUnreadBySeverityForUser(ENV_ID, USER_ID); + verify(instanceRepo, times(1)).countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any()); } @Test void countUnread_callAfter5sRefreshesCache() { - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(1L, 1L, 1L)) // first call — total 3 .thenReturn(severities(4L, 3L, 2L)); // after TTL — total 9 @@ -165,29 +172,36 @@ class InAppInboxQueryTest { assertThat(first.total()).isEqualTo(3L); assertThat(third.total()).isEqualTo(9L); - verify(instanceRepo, times(2)).countUnreadBySeverityForUser(ENV_ID, USER_ID); + verify(instanceRepo, times(2)).countUnreadBySeverity(eq(ENV_ID), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any()); } @Test void countUnread_differentUsersDontShareCache() { - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, "alice")) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq("alice"), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(0L, 1L, 1L)); - when(instanceRepo.countUnreadBySeverityForUser(ENV_ID, "bob")) + when(instanceRepo.countUnreadBySeverity(eq(ENV_ID), eq("bob"), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(2L, 2L, 4L)); assertThat(query.countUnread(ENV_ID, "alice").total()).isEqualTo(2L); assertThat(query.countUnread(ENV_ID, "bob").total()).isEqualTo(8L); - verify(instanceRepo).countUnreadBySeverityForUser(ENV_ID, "alice"); - verify(instanceRepo).countUnreadBySeverityForUser(ENV_ID, "bob"); + verify(instanceRepo).countUnreadBySeverity(eq(ENV_ID), eq("alice"), + ArgumentMatchers.>any(), ArgumentMatchers.>any()); + verify(instanceRepo).countUnreadBySeverity(eq(ENV_ID), eq("bob"), + ArgumentMatchers.>any(), ArgumentMatchers.>any()); } @Test void countUnread_differentEnvsDontShareCache() { UUID envA = UUID.randomUUID(); UUID envB = UUID.randomUUID(); - when(instanceRepo.countUnreadBySeverityForUser(envA, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(envA), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(0L, 0L, 1L)); - when(instanceRepo.countUnreadBySeverityForUser(envB, USER_ID)) + when(instanceRepo.countUnreadBySeverity(eq(envB), eq(USER_ID), + ArgumentMatchers.>any(), ArgumentMatchers.>any())) .thenReturn(severities(1L, 1L, 2L)); assertThat(query.countUnread(envA, USER_ID).total()).isEqualTo(1L); diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 7abcbc2c..5cf2d946 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -50,7 +50,6 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { @AfterEach void cleanup() { - jdbcTemplate.update("DELETE FROM alert_reads WHERE user_id = ?", userId); jdbcTemplate.update("DELETE FROM alert_notifications WHERE alert_instance_id IN " + "(SELECT id FROM alert_instances WHERE environment_id = ?)", envId); jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id = ?", envId); @@ -92,7 +91,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { repo.save(byRole); // User is member of the group AND has the role - var inbox = repo.listForInbox(envId, List.of(groupId), userId, List.of(roleName), 50); + var inbox = repo.listForInbox(envId, List.of(groupId), userId, List.of(roleName), null, null, null, null, 50); assertThat(inbox).extracting(AlertInstance::id) .containsExactlyInAnyOrder(byUser.id(), byGroup.id(), byRole.id()); } @@ -102,33 +101,30 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { var byUser = newInstance(ruleId, List.of(userId), List.of(), List.of()); repo.save(byUser); - var inbox = repo.listForInbox(envId, List.of(), userId, List.of(), 50); + var inbox = repo.listForInbox(envId, List.of(), userId, List.of(), null, null, null, null, 50); assertThat(inbox).hasSize(1); assertThat(inbox.get(0).id()).isEqualTo(byUser.id()); } @Test - void countUnreadBySeverityForUser_decreasesAfterMarkRead() { + void countUnreadBySeverity_decreasesAfterMarkRead() { var inst = newInstance(ruleId, List.of(userId), List.of(), List.of()); repo.save(inst); - var before = repo.countUnreadBySeverityForUser(envId, userId); + var before = repo.countUnreadBySeverity(envId, userId, List.of(), List.of()); assertThat(before) .containsEntry(AlertSeverity.WARNING, 1L) .containsEntry(AlertSeverity.CRITICAL, 0L) .containsEntry(AlertSeverity.INFO, 0L); - // Insert read record directly (AlertReadRepository not yet wired in this test) - jdbcTemplate.update( - "INSERT INTO alert_reads (user_id, alert_instance_id) VALUES (?, ?) ON CONFLICT DO NOTHING", - userId, inst.id()); + repo.markRead(inst.id(), Instant.now()); - var after = repo.countUnreadBySeverityForUser(envId, userId); + var after = repo.countUnreadBySeverity(envId, userId, List.of(), List.of()); assertThat(after.values()).allMatch(v -> v == 0L); } @Test - void countUnreadBySeverityForUser_groupsBySeverity() { + void countUnreadBySeverity_groupsBySeverity() { // Each open instance needs its own rule to satisfy V13's unique partial index. UUID critRule = seedRuleWithSeverity("crit", AlertSeverity.CRITICAL); UUID warnRule = seedRuleWithSeverity("warn", AlertSeverity.WARNING); @@ -138,7 +134,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { repo.save(newInstance(warnRule, AlertSeverity.WARNING, List.of(userId), List.of(), List.of())); repo.save(newInstance(infoRule, AlertSeverity.INFO, List.of(userId), List.of(), List.of())); - var counts = repo.countUnreadBySeverityForUser(envId, userId); + var counts = repo.countUnreadBySeverity(envId, userId, List.of(), List.of()); assertThat(counts) .containsEntry(AlertSeverity.CRITICAL, 1L) @@ -147,10 +143,10 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { } @Test - void countUnreadBySeverityForUser_emptyMapStillHasAllKeys() { + void countUnreadBySeverity_emptyMapStillHasAllKeys() { // No instances saved — every severity must still be present with value 0 // so callers never deal with null/missing keys. - var counts = repo.countUnreadBySeverityForUser(envId, userId); + var counts = repo.countUnreadBySeverity(envId, userId, List.of(), List.of()); assertThat(counts).hasSize(3); assertThat(counts.values()).allMatch(v -> v == 0L); } @@ -269,7 +265,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { Long count = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM alert_instances " + - " WHERE rule_id = ? AND state IN ('PENDING','FIRING','ACKNOWLEDGED')", + " WHERE rule_id = ? AND state IN ('PENDING','FIRING')", Long.class, ruleId); assertThat(count).isEqualTo(3L); } @@ -293,7 +289,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { Long count = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM alert_instances " + - " WHERE rule_id = ? AND state IN ('PENDING','FIRING','ACKNOWLEDGED')", + " WHERE rule_id = ? AND state IN ('PENDING','FIRING')", Long.class, ruleId); assertThat(count).isEqualTo(1L); } @@ -308,8 +304,83 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { assertThat(repo.findById(inst.id()).orElseThrow().silenced()).isTrue(); } + @Test + void markRead_is_idempotent_and_sets_read_at() { + var inst = insertFreshFiring(); + repo.markRead(inst.id(), Instant.parse("2026-04-21T10:00:00Z")); + repo.markRead(inst.id(), Instant.parse("2026-04-21T11:00:00Z")); // idempotent — no-op + var loaded = repo.findById(inst.id()).orElseThrow(); + assertThat(loaded.readAt()).isEqualTo(Instant.parse("2026-04-21T10:00:00Z")); + } + + @Test + void softDelete_excludes_from_listForInbox() { + var inst = insertFreshFiring(); + repo.softDelete(inst.id(), Instant.parse("2026-04-21T10:00:00Z")); + var rows = repo.listForInbox(envId, List.of(), userId, List.of(), + null, null, null, null, 100); + assertThat(rows).extracting(AlertInstance::id).doesNotContain(inst.id()); + } + + @Test + void findOpenForRule_returns_acked_firing() { + var inst = insertFreshFiring(); + repo.ack(inst.id(), userId, Instant.parse("2026-04-21T10:00:00Z")); + var open = repo.findOpenForRule(inst.ruleId()); + assertThat(open).isPresent(); // ack no longer closes the open slot — state stays FIRING + } + + @Test + void findOpenForRule_skips_soft_deleted() { + var inst = insertFreshFiring(); + repo.softDelete(inst.id(), Instant.now()); + assertThat(repo.findOpenForRule(inst.ruleId())).isEmpty(); + } + + @Test + void bulk_ack_only_touches_unacked_rows() { + var a = insertFreshFiring(); + var b = insertFreshFiring(); + // ack 'a' first with userId; bulkAck should leave 'a' untouched (already acked) + repo.ack(a.id(), userId, Instant.parse("2026-04-21T09:00:00Z")); + repo.bulkAck(List.of(a.id(), b.id()), userId, Instant.parse("2026-04-21T10:00:00Z")); + // a was already acked — acked_at stays at the first timestamp, not updated again + assertThat(repo.findById(a.id()).orElseThrow().ackedBy()).isEqualTo(userId); + assertThat(repo.findById(b.id()).orElseThrow().ackedBy()).isEqualTo(userId); + } + + @Test + void listForInbox_acked_false_hides_acked_rows() { + var a = insertFreshFiring(); + var b = insertFreshFiring(); + repo.ack(a.id(), userId, Instant.now()); + var rows = repo.listForInbox(envId, List.of(), userId, List.of(), + null, null, /*acked*/ false, null, 100); + assertThat(rows).extracting(AlertInstance::id).doesNotContain(a.id()); + assertThat(rows).extracting(AlertInstance::id).contains(b.id()); + } + + @Test + void restore_clears_deleted_at() { + var inst = insertFreshFiring(); + repo.softDelete(inst.id(), Instant.now()); + repo.restore(inst.id()); + var loaded = repo.findById(inst.id()).orElseThrow(); + assertThat(loaded.deletedAt()).isNull(); + var rows = repo.listForInbox(envId, List.of(), userId, List.of(), + null, null, null, null, 100); + assertThat(rows).extracting(AlertInstance::id).contains(inst.id()); + } + // ------------------------------------------------------------------------- + /** Creates and saves a fresh FIRING instance targeted at the test userId with its own rule. */ + private AlertInstance insertFreshFiring() { + UUID freshRuleId = seedRule("fresh-rule"); + var inst = newInstance(freshRuleId, List.of(userId), List.of(), List.of()); + return repo.save(inst); + } + private AlertInstance newInstance(UUID ruleId, List userIds, List groupIds, diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java index 6d8d372f..418c1fb7 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java @@ -1,120 +1,18 @@ package com.cameleer.server.app.alerting.storage; -import com.cameleer.server.app.AbstractPostgresIT; -import com.cameleer.server.app.search.ClickHouseLogStore; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import org.springframework.boot.test.mock.mockito.MockBean; -import java.util.List; -import java.util.UUID; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; - -class PostgresAlertReadRepositoryIT extends AbstractPostgresIT { - - @MockBean(name = "clickHouseLogStore") ClickHouseLogStore clickHouseLogStore; - - private PostgresAlertReadRepository repo; - private UUID envId; - private UUID instanceId1; - private UUID instanceId2; - private UUID instanceId3; - private final String userId = "read-user-" + UUID.randomUUID(); - - @BeforeEach - void setup() { - repo = new PostgresAlertReadRepository(jdbcTemplate); - envId = UUID.randomUUID(); - instanceId1 = UUID.randomUUID(); - instanceId2 = UUID.randomUUID(); - instanceId3 = UUID.randomUUID(); - - jdbcTemplate.update( - "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)", - envId, "test-env-" + UUID.randomUUID(), "Test Env"); - jdbcTemplate.update( - "INSERT INTO users (user_id, provider, email) VALUES ('sys-user', 'local', 'sys@example.com') ON CONFLICT (user_id) DO NOTHING"); - jdbcTemplate.update( - "INSERT INTO users (user_id, provider, email) VALUES (?, 'local', ?) ON CONFLICT (user_id) DO NOTHING", - userId, userId + "@example.com"); - - // Each open alert_instance needs its own rule_id — the alert_instances_open_rule_uq - // partial unique forbids multiple open instances sharing the same rule_id + exchange - // discriminator (V13/V15). Three separate rules let all three instances coexist - // in FIRING state so alert_reads tests can target each one independently. - for (UUID instanceId : List.of(instanceId1, instanceId2, instanceId3)) { - UUID ruleId = UUID.randomUUID(); - jdbcTemplate.update( - "INSERT INTO alert_rules (id, environment_id, name, severity, condition_kind, condition, " + - "notification_title_tmpl, notification_message_tmpl, created_by, updated_by) " + - "VALUES (?, ?, ?, 'WARNING', 'AGENT_STATE', '{}'::jsonb, 't', 'm', 'sys-user', 'sys-user')", - ruleId, envId, "rule-" + instanceId); - jdbcTemplate.update( - "INSERT INTO alert_instances (id, rule_id, rule_snapshot, environment_id, state, severity, " + - "fired_at, context, title, message) VALUES (?, ?, '{}'::jsonb, ?, 'FIRING', 'WARNING', " + - "now(), '{}'::jsonb, 'title', 'msg')", - instanceId, ruleId, envId); - } - } - - @AfterEach - void cleanup() { - jdbcTemplate.update("DELETE FROM alert_reads WHERE user_id = ?", userId); - jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id = ?", envId); - jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", envId); - jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId); - jdbcTemplate.update("DELETE FROM users WHERE user_id = ?", userId); - } +/** + * Placeholder — PostgresAlertReadRepository was deleted in Task 5. + * Task 7 will rewrite this test class to cover the new read/delete mutation methods + * on {@link com.cameleer.server.app.alerting.storage.PostgresAlertInstanceRepository}. + */ +@Disabled("Task 7: rewrite after AlertReadRepository removal") +class PostgresAlertReadRepositoryIT { @Test - void markRead_insertsReadRecord() { - repo.markRead(userId, instanceId1); - - int count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM alert_reads WHERE user_id = ? AND alert_instance_id = ?", - Integer.class, userId, instanceId1); - assertThat(count).isEqualTo(1); - } - - @Test - void markRead_isIdempotent() { - repo.markRead(userId, instanceId1); - // second call should not throw - assertThatCode(() -> repo.markRead(userId, instanceId1)).doesNotThrowAnyException(); - - int count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM alert_reads WHERE user_id = ? AND alert_instance_id = ?", - Integer.class, userId, instanceId1); - assertThat(count).isEqualTo(1); - } - - @Test - void bulkMarkRead_marksMultiple() { - repo.bulkMarkRead(userId, List.of(instanceId1, instanceId2, instanceId3)); - - int count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM alert_reads WHERE user_id = ?", - Integer.class, userId); - assertThat(count).isEqualTo(3); - } - - @Test - void bulkMarkRead_emptyListDoesNotThrow() { - assertThatCode(() -> repo.bulkMarkRead(userId, List.of())).doesNotThrowAnyException(); - } - - @Test - void bulkMarkRead_isIdempotent() { - repo.bulkMarkRead(userId, List.of(instanceId1, instanceId2)); - assertThatCode(() -> repo.bulkMarkRead(userId, List.of(instanceId1, instanceId2))) - .doesNotThrowAnyException(); - - int count = jdbcTemplate.queryForObject( - "SELECT count(*) FROM alert_reads WHERE user_id = ?", - Integer.class, userId); - assertThat(count).isEqualTo(2); + void placeholder() { + // replaced in Task 7 } } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java index a01cbfec..964cffe6 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java @@ -43,6 +43,8 @@ public interface AlertInstanceRepository { * Count unread alert instances visible to the user, grouped by severity. * Visibility: targets user directly, or via one of the given groups/roles. * "Unread" = {@code read_at IS NULL AND deleted_at IS NULL}. + * Always returns a map with an entry for every {@link AlertSeverity} (value 0 if no rows), + * so callers never need null-checks. */ Map countUnreadBySeverity(UUID environmentId, String userId, From e1321a40027a663e85ca8b3deafd92c5834ee4b9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:00:00 +0200 Subject: [PATCH 32/49] chore(alerts): delete orphan PostgresAlertReadRepositoryIT The class under test was removed in da281933; the IT became a @Disabled placeholder. Deleting per no-backwards-compat policy. Read mutation coverage lives in PostgresAlertInstanceRepositoryIT going forward. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../storage/PostgresAlertReadRepositoryIT.java | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java deleted file mode 100644 index 418c1fb7..00000000 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertReadRepositoryIT.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.cameleer.server.app.alerting.storage; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; - -/** - * Placeholder — PostgresAlertReadRepository was deleted in Task 5. - * Task 7 will rewrite this test class to cover the new read/delete mutation methods - * on {@link com.cameleer.server.app.alerting.storage.PostgresAlertInstanceRepository}. - */ -@Disabled("Task 7: rewrite after AlertReadRepository removal") -class PostgresAlertReadRepositoryIT { - - @Test - void placeholder() { - // replaced in Task 7 - } -} From dd2a5536ab9b6a096a538f93080bf86402327d90 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:04:39 +0200 Subject: [PATCH 33/49] test(alerts): rename ack test to reflect state is unchanged Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/alerting/storage/PostgresAlertInstanceRepositoryIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 5cf2d946..6e53a91c 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -164,7 +164,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { } @Test - void ack_setsAckedAtAndState() { + void ack_setsAckedAtAndLeavesStateFiring() { var inst = newInstance(ruleId, List.of(userId), List.of(), List.of()); repo.save(inst); From efd8396045721b4a91e64b99cc9829e3cdde61ea Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:15:16 +0200 Subject: [PATCH 34/49] =?UTF-8?q?feat(alerts):=20controller=20=E2=80=94=20?= =?UTF-8?q?DELETE/bulk-delete/bulk-ack/restore=20+=20acked/read=20filters?= =?UTF-8?q?=20+=20readAt=20on=20DTO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /alerts gains tri-state acked + read query params - new endpoints: DELETE /{id} (soft-delete), POST /bulk-delete, POST /bulk-ack, POST /{id}/restore - requireLiveInstance 404s on soft-deleted rows; restore() reads the row regardless - BulkReadRequest → BulkIdsRequest (shared body for bulk read/ack/delete) - AlertDto gains readAt; deletedAt stays off the wire - InAppInboxQuery.listInbox threads acked/read through to the repo (7-arg, no more null placeholders) - SecurityConfig: new matchers for bulk-ack (VIEWER+), DELETE/bulk-delete/restore (OPERATOR+) - AlertControllerIT: persistence assertions on /read + /bulk-read; full coverage for new endpoints - InAppInboxQueryTest: updated to 7-arg listInbox signature Co-Authored-By: Claude Opus 4.7 (1M context) --- .claude/rules/app-classes.md | 2 +- .../alerting/controller/AlertController.java | 91 ++++-- .../server/app/alerting/dto/AlertDto.java | 4 +- .../app/alerting/dto/BulkIdsRequest.java | 10 + .../app/alerting/dto/BulkReadRequest.java | 12 - .../app/alerting/notify/InAppInboxQuery.java | 22 +- .../server/app/security/SecurityConfig.java | 7 +- .../controller/AlertControllerIT.java | 268 +++++++++++++++++- .../alerting/notify/InAppInboxQueryTest.java | 4 +- 9 files changed, 361 insertions(+), 59 deletions(-) create mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkIdsRequest.java delete mode 100644 cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkReadRequest.java diff --git a/.claude/rules/app-classes.md b/.claude/rules/app-classes.md index e04365e0..95cbd870 100644 --- a/.claude/rules/app-classes.md +++ b/.claude/rules/app-classes.md @@ -66,7 +66,7 @@ Env-scoped read-path controllers (`AlertController`, `AlertRuleController`, `Ale - `AgentMetricsController` — GET `/api/v1/environments/{envSlug}/agents/{agentId}/metrics` (JVM/Camel metrics). Rejects cross-env agents (404) as defence-in-depth. - `DiagramRenderController` — GET `/api/v1/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram` (env-scoped lookup). Also GET `/api/v1/diagrams/{contentHash}/render` (flat — content hashes are globally unique). - `AlertRuleController` — `/api/v1/environments/{envSlug}/alerts/rules`. GET list / POST create / GET `{id}` / PUT `{id}` / DELETE `{id}` / POST `{id}/enable` / POST `{id}/disable` / POST `{id}/render-preview` / POST `{id}/test-evaluate`. OPERATOR+ for mutations, VIEWER+ for reads. CRITICAL: attribute keys in `ExchangeMatchCondition.filter.attributes` are validated at rule-save time against `^[a-zA-Z0-9._-]+$` — they are later inlined into ClickHouse SQL. `AgentLifecycleCondition` is allowlist-only — the `AgentLifecycleEventType` enum (REGISTERED / RE_REGISTERED / DEREGISTERED / WENT_STALE / WENT_DEAD / RECOVERED) plus the record compact ctor (non-empty `eventTypes`, `withinSeconds ≥ 1`) do the validation; custom agent-emitted event types are tracked in backlog issue #145. Webhook validation: verifies `outboundConnectionId` exists and `isAllowedInEnvironment`. Null notification templates default to `""` (NOT NULL constraint). Audit: `ALERT_RULE_CHANGE`. -- `AlertController` — `/api/v1/environments/{envSlug}/alerts`. GET list (inbox filtered by userId/groupIds/roleNames via `InAppInboxQuery`; optional multi-value `state` + `severity` query params push filtering into PostgreSQL via `listForInbox` with `state::text = ANY(?)` / `severity::text = ANY(?)`) / GET `/unread-count` / GET `{id}` / POST `{id}/ack` / POST `{id}/read` / POST `/bulk-read`. VIEWER+ for all. Inbox SQL: `? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?` — requires at least one matching target (no broadcast concept). +- `AlertController` — `/api/v1/environments/{envSlug}/alerts`. GET list (inbox filtered by userId/groupIds/roleNames via `InAppInboxQuery`; optional multi-value `state`, `severity`, tri-state `acked`, tri-state `read` query params; soft-deleted rows always excluded) / GET `/unread-count` / GET `{id}` / POST `{id}/ack` / POST `{id}/read` / POST `/bulk-read` / POST `/bulk-ack` (VIEWER+) / DELETE `{id}` (OPERATOR+, soft-delete) / POST `/bulk-delete` (OPERATOR+) / POST `{id}/restore` (OPERATOR+, clears `deleted_at`). `requireLiveInstance` helper returns 404 on soft-deleted rows; `restore` explicitly fetches regardless of `deleted_at`. `BulkIdsRequest` is the shared body for bulk-read/ack/delete (`{ instanceIds }`). `AlertDto` includes `readAt`; `deletedAt` is intentionally NOT on the wire. Inbox SQL: `? = ANY(target_user_ids) OR target_group_ids && ? OR target_role_names && ?` — requires at least one matching target (no broadcast concept). - `AlertSilenceController` — `/api/v1/environments/{envSlug}/alerts/silences`. GET list / POST create / DELETE `{id}`. 422 if `endsAt <= startsAt`. OPERATOR+ for mutations, VIEWER+ for list. Audit: `ALERT_SILENCE_CHANGE`. - `AlertNotificationController` — Dual-path (no class-level prefix). GET `/api/v1/environments/{envSlug}/alerts/{alertId}/notifications` (VIEWER+); POST `/api/v1/alerts/notifications/{id}/retry` (OPERATOR+, flat — notification IDs globally unique). Retry resets attempts to 0 and sets `nextAttemptAt = now`. diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java index 69c4fb32..2726cf88 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java @@ -1,7 +1,7 @@ package com.cameleer.server.app.alerting.controller; import com.cameleer.server.app.alerting.dto.AlertDto; -import com.cameleer.server.app.alerting.dto.BulkReadRequest; +import com.cameleer.server.app.alerting.dto.BulkIdsRequest; import com.cameleer.server.app.alerting.dto.UnreadCountResponse; import com.cameleer.server.app.alerting.notify.InAppInboxQuery; import com.cameleer.server.app.web.EnvPath; @@ -13,8 +13,10 @@ import com.cameleer.server.core.runtime.Environment; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -30,7 +32,7 @@ import java.util.UUID; /** * REST controller for the in-app alert inbox (env-scoped). - * VIEWER+ can read their own inbox; OPERATOR+ can ack any alert. + * VIEWER+ can read their own inbox; OPERATOR+ can soft-delete and restore alerts. */ @RestController @RequestMapping("/api/v1/environments/{envSlug}/alerts") @@ -38,8 +40,6 @@ import java.util.UUID; @PreAuthorize("hasAnyRole('VIEWER','OPERATOR','ADMIN')") public class AlertController { - private static final int DEFAULT_LIMIT = 50; - private final InAppInboxQuery inboxQuery; private final AlertInstanceRepository instanceRepo; @@ -54,10 +54,12 @@ public class AlertController { @EnvPath Environment env, @RequestParam(defaultValue = "50") int limit, @RequestParam(required = false) List state, - @RequestParam(required = false) List severity) { + @RequestParam(required = false) List severity, + @RequestParam(required = false) Boolean acked, + @RequestParam(required = false) Boolean read) { String userId = currentUserId(); int effectiveLimit = Math.min(limit, 200); - return inboxQuery.listInbox(env.id(), userId, state, severity, effectiveLimit) + return inboxQuery.listInbox(env.id(), userId, state, severity, acked, read, effectiveLimit) .stream().map(AlertDto::from).toList(); } @@ -68,13 +70,13 @@ public class AlertController { @GetMapping("/{id}") public AlertDto get(@EnvPath Environment env, @PathVariable UUID id) { - AlertInstance instance = requireInstance(id, env.id()); + AlertInstance instance = requireLiveInstance(id, env.id()); return AlertDto.from(instance); } @PostMapping("/{id}/ack") public AlertDto ack(@EnvPath Environment env, @PathVariable UUID id) { - AlertInstance instance = requireInstance(id, env.id()); + AlertInstance instance = requireLiveInstance(id, env.id()); String userId = currentUserId(); instanceRepo.ack(id, userId, Instant.now()); // Re-fetch to return fresh state @@ -84,37 +86,76 @@ public class AlertController { @PostMapping("/{id}/read") public void read(@EnvPath Environment env, @PathVariable UUID id) { - requireInstance(id, env.id()); + requireLiveInstance(id, env.id()); instanceRepo.markRead(id, Instant.now()); } @PostMapping("/bulk-read") public void bulkRead(@EnvPath Environment env, - @Valid @RequestBody BulkReadRequest req) { - // filter to only instances in this env - List filtered = req.instanceIds().stream() - .filter(instanceId -> instanceRepo.findById(instanceId) - .map(i -> i.environmentId().equals(env.id())) - .orElse(false)) - .toList(); + @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvLiveIds(req.instanceIds(), env.id()); if (!filtered.isEmpty()) { instanceRepo.bulkMarkRead(filtered, Instant.now()); } } + @PostMapping("/bulk-ack") + public void bulkAck(@EnvPath Environment env, + @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvLiveIds(req.instanceIds(), env.id()); + if (!filtered.isEmpty()) { + instanceRepo.bulkAck(filtered, currentUserId(), Instant.now()); + } + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") + public ResponseEntity delete(@EnvPath Environment env, @PathVariable UUID id) { + requireLiveInstance(id, env.id()); + instanceRepo.softDelete(id, Instant.now()); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/bulk-delete") + @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") + public void bulkDelete(@EnvPath Environment env, + @Valid @RequestBody BulkIdsRequest req) { + List filtered = inEnvLiveIds(req.instanceIds(), env.id()); + if (!filtered.isEmpty()) { + instanceRepo.bulkSoftDelete(filtered, Instant.now()); + } + } + + @PostMapping("/{id}/restore") + @PreAuthorize("hasAnyRole('OPERATOR','ADMIN')") + public ResponseEntity restore(@EnvPath Environment env, @PathVariable UUID id) { + // Unlike requireLiveInstance, restore explicitly targets soft-deleted rows + AlertInstance inst = instanceRepo.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found")); + if (!inst.environmentId().equals(env.id())) + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found in env"); + instanceRepo.restore(id); + return ResponseEntity.noContent().build(); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- - private AlertInstance requireInstance(UUID id, UUID envId) { - AlertInstance instance = instanceRepo.findById(id) - .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, - "Alert not found: " + id)); - if (!instance.environmentId().equals(envId)) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, - "Alert not found in this environment: " + id); - } - return instance; + private AlertInstance requireLiveInstance(UUID id, UUID envId) { + AlertInstance i = instanceRepo.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found")); + if (!i.environmentId().equals(envId) || i.deletedAt() != null) + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Alert not found in env"); + return i; + } + + private List inEnvLiveIds(List ids, UUID envId) { + return ids.stream() + .filter(id -> instanceRepo.findById(id) + .map(i -> i.environmentId().equals(envId) && i.deletedAt() == null) + .orElse(false)) + .toList(); } private String currentUserId() { diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java index 1ddfb514..a1dfc57f 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/AlertDto.java @@ -20,6 +20,7 @@ public record AlertDto( Instant ackedAt, String ackedBy, Instant resolvedAt, + Instant readAt, // global "has anyone read this" boolean silenced, Double currentValue, Double threshold, @@ -29,6 +30,7 @@ public record AlertDto( return new AlertDto( i.id(), i.ruleId(), i.environmentId(), i.state(), i.severity(), i.title(), i.message(), i.firedAt(), i.ackedAt(), i.ackedBy(), - i.resolvedAt(), i.silenced(), i.currentValue(), i.threshold(), i.context()); + i.resolvedAt(), i.readAt(), i.silenced(), + i.currentValue(), i.threshold(), i.context()); } } diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkIdsRequest.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkIdsRequest.java new file mode 100644 index 00000000..280faaf7 --- /dev/null +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkIdsRequest.java @@ -0,0 +1,10 @@ +package com.cameleer.server.app.alerting.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +import java.util.List; +import java.util.UUID; + +/** Shared body for bulk-read / bulk-ack / bulk-delete requests. */ +public record BulkIdsRequest(@NotNull @Size(min = 1, max = 500) List instanceIds) {} diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkReadRequest.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkReadRequest.java deleted file mode 100644 index fa2dca1e..00000000 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/dto/BulkReadRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.cameleer.server.app.alerting.dto; - -import jakarta.validation.constraints.NotNull; - -import java.util.List; -import java.util.UUID; - -public record BulkReadRequest(@NotNull List instanceIds) { - public BulkReadRequest { - instanceIds = instanceIds == null ? List.of() : List.copyOf(instanceIds); - } -} diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java index c3d0b497..ba528fdf 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/notify/InAppInboxQuery.java @@ -49,28 +49,22 @@ public class InAppInboxQuery { } /** - * Returns the most recent {@code limit} alert instances visible to the given user. - *

- * Visibility: the instance must target this user directly, or target a group the user belongs to, - * or target a role the user holds. Empty target lists mean "broadcast to all". - */ - public List listInbox(UUID envId, String userId, int limit) { - return listInbox(envId, userId, null, null, limit); - } - - /** - * Filtered variant of {@link #listInbox(UUID, String, int)}: optional {@code states} - * and {@code severities} narrow the result set. {@code null} or empty lists mean - * "no filter on that dimension". + * Full filtered variant: optional {@code states}, {@code severities}, {@code acked}, + * and {@code read} narrow the result set. {@code null} or empty lists mean + * "no filter on that dimension". {@code acked}/{@code read} are tri-state: + * {@code null} = no filter, {@code TRUE} = only acked/read, {@code FALSE} = only unacked/unread. */ public List listInbox(UUID envId, String userId, List states, List severities, + Boolean acked, + Boolean read, int limit) { List groupIds = resolveGroupIds(userId); List roleNames = resolveRoleNames(userId); - return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, states, severities, null, null, limit); + return instanceRepo.listForInbox(envId, groupIds, userId, roleNames, + states, severities, acked, read, limit); } /** diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java index c72f727d..afb2f453 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/security/SecurityConfig.java @@ -171,10 +171,15 @@ public class SecurityConfig { .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN") .requestMatchers(HttpMethod.PUT, "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN") .requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/silences/**").hasAnyRole("OPERATOR", "ADMIN") - // Alerting — ack/read (VIEWER+ self-service) + // Alerting — ack/read/bulk-ack (VIEWER+ self-service) .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/*/ack").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/*/read").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/bulk-read").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") + .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/bulk-ack").hasAnyRole("VIEWER", "OPERATOR", "ADMIN") + // Alerting — soft-delete / restore (OPERATOR+) + .requestMatchers(HttpMethod.DELETE, "/api/v1/environments/*/alerts/*").hasAnyRole("OPERATOR", "ADMIN") + .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/bulk-delete").hasAnyRole("OPERATOR", "ADMIN") + .requestMatchers(HttpMethod.POST, "/api/v1/environments/*/alerts/*/restore").hasAnyRole("OPERATOR", "ADMIN") // Alerting — notification retry (flat path; notification IDs globally unique) .requestMatchers(HttpMethod.POST, "/api/v1/alerts/notifications/*/retry").hasAnyRole("OPERATOR", "ADMIN") diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java index 5502d669..73925fc5 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java @@ -69,6 +69,10 @@ class AlertControllerIT extends AbstractPostgresIT { jdbcTemplate.update("DELETE FROM users WHERE user_id IN ('test-operator','test-viewer')"); } + // ------------------------------------------------------------------------- + // Existing tests (baseline) + // ------------------------------------------------------------------------- + @Test void listReturnsAlertsForEnv() throws Exception { AlertInstance instance = seedInstance(envIdA); @@ -82,7 +86,6 @@ class AlertControllerIT extends AbstractPostgresIT { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); JsonNode body = objectMapper.readTree(resp.getBody()); assertThat(body.isArray()).isTrue(); - // The alert we seeded should be present boolean found = false; for (JsonNode node : body) { if (node.path("id").asText().equals(instance.id().toString())) { @@ -95,10 +98,8 @@ class AlertControllerIT extends AbstractPostgresIT { @Test void envIsolation() throws Exception { - // Seed an alert in env-A AlertInstance instanceA = seedInstance(envIdA); - // env-B inbox should NOT see env-A's alert ResponseEntity resp = restTemplate.exchange( "/api/v1/environments/" + envSlugB + "/alerts", HttpMethod.GET, @@ -150,6 +151,10 @@ class AlertControllerIT extends AbstractPostgresIT { String.class); assertThat(read.getStatusCode()).isEqualTo(HttpStatus.OK); + + // Verify persistence — readAt must now be set + AlertInstance updated = instanceRepo.findById(instance.id()).orElseThrow(); + assertThat(updated.readAt()).as("readAt must be set after /read").isNotNull(); } @Test @@ -168,6 +173,12 @@ class AlertControllerIT extends AbstractPostgresIT { String.class); assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + + // Verify persistence — both must have readAt set + assertThat(instanceRepo.findById(i1.id()).orElseThrow().readAt()) + .as("i1 readAt must be set after bulk-read").isNotNull(); + assertThat(instanceRepo.findById(i2.id()).orElseThrow().readAt()) + .as("i2 readAt must be set after bulk-read").isNotNull(); } @Test @@ -180,6 +191,257 @@ class AlertControllerIT extends AbstractPostgresIT { assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); } + // ------------------------------------------------------------------------- + // New endpoint tests + // ------------------------------------------------------------------------- + + @Test + void delete_softDeletes_and_subsequent_get_returns_404() { + AlertInstance instance = seedInstance(envIdA); + + // OPERATOR deletes + ResponseEntity del = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(), + HttpMethod.DELETE, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(del.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + // Subsequent GET returns 404 + ResponseEntity get = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(), + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(get.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void delete_non_operator_returns_403() { + AlertInstance instance = seedInstance(envIdA); + + ResponseEntity del = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(), + HttpMethod.DELETE, + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), + String.class); + assertThat(del.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void bulkDelete_only_affects_matching_env() { + AlertInstance inEnvA = seedInstance(envIdA); + AlertInstance inEnvA2 = seedInstance(envIdA); + AlertInstance inEnvB = seedInstance(envIdB); + + String body = """ + {"instanceIds":["%s","%s","%s"]} + """.formatted(inEnvA.id(), inEnvA2.id(), inEnvB.id()); + + ResponseEntity resp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/bulk-delete", + HttpMethod.POST, + new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)), + String.class); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + + // env-A alerts should be soft-deleted + assertThat(instanceRepo.findById(inEnvA.id()).orElseThrow().deletedAt()) + .as("inEnvA should be soft-deleted").isNotNull(); + assertThat(instanceRepo.findById(inEnvA2.id()).orElseThrow().deletedAt()) + .as("inEnvA2 should be soft-deleted").isNotNull(); + + // env-B alert must NOT be soft-deleted + assertThat(instanceRepo.findById(inEnvB.id()).orElseThrow().deletedAt()) + .as("inEnvB must not be soft-deleted via env-A bulk-delete").isNull(); + } + + @Test + void bulkAck_only_touches_unacked_rows() { + AlertInstance i1 = seedInstance(envIdA); + AlertInstance i2 = seedInstance(envIdA); + + // Pre-ack i1 with an existing user (must be in users table due to FK) + instanceRepo.ack(i1.id(), "test-viewer", Instant.now().minusSeconds(60)); + + String body = """ + {"instanceIds":["%s","%s"]} + """.formatted(i1.id(), i2.id()); + + ResponseEntity resp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/bulk-ack", + HttpMethod.POST, + new HttpEntity<>(body, securityHelper.authHeaders(operatorJwt)), + String.class); + assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.OK); + + // i1's ackedBy must remain "test-viewer" (bulk-ack skips already-acked rows) + AlertInstance refreshed1 = instanceRepo.findById(i1.id()).orElseThrow(); + assertThat(refreshed1.ackedBy()).as("previously-acked row must keep original ackedBy").isEqualTo("test-viewer"); + + // i2 must now be acked + AlertInstance refreshed2 = instanceRepo.findById(i2.id()).orElseThrow(); + assertThat(refreshed2.ackedAt()).as("i2 must be acked after bulk-ack").isNotNull(); + } + + @Test + void restore_clears_deleted_at_and_reappears_in_inbox() throws Exception { + AlertInstance instance = seedInstance(envIdA); + + // Soft-delete first + restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(), + HttpMethod.DELETE, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(instanceRepo.findById(instance.id()).orElseThrow().deletedAt()).isNotNull(); + + // Restore + ResponseEntity restoreResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/restore", + HttpMethod.POST, + new HttpEntity<>(securityHelper.authHeaders(operatorJwt)), + String.class); + assertThat(restoreResp.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + + // deletedAt must be cleared + assertThat(instanceRepo.findById(instance.id()).orElseThrow().deletedAt()) + .as("deletedAt must be null after restore").isNull(); + + // Alert reappears in inbox list + ResponseEntity listResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode body = objectMapper.readTree(listResp.getBody()); + boolean found = false; + for (JsonNode node : body) { + if (node.path("id").asText().equals(instance.id().toString())) { + found = true; + break; + } + } + assertThat(found).as("restored alert must reappear in inbox").isTrue(); + } + + @Test + void list_respects_acked_filter_tristate() throws Exception { + AlertInstance unacked = seedInstance(envIdA); + AlertInstance acked = seedInstance(envIdA); + instanceRepo.ack(acked.id(), "test-operator", Instant.now()); + + // ?acked=false — only unacked + ResponseEntity falseResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts?acked=false", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(falseResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode falseBody = objectMapper.readTree(falseResp.getBody()); + boolean unackedFound = false, ackedFoundInFalse = false; + for (JsonNode node : falseBody) { + String id = node.path("id").asText(); + if (id.equals(unacked.id().toString())) unackedFound = true; + if (id.equals(acked.id().toString())) ackedFoundInFalse = true; + } + assertThat(unackedFound).as("unacked alert must appear with ?acked=false").isTrue(); + assertThat(ackedFoundInFalse).as("acked alert must NOT appear with ?acked=false").isFalse(); + + // ?acked=true — only acked + ResponseEntity trueResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts?acked=true", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(trueResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode trueBody = objectMapper.readTree(trueResp.getBody()); + boolean ackedFound = false, unackedFoundInTrue = false; + for (JsonNode node : trueBody) { + String id = node.path("id").asText(); + if (id.equals(acked.id().toString())) ackedFound = true; + if (id.equals(unacked.id().toString())) unackedFoundInTrue = true; + } + assertThat(ackedFound).as("acked alert must appear with ?acked=true").isTrue(); + assertThat(unackedFoundInTrue).as("unacked alert must NOT appear with ?acked=true").isFalse(); + + // no param — both visible + ResponseEntity allResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(allResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode allBody = objectMapper.readTree(allResp.getBody()); + boolean bothUnacked = false, bothAcked = false; + for (JsonNode node : allBody) { + String id = node.path("id").asText(); + if (id.equals(unacked.id().toString())) bothUnacked = true; + if (id.equals(acked.id().toString())) bothAcked = true; + } + assertThat(bothUnacked).as("unacked must appear with no acked filter").isTrue(); + assertThat(bothAcked).as("acked must appear with no acked filter").isTrue(); + } + + @Test + void list_respects_read_filter_tristate() throws Exception { + AlertInstance unread = seedInstance(envIdA); + AlertInstance read = seedInstance(envIdA); + instanceRepo.markRead(read.id(), Instant.now()); + + // ?read=false — only unread + ResponseEntity falseResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts?read=false", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(falseResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode falseBody = objectMapper.readTree(falseResp.getBody()); + boolean unreadFound = false, readFoundInFalse = false; + for (JsonNode node : falseBody) { + String id = node.path("id").asText(); + if (id.equals(unread.id().toString())) unreadFound = true; + if (id.equals(read.id().toString())) readFoundInFalse = true; + } + assertThat(unreadFound).as("unread alert must appear with ?read=false").isTrue(); + assertThat(readFoundInFalse).as("read alert must NOT appear with ?read=false").isFalse(); + + // ?read=true — only read + ResponseEntity trueResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts?read=true", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(trueResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode trueBody = objectMapper.readTree(trueResp.getBody()); + boolean readFound = false, unreadFoundInTrue = false; + for (JsonNode node : trueBody) { + String id = node.path("id").asText(); + if (id.equals(read.id().toString())) readFound = true; + if (id.equals(unread.id().toString())) unreadFoundInTrue = true; + } + assertThat(readFound).as("read alert must appear with ?read=true").isTrue(); + assertThat(unreadFoundInTrue).as("unread alert must NOT appear with ?read=true").isFalse(); + + // no param — both visible + ResponseEntity allResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(operatorJwt)), + String.class); + assertThat(allResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode allBody = objectMapper.readTree(allResp.getBody()); + boolean bothUnread = false, bothRead = false; + for (JsonNode node : allBody) { + String id = node.path("id").asText(); + if (id.equals(unread.id().toString())) bothUnread = true; + if (id.equals(read.id().toString())) bothRead = true; + } + assertThat(bothUnread).as("unread must appear with no read filter").isTrue(); + assertThat(bothRead).as("read must appear with no read filter").isTrue(); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java index e0189c80..166d74fe 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/notify/InAppInboxQueryTest.java @@ -80,7 +80,7 @@ class InAppInboxQueryTest { eq(USER_ID), eq(List.of("OPERATOR")), isNull(), isNull(), isNull(), isNull(), eq(20))) .thenReturn(List.of()); - List result = query.listInbox(ENV_ID, USER_ID, 20); + List result = query.listInbox(ENV_ID, USER_ID, null, null, null, null, 20); assertThat(result).isEmpty(); verify(instanceRepo).listForInbox(ENV_ID, List.of(groupId.toString()), USER_ID, List.of("OPERATOR"), null, null, null, null, 20); @@ -99,7 +99,7 @@ class InAppInboxQueryTest { eq(states), eq(severities), isNull(), isNull(), eq(25))) .thenReturn(List.of()); - query.listInbox(ENV_ID, USER_ID, states, severities, 25); + query.listInbox(ENV_ID, USER_ID, states, severities, null, null, 25); verify(instanceRepo).listForInbox(ENV_ID, List.of(), USER_ID, List.of(), states, severities, null, null, 25); } From c70fa130ab678e317d2840c969bc20805126d4bf Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:20:21 +0200 Subject: [PATCH 35/49] =?UTF-8?q?test(alerts):=20cover=20global=20read=20?= =?UTF-8?q?=E2=80=94=20one=20user=20marks=20read,=20others=20see=20readAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../controller/AlertControllerIT.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java index 73925fc5..742ba47b 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/controller/AlertControllerIT.java @@ -442,6 +442,62 @@ class AlertControllerIT extends AbstractPostgresIT { assertThat(bothRead).as("read must appear with no read filter").isTrue(); } + @Test + void read_is_global_other_users_see_readAt_set() throws Exception { + // Seed an alert targeting BOTH users so the viewer's GET /{id} is visible + AlertInstance instance = new AlertInstance( + UUID.randomUUID(), null, null, envIdA, + AlertState.FIRING, AlertSeverity.WARNING, + Instant.now(), null, null, null, null, null, null, false, + 42.0, 1000.0, null, "Global read test", "Operator reads, viewer sees it", + List.of("test-operator", "test-viewer"), List.of(), List.of()); + instance = instanceRepo.save(instance); + + // Operator (user A) marks the alert as read + ResponseEntity readResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id() + "/read", + HttpMethod.POST, + new HttpEntity<>(securityHelper.authHeaders(operatorJwt)), + String.class); + assertThat(readResp.getStatusCode()).isEqualTo(HttpStatus.OK); + + // Viewer (user B) fetches the same alert and must see readAt != null + ResponseEntity getResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts/" + instance.id(), + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), + String.class); + assertThat(getResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode node = objectMapper.readTree(getResp.getBody()); + assertThat(node.path("readAt").isNull()) + .as("viewer must see readAt as non-null after operator marked read") + .isFalse(); + assertThat(node.path("readAt").isMissingNode()) + .as("readAt field must be present in response") + .isFalse(); + + // Viewer's list endpoint must also show the alert with readAt set + ResponseEntity listResp = restTemplate.exchange( + "/api/v1/environments/" + envSlugA + "/alerts", + HttpMethod.GET, + new HttpEntity<>(securityHelper.authHeadersNoBody(viewerJwt)), + String.class); + assertThat(listResp.getStatusCode()).isEqualTo(HttpStatus.OK); + JsonNode listBody = objectMapper.readTree(listResp.getBody()); + UUID instanceId = instance.id(); + boolean foundWithReadAt = false; + for (JsonNode item : listBody) { + if (item.path("id").asText().equals(instanceId.toString())) { + assertThat(item.path("readAt").isNull()) + .as("list entry readAt must be non-null for viewer after global read") + .isFalse(); + foundWithReadAt = true; + break; + } + } + assertThat(foundWithReadAt).as("alert must appear in viewer's list with readAt set").isTrue(); + } + // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- From 99b739d946b7e466cabec14617b65c9915b4e4ab Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:48:57 +0200 Subject: [PATCH 36/49] fix(alerts): backend hardening + complete ACKNOWLEDGED migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new AlertInstanceRepository.filterInEnvLive(ids, env): single-query bulk ID validation - AlertController.inEnvLiveIds now one SQL round-trip instead of N - bulkMarkRead SQL: defense-in-depth AND deleted_at IS NULL - bulkAck SQL already had deleted_at IS NULL guard — no change needed - PostgresAlertInstanceRepositoryIT: add filterInEnvLive_excludes_other_env_and_soft_deleted - V12MigrationIT: remove alert_reads assertion (table dropped by V17) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/alerting/controller/AlertController.java | 6 +----- .../storage/PostgresAlertInstanceRepository.java | 13 ++++++++++++- .../storage/PostgresAlertInstanceRepositoryIT.java | 12 ++++++++++++ .../server/app/alerting/storage/V12MigrationIT.java | 5 +++-- .../core/alerting/AlertInstanceRepository.java | 6 ++++++ 5 files changed, 34 insertions(+), 8 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java index 2726cf88..54a79afb 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/controller/AlertController.java @@ -151,11 +151,7 @@ public class AlertController { } private List inEnvLiveIds(List ids, UUID envId) { - return ids.stream() - .filter(id -> instanceRepo.findById(id) - .map(i -> i.environmentId().equals(envId) && i.deletedAt() == null) - .orElse(false)) - .toList(); + return instanceRepo.filterInEnvLive(ids, envId); } private String currentUserId() { diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java index e76fab97..395296dc 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepository.java @@ -196,7 +196,7 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository c.createArrayOf("uuid", ids.toArray())); jdbc.update(""" UPDATE alert_instances SET read_at = ? - WHERE id = ANY(?) AND read_at IS NULL + WHERE id = ANY(?) AND read_at IS NULL AND deleted_at IS NULL """, Timestamp.from(when), idArray); } @@ -262,6 +262,17 @@ public class PostgresAlertInstanceRepository implements AlertInstanceRepository """, rowMapper(), Timestamp.from(now)); } + @Override + public List filterInEnvLive(List ids, UUID environmentId) { + if (ids == null || ids.isEmpty()) return List.of(); + Array idArray = jdbc.execute((ConnectionCallback) c -> + c.createArrayOf("uuid", ids.toArray())); + return jdbc.query(""" + SELECT id FROM alert_instances + WHERE id = ANY(?) AND environment_id = ? AND deleted_at IS NULL + """, (rs, i) -> (UUID) rs.getObject("id"), idArray, environmentId); + } + @Override public void deleteResolvedBefore(Instant cutoff) { jdbc.update(""" diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 6e53a91c..5ee591a0 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -372,6 +372,18 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { assertThat(rows).extracting(AlertInstance::id).contains(inst.id()); } + @Test + void filterInEnvLive_excludes_other_env_and_soft_deleted() { + var a = insertFreshFiring(); // env envId, live + var b = insertFreshFiring(); // env envId, will be soft-deleted + repo.softDelete(b.id(), Instant.now()); + + UUID unknownId = UUID.randomUUID(); // not in DB at all + + var kept = repo.filterInEnvLive(List.of(a.id(), b.id(), unknownId), envId); + assertThat(kept).containsExactly(a.id()); + } + // ------------------------------------------------------------------------- /** Creates and saves a fresh FIRING instance targeted at the test userId with its own rule. */ diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java index 5f59e421..a6d5bcd7 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/V12MigrationIT.java @@ -22,14 +22,15 @@ class V12MigrationIT extends AbstractPostgresIT { @Test void allAlertingTablesAndEnumsExist() { + // Note: alert_reads was created in V12 but dropped by V17 (superseded by read_at column). var tables = jdbcTemplate.queryForList( "SELECT table_name FROM information_schema.tables WHERE table_schema='public' " + "AND table_name IN ('alert_rules','alert_rule_targets','alert_instances'," + - "'alert_silences','alert_notifications','alert_reads')", + "'alert_silences','alert_notifications')", String.class); assertThat(tables).containsExactlyInAnyOrder( "alert_rules","alert_rule_targets","alert_instances", - "alert_silences","alert_notifications","alert_reads"); + "alert_silences","alert_notifications"); var enums = jdbcTemplate.queryForList( "SELECT typname FROM pg_type WHERE typname IN " + diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java index 964cffe6..2522b3b4 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/alerting/AlertInstanceRepository.java @@ -73,4 +73,10 @@ public interface AlertInstanceRepository { void bulkAck(List ids, String userId, Instant when); List listFiringDueForReNotify(Instant now); + + /** + * Filter the given IDs to those that exist in the given environment and are not + * soft-deleted. Single SQL round-trip — avoids N+1 in bulk operations. + */ + List filterInEnvLive(List ids, UUID environmentId); } From 69fe80353ceca149a4d130d88c57ede976274081 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:55:12 +0200 Subject: [PATCH 37/49] =?UTF-8?q?test(alerts):=20close=20repo=20IT=20gaps?= =?UTF-8?q?=20=E2=80=94=20filterInEnvLive=20other-env=20+=20bulkMarkRead?= =?UTF-8?q?=20soft-delete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- .../PostgresAlertInstanceRepositoryIT.java | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java index 5ee591a0..b2a221a6 100644 --- a/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java +++ b/cameleer-server-app/src/test/java/com/cameleer/server/app/alerting/storage/PostgresAlertInstanceRepositoryIT.java @@ -22,6 +22,7 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { private PostgresAlertInstanceRepository repo; private UUID envId; + private UUID otherEnvId; private UUID ruleId; private final String userId = "inbox-user-" + UUID.randomUUID(); private final String groupId = UUID.randomUUID().toString(); @@ -31,11 +32,15 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { void setup() { repo = new PostgresAlertInstanceRepository(jdbcTemplate, new ObjectMapper()); envId = UUID.randomUUID(); + otherEnvId = UUID.randomUUID(); ruleId = UUID.randomUUID(); jdbcTemplate.update( "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)", envId, "test-env-" + UUID.randomUUID(), "Test Env"); + jdbcTemplate.update( + "INSERT INTO environments (id, slug, display_name) VALUES (?, ?, ?)", + otherEnvId, "other-env-" + UUID.randomUUID(), "Other Env"); jdbcTemplate.update( "INSERT INTO users (user_id, provider, email) VALUES (?, 'local', ?) ON CONFLICT (user_id) DO NOTHING", userId, userId + "@example.com"); @@ -52,9 +57,14 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { void cleanup() { jdbcTemplate.update("DELETE FROM alert_notifications WHERE alert_instance_id IN " + "(SELECT id FROM alert_instances WHERE environment_id = ?)", envId); + jdbcTemplate.update("DELETE FROM alert_notifications WHERE alert_instance_id IN " + + "(SELECT id FROM alert_instances WHERE environment_id = ?)", otherEnvId); jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id = ?", envId); + jdbcTemplate.update("DELETE FROM alert_instances WHERE environment_id = ?", otherEnvId); jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", envId); + jdbcTemplate.update("DELETE FROM alert_rules WHERE environment_id = ?", otherEnvId); jdbcTemplate.update("DELETE FROM environments WHERE id = ?", envId); + jdbcTemplate.update("DELETE FROM environments WHERE id = ?", otherEnvId); jdbcTemplate.update("DELETE FROM users WHERE user_id = ?", userId); } @@ -380,8 +390,34 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { UUID unknownId = UUID.randomUUID(); // not in DB at all - var kept = repo.filterInEnvLive(List.of(a.id(), b.id(), unknownId), envId); + // Insert a rule + instance in the second environment (otherEnvId) to prove + // that the SQL env-filter actually excludes rows from a different environment. + UUID otherRuleId = seedRuleInEnv("other-rule", otherEnvId); + var otherEnvInst = newInstanceInEnv(otherRuleId, otherEnvId, List.of(userId), List.of(), List.of()); + repo.save(otherEnvInst); + + var kept = repo.filterInEnvLive(List.of(a.id(), b.id(), unknownId, otherEnvInst.id()), envId); assertThat(kept).containsExactly(a.id()); + assertThat(kept).doesNotContain(otherEnvInst.id()); + } + + @Test + void bulkMarkRead_respects_deleted_at() { + var live = insertFreshFiring(); + // second instance — need a fresh ruleId due to the open-rule unique index + UUID ruleId2 = seedRule("rule-deleted"); + var deleted = newInstance(ruleId2, List.of(userId), List.of(), List.of()); + repo.save(deleted); + + repo.softDelete(deleted.id(), Instant.parse("2026-04-21T10:00:00Z")); + + repo.bulkMarkRead(List.of(live.id(), deleted.id()), Instant.parse("2026-04-21T10:05:00Z")); + + // live row is marked read + assertThat(repo.findById(live.id()).orElseThrow().readAt()) + .isEqualTo(Instant.parse("2026-04-21T10:05:00Z")); + // soft-deleted row is NOT touched by bulkMarkRead + assertThat(repo.findById(deleted.id()).orElseThrow().readAt()).isNull(); } // ------------------------------------------------------------------------- @@ -453,6 +489,32 @@ class PostgresAlertInstanceRepositoryIT extends AbstractPostgresIT { return id; } + /** Inserts a minimal alert_rule in a specific environment and returns its id. */ + private UUID seedRuleInEnv(String name, UUID targetEnvId) { + UUID id = UUID.randomUUID(); + jdbcTemplate.update( + "INSERT INTO alert_rules (id, environment_id, name, severity, condition_kind, condition, " + + "notification_title_tmpl, notification_message_tmpl, created_by, updated_by) " + + "VALUES (?, ?, ?, 'WARNING', 'AGENT_STATE', '{}'::jsonb, 't', 'm', 'sys-user', 'sys-user')", + id, targetEnvId, name + "-" + id); + return id; + } + + /** Creates an AlertInstance bound to a specific environment (not the default envId). */ + private AlertInstance newInstanceInEnv(UUID ruleId, + UUID targetEnvId, + List userIds, + List groupIds, + List roleNames) { + return new AlertInstance( + UUID.randomUUID(), ruleId, Map.of(), targetEnvId, + AlertState.FIRING, AlertSeverity.WARNING, + Instant.now(), null, null, null, null, null, null, + false, null, null, + Map.of(), "title", "message", + userIds, groupIds, roleNames); + } + /** Inserts a minimal alert_rule with re_notify_minutes=1 and returns its id. */ private UUID seedReNotifyRule(String name) { UUID id = UUID.randomUUID(); From 207ae246af4c0c35005d817870cf9171c23052f8 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 18:58:26 +0200 Subject: [PATCH 38/49] chore(ui): regenerate OpenAPI schema for alerts inbox redesign New endpoints visible to the SPA: DELETE /alerts/{id}, POST /alerts/{id}/restore, POST /alerts/bulk-delete, POST /alerts/bulk-ack. GET /alerts gains tri-state acked / read query params. AlertDto now includes readAt. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/api/openapi.json | 2 +- ui/src/api/schema.d.ts | 154 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/ui/src/api/openapi.json b/ui/src/api/openapi.json index d28f7b11..6c5167d1 100644 --- a/ui/src/api/openapi.json +++ b/ui/src/api/openapi.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkReadRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"state","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]}}},{"name":"severity","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["CRITICAL","WARNING","INFO"]}}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentLifecycleCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"eventTypes":{"type":"array","items":{"type":"string","enum":["REGISTERED","RE_REGISTERED","DEREGISTERED","WENT_STALE","WENT_DEAD","RECOVERED"]}},"withinSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","ACKNOWLEDGED","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkReadRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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":"Cameleer Server API","version":"1.0"},"servers":[{"url":"/api/v1","description":"Relative"}],"security":[{"bearer":[]}],"tags":[{"name":"Route Metrics","description":"Route performance metrics (env-scoped)"},{"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":"Agent List","description":"List registered agents in an environment"},{"name":"Sensitive Keys Admin","description":"Global sensitive key masking configuration (ADMIN only)"},{"name":"License Admin","description":"License management"},{"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":"Alerts Inbox","description":"In-app alert inbox, ack and read tracking (env-scoped)"},{"name":"Application Config","description":"Per-application observability configuration (user-facing)"},{"name":"App Management","description":"Application lifecycle and JAR uploads (env-scoped)"},{"name":"Catalog","description":"Unified application catalog"},{"name":"ClickHouse Admin","description":"ClickHouse monitoring and diagnostics (ADMIN only)"},{"name":"Alert Silences","description":"Alert silence management (env-scoped)"},{"name":"Ingestion","description":"Data ingestion endpoints"},{"name":"Group Admin","description":"Group management (ADMIN only)"},{"name":"Usage Analytics","description":"UI usage pattern analytics"},{"name":"Alert Notifications","description":"Outbound webhook notification management"},{"name":"Deployment Management","description":"Deploy, stop, promote, and view logs"},{"name":"Detail","description":"Execution detail and processor snapshot endpoints"},{"name":"Outbound Connections Admin","description":"Admin-managed outbound HTTPS destinations"},{"name":"Agent Config","description":"Agent-authoritative config read (AGENT only)"},{"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":"Agent Events","description":"Agent lifecycle event log (env-scoped)"},{"name":"Route Catalog","description":"Route catalog and discovery (env-scoped)"},{"name":"Application Logs","description":"Query application logs (env-scoped)"},{"name":"Agent SSE","description":"Server-Sent Events endpoint for agent communication"},{"name":"Search","description":"Transaction search and stats (env-scoped)"},{"name":"Audit Log","description":"Audit log viewer (ADMIN only)"},{"name":"Claim Mapping Admin","description":"Manage OIDC claim-to-role/group mapping rules"},{"name":"Diagrams","description":"Diagram rendering endpoints"},{"name":"Environment Admin","description":"Environment management (ADMIN only)"},{"name":"App Settings","description":"Per-application dashboard settings (ADMIN/OPERATOR)"},{"name":"Alert Rules","description":"Alert rule management (env-scoped)"}],"paths":{"/environments/{envSlug}/apps/{appSlug}/settings":{"get":{"tags":["App Settings"],"summary":"Get settings for an application in this environment (returns defaults if not configured)","operationId":"getByAppId","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"put":{"tags":["App Settings"],"summary":"Create or update settings for an application in this environment","operationId":"update","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AppSettingsRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppSettings"}}}}}},"delete":{"tags":["App Settings"],"summary":"Delete application settings for this environment (reverts to defaults)","operationId":"delete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/apps/{appSlug}/container-config":{"put":{"tags":["App Management"],"summary":"Update container config for this app","operationId":"updateContainerConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config":{"get":{"tags":["Application Config"],"summary":"Get application config for this environment","description":"Returns stored config merged with global sensitive keys. Falls back to defaults if no row is persisted yet.","operationId":"getConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}},"put":{"tags":["Application Config"],"summary":"Update application config for this environment","description":"Saves config and pushes CONFIG_UPDATE to LIVE agents of this application in the given environment","operationId":"updateConfig","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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/ConfigUpdateResponse"}}}}}}},"/environments/{envSlug}/alerts/silences/{id}":{"put":{"tags":["Alert Silences"],"operationId":"update_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}},"delete":{"tags":["Alert Silences"],"operationId":"delete_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/rules/{id}":{"get":{"tags":["Alert Rules"],"operationId":"get","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"put":{"tags":["Alert Rules"],"operationId":"update_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}},"delete":{"tags":["Alert Rules"],"operationId":"delete_2","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"},"409":{"description":"Cannot delete the last admin user"}}}},"/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/sensitive-keys":{"get":{"tags":["Sensitive Keys Admin"],"summary":"Get global sensitive keys configuration","operationId":"getSensitiveKeys","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysConfig"}}}}}},"put":{"tags":["Sensitive Keys Admin"],"summary":"Update global sensitive keys configuration","description":"Saves the global sensitive keys. Optionally fans out merged keys to all live agents.","operationId":"updateSensitiveKeys","parameters":[{"name":"pushToAgents","in":"query","required":false,"schema":{"type":"boolean","default":false}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensitiveKeysRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SensitiveKeysResponse"}}}}}}},"/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/outbound-connections/{id}":{"get":{"tags":["Outbound Connections Admin"],"operationId":"get_1","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"put":{"tags":["Outbound Connections Admin"],"operationId":"update_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}},"delete":{"tags":["Outbound Connections Admin"],"operationId":"delete_3","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/admin/environments/{envSlug}":{"get":{"tags":["Environment Admin"],"summary":"Get environment by slug","operationId":"getEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Environment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Environment"}}}}}},"put":{"tags":["Environment Admin"],"summary":"Update an environment's mutable fields (displayName, production, enabled)","description":"Slug is immutable after creation and cannot be changed. Any slug field in the request body is ignored.","operationId":"updateEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEnvironmentRequest"}}},"required":true},"responses":{"200":{"description":"Environment updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}},"delete":{"tags":["Environment Admin"],"summary":"Delete an environment","operationId":"deleteEnvironment","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Environment deleted","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Cannot delete default environment","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/jar-retention":{"put":{"tags":["Environment Admin"],"summary":"Update JAR retention policy for an environment","operationId":"updateJarRetention","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JarRetentionRequest"}}},"required":true},"responses":{"200":{"description":"Retention policy updated","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/environments/{envSlug}/default-container-config":{"put":{"tags":["Environment Admin"],"summary":"Update default container config for an environment","operationId":"updateDefaultContainerConfig","parameters":[{"name":"envSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{"type":"object"}}}},"required":true},"responses":{"200":{"description":"Default container config updated","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid configuration","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/admin/claim-mappings/{id}":{"get":{"tags":["Claim Mapping Admin"],"summary":"Get a claim mapping rule by ID","operationId":"get_2","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"put":{"tags":["Claim Mapping Admin"],"summary":"Update a claim mapping rule","operationId":"update_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}},"delete":{"tags":["Claim Mapping Admin"],"summary":"Delete a claim mapping rule","operationId":"delete_4","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/executions/search":{"post":{"tags":["Search"],"summary":"Advanced search with all filters","description":"Env from the path overrides any environment field in the body.","operationId":"searchPost","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/SearchResultExecutionSummary"}}}}}}},"/environments/{envSlug}/apps":{"get":{"tags":["App Management"],"summary":"List apps in this environment","operationId":"listApps","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"App list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/App"}}}}}}},"post":{"tags":["App Management"],"summary":"Create a new app in this environment","description":"Slug must match ^[a-z0-9][a-z0-9-]{0,63}$ and be unique within the environment. Slug is immutable after creation.","operationId":"createApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAppRequest"}}},"required":true},"responses":{"201":{"description":"App created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug, or slug already exists in this environment","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/versions":{"get":{"tags":["App Management"],"summary":"List versions for this app","operationId":"listVersions","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Version list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"post":{"tags":["App Management"],"summary":"Upload a JAR for a new version of this app","operationId":"uploadJar","parameters":[{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"env":{"$ref":"#/components/schemas/Environment"},"file":{"type":"string","format":"binary"}},"required":["file"]}}}},"responses":{"201":{"description":"JAR uploaded and version created","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppVersion"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments":{"get":{"tags":["Deployment Management"],"summary":"List deployments for this app in this environment","operationId":"listDeployments","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deployment list returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Deployment"}}}}}}},"post":{"tags":["Deployment Management"],"summary":"Create and start a new deployment for this app in this environment","operationId":"deploy","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeployRequest"}}},"required":true},"responses":{"202":{"description":"Deployment accepted and starting","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/stop":{"post":{"tags":["Deployment Management"],"summary":"Stop a running deployment","operationId":"stop","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment stopped","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/promote":{"post":{"tags":["Deployment Management"],"summary":"Promote this deployment to a different environment","description":"Target environment is specified by slug in the request body. The same app slug must exist in the target environment (or be created separately first).","operationId":"promote","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromoteRequest"}}},"required":true},"responses":{"202":{"description":"Promotion accepted and starting","content":{"*/*":{"schema":{"type":"object"}}}},"404":{"description":"Deployment or target environment not found","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/config/test-expression":{"post":{"tags":["Application Config"],"summary":"Test a tap expression against sample data via a live agent in this environment","operationId":"testExpression","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","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 in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestExpressionResponse"}}}}}}},"/environments/{envSlug}/alerts/{id}/restore":{"post":{"tags":["Alerts Inbox"],"operationId":"restore","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/read":{"post":{"tags":["Alerts Inbox"],"operationId":"read","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{id}/ack":{"post":{"tags":["Alerts Inbox"],"operationId":"ack","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}}},"/environments/{envSlug}/alerts/silences":{"get":{"tags":["Alert Silences"],"operationId":"list","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"post":{"tags":["Alert Silences"],"operationId":"create","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertSilenceRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertSilenceResponse"}}}}}}},"/environments/{envSlug}/alerts/rules":{"get":{"tags":["Alert Rules"],"operationId":"list_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"post":{"tags":["Alert Rules"],"operationId":"create_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/test-evaluate":{"post":{"tags":["Alert Rules"],"operationId":"testEvaluate","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestEvaluateRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestEvaluateResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/render-preview":{"post":{"tags":["Alert Rules"],"operationId":"renderPreview","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RenderPreviewRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/RenderPreviewResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/enable":{"post":{"tags":["Alert Rules"],"operationId":"enable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/rules/{id}/disable":{"post":{"tags":["Alert Rules"],"operationId":"disable","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertRuleResponse"}}}}}}},"/environments/{envSlug}/alerts/bulk-read":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkRead","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkIdsRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/bulk-delete":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkDelete","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkIdsRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/bulk-ack":{"post":{"tags":["Alerts Inbox"],"operationId":"bulkAck","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkIdsRequest"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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 buffered and flushed periodically.","operationId":"ingestLogs","requestBody":{"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LogEntry"}}}},"required":true},"responses":{"202":{"description":"Logs accepted for indexing"}}}},"/data/executions":{"post":{"tags":["Ingestion"],"summary":"Ingest execution chunk","operationId":"ingestChunks","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/data/events":{"post":{"tags":["Ingestion"],"summary":"Ingest agent events","operationId":"ingestEvents","requestBody":{"content":{"application/json":{"schema":{"type":"string"}}},"required":true},"responses":{"200":{"description":"OK"}}}},"/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"}}}},"/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"}}}},"429":{"description":"Account locked due to too many failed attempts","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthTokenResponse"}}}}}}},"/alerts/notifications/{id}/retry":{"post":{"tags":["Alert Notifications"],"operationId":"retry","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}},"/agents/{id}/replay":{"post":{"tags":["Agent Commands"],"summary":"Replay an exchange on a specific agent (synchronous)","description":"Sends a replay command and waits for the agent to complete the replay. Returns the replay result including status, replayExchangeId, and duration.","operationId":"replayExchange","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReplayRequest"}}},"required":true},"responses":{"200":{"description":"Replay completed (check status for success/failure)","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"404":{"description":"Agent not found or not connected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}},"504":{"description":"Agent did not respond in time","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ReplayResponse"}}}}}}},"/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. Auto-registers the agent if not in registry (e.g. after server restart).","operationId":"heartbeat","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeartbeatRequest"}}}},"responses":{"200":{"description":"Heartbeat accepted"}}}},"/agents/{id}/deregister":{"post":{"tags":["Agent Management"],"summary":"Deregister agent","description":"Removes the agent from the registry. Called by agents during graceful shutdown.","operationId":"deregister","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agent deregistered"},"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 and waits for responses","operationId":"sendGroupCommand","parameters":[{"name":"group","in":"path","required":true,"schema":{"type":"string"}},{"name":"environment","in":"query","required":false,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CommandRequest"}}},"required":true},"responses":{"200":{"description":"Commands dispatched and responses collected","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}},"400":{"description":"Invalid command payload","content":{"*/*":{"schema":{"$ref":"#/components/schemas/CommandGroupResponse"}}}}}}},"/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","parameters":[{"name":"environment","in":"query","required":false,"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"}}}}}}},"/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":{"type":"object"}}}},"400":{"description":"Disabled in OIDC mode","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"},"400":{"description":"Disabled in OIDC mode or policy violation"}}}},"/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/outbound-connections":{"get":{"tags":["Outbound Connections Admin"],"operationId":"list_2","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"post":{"tags":["Outbound Connections Admin"],"operationId":"create_2","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutboundConnectionRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionDto"}}}}}}},"/admin/outbound-connections/{id}/test":{"post":{"tags":["Outbound Connections Admin"],"operationId":"test","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/OutboundConnectionTestResult"}}}}}}},"/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/license":{"get":{"tags":["License Admin"],"summary":"Get current license info","operationId":"getCurrent","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LicenseInfo"}}}}}},"post":{"tags":["License Admin"],"summary":"Update license token at runtime","operationId":"update_5","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLicenseRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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/environments":{"get":{"tags":["Environment Admin"],"summary":"List all environments","operationId":"listEnvironments","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Environment"}}}}}}},"post":{"tags":["Environment Admin"],"summary":"Create a new environment","operationId":"createEnvironment","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEnvironmentRequest"}}},"required":true},"responses":{"201":{"description":"Environment created","content":{"*/*":{"schema":{"type":"object"}}}},"400":{"description":"Invalid slug or slug already exists","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/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"}}}},"/admin/claim-mappings":{"get":{"tags":["Claim Mapping Admin"],"summary":"List all claim mapping rules","operationId":"list_3","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"post":{"tags":["Claim Mapping Admin"],"summary":"Create a claim mapping rule","operationId":"create_3","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRuleRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClaimMappingRule"}}}}}}},"/admin/claim-mappings/test":{"post":{"tags":["Claim Mapping Admin"],"summary":"Test claim mapping rules against a set of claims (accepts unsaved rules)","operationId":"test_1","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/TestResponse"}}}}}}},"/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 by index","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"}}}}}}}},"/executions/{executionId}/processors/by-seq/{seq}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a processor by seq number","operationId":"processorSnapshotBySeq","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"seq","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"}}}}}}}},"/executions/{executionId}/processors/by-id/{processorId}/snapshot":{"get":{"tags":["Detail"],"summary":"Get exchange snapshot for a specific processor by processorId","operationId":"processorSnapshotById","parameters":[{"name":"executionId","in":"path","required":true,"schema":{"type":"string"}},{"name":"processorId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Snapshot data","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}},"404":{"description":"Snapshot not found","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/stats":{"get":{"tags":["Search"],"summary":"Aggregate execution stats (P99 latency, active count, SLA compliance)","operationId":"stats","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries":{"get":{"tags":["Search"],"summary":"Bucketed time-series stats over a time window","operationId":"timeseries","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/stats/timeseries/by-route":{"get":{"tags":["Search"],"summary":"Timeseries grouped by route for an application","operationId":"timeseriesByRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/timeseries/by-app":{"get":{"tags":["Search"],"summary":"Timeseries grouped by application","operationId":"timeseriesByApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"from","in":"query","required":true,"schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"buckets","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":24}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/StatsTimeseries"}}}}}}}},"/environments/{envSlug}/stats/punchcard":{"get":{"tags":["Search"],"summary":"Transaction punchcard: weekday x hour grid (rolling 7 days)","operationId":"punchcard","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PunchcardCell"}}}}}}}},"/environments/{envSlug}/routes":{"get":{"tags":["Route Catalog"],"summary":"Get route catalog for this environment","description":"Returns all applications with their routes, agents, and health status — filtered to this environment","operationId":"getCatalog","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics":{"get":{"tags":["Route Metrics"],"summary":"Get route metrics for this environment","description":"Returns aggregated performance metrics per route for the given time window. Optional appId filter narrows to a single application.","operationId":"getMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/routes/metrics/processors":{"get":{"tags":["Route Metrics"],"summary":"Get processor metrics for this environment","description":"Returns aggregated performance metrics per processor for the given route and time window","operationId":"getProcessorMetrics","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}}},"/environments/{envSlug}/logs":{"get":{"tags":["Application Logs"],"summary":"Search application log entries in this environment","description":"Cursor-paginated log search scoped to the env in the path. Supports free-text search, multi-level filtering, and optional application/agent scoping.","operationId":"searchLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"}},{"name":"query","in":"query","required":false,"schema":{"type":"string"}},{"name":"level","in":"query","required":false,"schema":{"type":"string"}},{"name":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","required":false,"schema":{"type":"string"}},{"name":"exchangeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"logger","in":"query","required":false,"schema":{"type":"string"}},{"name":"source","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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":100}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"desc"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/LogSearchPageResponse"}}}}}}},"/environments/{envSlug}/executions":{"get":{"tags":["Search"],"summary":"Search executions with basic filters (env from path)","operationId":"searchGet","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/errors/top":{"get":{"tags":["Search"],"summary":"Top N errors with velocity trend","operationId":"topErrors","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"application","in":"query","required":false,"schema":{"type":"string"}},{"name":"routeId","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":5}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/TopError"}}}}}}}},"/environments/{envSlug}/config":{"get":{"tags":["Application Config"],"summary":"List application configs in this environment","operationId":"listConfigs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"Configs returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ApplicationConfig"}}}}}}}},"/environments/{envSlug}/attributes/keys":{"get":{"tags":["Search"],"summary":"Distinct attribute key names for this environment","operationId":"attributeKeys","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}":{"get":{"tags":["App Management"],"summary":"Get app by env + slug","operationId":"getApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"App found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}},"404":{"description":"App not found in this environment","content":{"*/*":{"schema":{"$ref":"#/components/schemas/App"}}}}}},"delete":{"tags":["App Management"],"summary":"Delete this app","operationId":"deleteApp","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"App deleted"}}}},"/environments/{envSlug}/apps/{appSlug}/routes/{routeId}/diagram":{"get":{"tags":["Diagrams"],"summary":"Find the latest diagram for this app's route in this environment","description":"Resolves agents in this env for this app, then looks up the latest diagram for the route they reported. Env scope prevents a dev route from returning a prod diagram.","operationId":"findByAppAndRoute","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"routeId","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"responses":{"200":{"description":"Diagram layout returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}},"404":{"description":"No diagram found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/DiagramLayout"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/processor-routes":{"get":{"tags":["Application Config"],"summary":"Get processor to route mapping for this environment","description":"Returns a map of processorId → routeId for all processors seen in this application + environment","operationId":"getProcessorRouteMapping","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Mapping returned","content":{"*/*":{"schema":{"type":"object","additionalProperties":{"type":"string"}}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}":{"get":{"tags":["Deployment Management"],"summary":"Get deployment by ID","operationId":"getDeployment","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deployment found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}},"404":{"description":"Deployment not found","content":{"*/*":{"schema":{"$ref":"#/components/schemas/Deployment"}}}}}}},"/environments/{envSlug}/apps/{appSlug}/deployments/{deploymentId}/logs":{"get":{"tags":["Deployment Management"],"summary":"Get container logs for this deployment","operationId":"getLogs","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"appSlug","in":"path","required":true,"schema":{"type":"string"}},{"name":"deploymentId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Logs returned","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}},"404":{"description":"Deployment not found or no container","content":{"*/*":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/environments/{envSlug}/app-settings":{"get":{"tags":["App Settings"],"summary":"List application settings in this environment","operationId":"getAll","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AppSettings"}}}}}}}},"/environments/{envSlug}/alerts":{"get":{"tags":["Alerts Inbox"],"operationId":"list_4","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}},{"name":"state","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["PENDING","FIRING","RESOLVED"]}}},{"name":"severity","in":"query","required":false,"schema":{"type":"array","items":{"type":"string","enum":["CRITICAL","WARNING","INFO"]}}},{"name":"acked","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"read","in":"query","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertDto"}}}}}}}},"/environments/{envSlug}/alerts/{id}":{"get":{"tags":["Alerts Inbox"],"operationId":"get_3","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AlertDto"}}}}}},"delete":{"tags":["Alerts Inbox"],"operationId":"delete_5","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK"}}}},"/environments/{envSlug}/alerts/{alertId}/notifications":{"get":{"tags":["Alert Notifications"],"operationId":"listForInstance","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"name":"alertId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlertNotificationDto"}}}}}}}},"/environments/{envSlug}/alerts/unread-count":{"get":{"tags":["Alerts Inbox"],"operationId":"unreadCount","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UnreadCountResponse"}}}}}}},"/environments/{envSlug}/agents":{"get":{"tags":["Agent List"],"summary":"List all agents in this environment","description":"Returns registered agents with runtime metrics, optionally filtered by status and/or application","operationId":"listAgents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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"}}}}}}},"/environments/{envSlug}/agents/{agentId}/metrics":{"get":{"tags":["agent-metrics-controller"],"operationId":"getMetrics_1","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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}},{"name":"mode","in":"query","required":false,"schema":{"type":"string","default":"gauge"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentMetricsResponse"}}}}}}},"/environments/{envSlug}/agents/events":{"get":{"tags":["Agent Events"],"summary":"Query agent events in this environment","description":"Cursor-paginated. Returns newest first. Pass nextCursor back as ?cursor= for the next page.","operationId":"getEvents","parameters":[{"name":"env","in":"query","required":true,"schema":{"$ref":"#/components/schemas/Environment"}},{"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":"cursor","in":"query","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","format":"int32","default":50}}],"responses":{"200":{"description":"Event page returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AgentEventPageResponse"}}}}}}},"/diagrams/{contentHash}/render":{"get":{"tags":["Diagrams"],"summary":"Render a route diagram by content hash","description":"Returns SVG (default) or JSON layout based on Accept header. Content hashes are globally unique, so this endpoint is intentionally flat (no env).","operationId":"renderDiagram","parameters":[{"name":"contentHash","in":"path","required":true,"schema":{"type":"string"}},{"name":"direction","in":"query","required":false,"schema":{"type":"string","default":"LR"}}],"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"}}}}}}},"/catalog":{"get":{"tags":["Catalog"],"summary":"Get unified catalog","description":"Returns all applications (managed + unmanaged) with live agent data, routes, and deployment status","operationId":"getCatalog_1","parameters":[{"name":"environment","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"}}],"responses":{"200":{"description":"Catalog returned","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CatalogApp"}}}}}}}},"/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"}}}}}}},"/auth/me":{"get":{"tags":["Authentication"],"summary":"Get current user details","operationId":"me","responses":{"200":{"description":"Current user details","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}},"401":{"description":"Not authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/UserDetail"}}}}}}},"/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 and cannot be auto-registered","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/SseEmitter"}}}}}}},"/agents/config":{"get":{"tags":["Agent Config"],"summary":"Get application config for the calling agent","description":"Resolves (application, environment) from the agent's JWT + registry. Prefers the registry entry (heartbeat-authoritative); falls back to the JWT env claim. Returns 404 if neither identifies a valid agent.","operationId":"getConfigForAgent","responses":{"200":{"description":"Config returned","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}},"404":{"description":"Calling agent could not be resolved","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AppConfigResponse"}}}}}}},"/admin/usage":{"get":{"tags":["Usage Analytics"],"summary":"Query usage statistics","description":"Returns aggregated API usage stats grouped by endpoint, user, or hour","operationId":"getUsage","parameters":[{"name":"from","in":"query","required":false,"schema":{"type":"string"}},{"name":"to","in":"query","required":false,"schema":{"type":"string"}},{"name":"username","in":"query","required":false,"schema":{"type":"string"}},{"name":"groupBy","in":"query","required":false,"schema":{"type":"string","default":"endpoint"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UsageStats"}}}}}}}},"/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/outbound-connections/{id}/usage":{"get":{"tags":["Outbound Connections Admin"],"operationId":"usage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"type":"string","format":"uuid"}}}}}}}},"/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","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/clickhouse/tables":{"get":{"tags":["ClickHouse Admin"],"summary":"List ClickHouse tables with sizes","operationId":"getTables_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseTableInfo"}}}}}}}},"/admin/clickhouse/status":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse cluster status","operationId":"getStatus_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHouseStatusResponse"}}}}}}},"/admin/clickhouse/queries":{"get":{"tags":["ClickHouse Admin"],"summary":"Active ClickHouse queries","operationId":"getQueries_1","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ClickHouseQueryInfo"}}}}}}}},"/admin/clickhouse/pipeline":{"get":{"tags":["ClickHouse Admin"],"summary":"Search indexer pipeline statistics","operationId":"getPipeline","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/IndexerPipelineResponse"}}}}}}},"/admin/clickhouse/performance":{"get":{"tags":["ClickHouse Admin"],"summary":"ClickHouse storage and performance metrics","operationId":"getPerformance","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ClickHousePerformanceResponse"}}}}}}},"/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"}}}}}}},"/catalog/{applicationId}":{"delete":{"tags":["Catalog"],"summary":"Dismiss application and purge all data","operationId":"dismissApplication","parameters":[{"name":"applicationId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Application dismissed"},"409":{"description":"Cannot dismiss — live agents connected"}}}}},"components":{"schemas":{"Environment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"},"defaultContainerConfig":{"type":"object","additionalProperties":{"type":"object"}},"jarRetentionCount":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"AppSettingsRequest":{"type":"object","description":"Per-application dashboard settings","properties":{"slaThresholdMs":{"type":"integer","format":"int32","description":"SLA duration threshold in milliseconds","minimum":1},"healthErrorWarn":{"type":"number","format":"double","description":"Error rate % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthErrorCrit":{"type":"number","format":"double","description":"Error rate % threshold for critical (red) health dot","maximum":100,"minimum":0},"healthSlaWarn":{"type":"number","format":"double","description":"SLA compliance % threshold for warning (yellow) health dot","maximum":100,"minimum":0},"healthSlaCrit":{"type":"number","format":"double","description":"SLA compliance % threshold for critical (red) health dot","maximum":100,"minimum":0}},"required":["healthErrorCrit","healthErrorWarn","healthSlaCrit","healthSlaWarn","slaThresholdMs"]},"AppSettings":{"type":"object","properties":{"applicationId":{"type":"string"},"environment":{"type":"string"},"slaThresholdMs":{"type":"integer","format":"int32"},"healthErrorWarn":{"type":"number","format":"double"},"healthErrorCrit":{"type":"number","format":"double"},"healthSlaWarn":{"type":"number","format":"double"},"healthSlaCrit":{"type":"number","format":"double"},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"ApplicationConfig":{"type":"object","properties":{"application":{"type":"string"},"environment":{"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"}},"applicationLogLevel":{"type":"string"},"taps":{"type":"array","items":{"$ref":"#/components/schemas/TapDefinition"}},"tapVersion":{"type":"integer","format":"int32"},"routeRecording":{"type":"object","additionalProperties":{"type":"boolean"}},"compressSuccess":{"type":"boolean"},"agentLogLevel":{"type":"string"},"routeSamplingRates":{"type":"object","additionalProperties":{"type":"number","format":"double"}},"sensitiveKeys":{"type":"array","items":{"type":"string"}}}},"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"}}},"AgentResponse":{"type":"object","properties":{"agentId":{"type":"string"},"status":{"type":"string"},"message":{"type":"string"}}},"CommandGroupResponse":{"type":"object","properties":{"success":{"type":"boolean"},"total":{"type":"integer","format":"int32"},"responded":{"type":"integer","format":"int32"},"responses":{"type":"array","items":{"$ref":"#/components/schemas/AgentResponse"}},"timedOut":{"type":"array","items":{"type":"string"}}}},"ConfigUpdateResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"AlertSilenceRequest":{"type":"object","properties":{"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"}},"required":["endsAt","matcher","startsAt"]},"SilenceMatcher":{"type":"object","properties":{"ruleId":{"type":"string","format":"uuid"},"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"wildcard":{"type":"boolean"}}},"AlertSilenceResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"matcher":{"$ref":"#/components/schemas/SilenceMatcher"},"reason":{"type":"string"},"startsAt":{"type":"string","format":"date-time"},"endsAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"AgentLifecycleCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"eventTypes":{"type":"array","items":{"type":"string","enum":["REGISTERED","RE_REGISTERED","DEREGISTERED","WENT_STALE","WENT_DEAD","RECOVERED"]}},"withinSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AgentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"state":{"type":"string"},"forSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"AlertCondition":{"type":"object","discriminator":{"propertyName":"kind"},"properties":{"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]}}},"AlertRuleRequest":{"type":"object","properties":{"name":{"type":"string","minLength":1},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingRequest"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}}},"required":["condition","conditionKind","severity"]},"AlertRuleTarget":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["USER","GROUP","ROLE"]},"targetId":{"type":"string"}}},"AlertScope":{"type":"object","properties":{"appSlug":{"type":"string"},"routeId":{"type":"string"},"agentId":{"type":"string"}}},"DeploymentStateCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"states":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"ExchangeFilter":{"type":"object","properties":{"status":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}}}},"ExchangeMatchCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"filter":{"$ref":"#/components/schemas/ExchangeFilter"},"fireMode":{"type":"string","enum":["PER_EXCHANGE","COUNT_IN_WINDOW"]},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"perExchangeLingerSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"JvmMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string"},"aggregation":{"type":"string","enum":["MAX","MIN","AVG","LATEST"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"LogPatternCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"level":{"type":"string"},"pattern":{"type":"string"},"threshold":{"type":"integer","format":"int32"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"RouteMetricCondition":{"allOf":[{"$ref":"#/components/schemas/AlertCondition"},{"type":"object","properties":{"scope":{"$ref":"#/components/schemas/AlertScope"},"metric":{"type":"string","enum":["ERROR_RATE","AVG_DURATION_MS","P99_LATENCY_MS","THROUGHPUT","ERROR_COUNT"]},"comparator":{"type":"string","enum":["GT","GTE","LT","LTE","EQ"]},"threshold":{"type":"number","format":"double"},"windowSeconds":{"type":"integer","format":"int32"},"kind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"],"readOnly":true}}}]},"WebhookBindingRequest":{"type":"object","properties":{"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}},"required":["outboundConnectionId"]},"AlertRuleResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"enabled":{"type":"boolean"},"conditionKind":{"type":"string","enum":["ROUTE_METRIC","EXCHANGE_MATCH","AGENT_STATE","AGENT_LIFECYCLE","DEPLOYMENT_STATE","LOG_PATTERN","JVM_METRIC"]},"condition":{"oneOf":[{"$ref":"#/components/schemas/AgentLifecycleCondition"},{"$ref":"#/components/schemas/AgentStateCondition"},{"$ref":"#/components/schemas/DeploymentStateCondition"},{"$ref":"#/components/schemas/ExchangeMatchCondition"},{"$ref":"#/components/schemas/JvmMetricCondition"},{"$ref":"#/components/schemas/LogPatternCondition"},{"$ref":"#/components/schemas/RouteMetricCondition"}]},"evaluationIntervalSeconds":{"type":"integer","format":"int32"},"forDurationSeconds":{"type":"integer","format":"int32"},"reNotifyMinutes":{"type":"integer","format":"int32"},"notificationTitleTmpl":{"type":"string"},"notificationMessageTmpl":{"type":"string"},"webhooks":{"type":"array","items":{"$ref":"#/components/schemas/WebhookBindingResponse"}},"targets":{"type":"array","items":{"$ref":"#/components/schemas/AlertRuleTarget"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":"string"}}},"WebhookBindingResponse":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"bodyOverride":{"type":"string"},"headerOverrides":{"type":"object","additionalProperties":{"type":"string"}}}},"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)"}}},"ThresholdConfigRequest":{"type":"object","description":"Threshold configuration for admin monitoring","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholdsRequest"}},"required":["database"]},"DatabaseThresholds":{"type":"object","properties":{"connectionPoolWarning":{"type":"integer","format":"int32"},"connectionPoolCritical":{"type":"integer","format":"int32"},"queryDurationWarning":{"type":"number","format":"double"},"queryDurationCritical":{"type":"number","format":"double"}}},"ThresholdConfig":{"type":"object","properties":{"database":{"$ref":"#/components/schemas/DatabaseThresholds"}}},"SensitiveKeysRequest":{"type":"object","description":"Global sensitive keys configuration","properties":{"keys":{"type":"array","description":"List of key names or glob patterns to mask","items":{"type":"string"}}},"required":["keys"]},"SensitiveKeysResponse":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}},"pushResult":{"$ref":"#/components/schemas/CommandGroupResponse"}}},"UpdateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"Basic":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"username":{"type":"string"},"passwordCiphertext":{"type":"string"}}}]},"Bearer":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"},{"type":"object","properties":{"tokenCiphertext":{"type":"string"}}}]},"None":{"allOf":[{"$ref":"#/components/schemas/OutboundAuth"}]},"OutboundAuth":{"type":"object"},"OutboundConnectionRequest":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"minLength":0},"description":{"type":"string","maxLength":2000,"minLength":0},"url":{"type":"string","minLength":1,"pattern":"^https://.+"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecret":{"type":"string"},"auth":{"oneOf":[{"$ref":"#/components/schemas/Basic"},{"$ref":"#/components/schemas/Bearer"},{"$ref":"#/components/schemas/None"}]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}}},"required":["auth","method","tlsTrustMode"]},"OutboundConnectionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"defaultHeaders":{"type":"object","additionalProperties":{"type":"string"}},"defaultBodyTmpl":{"type":"string"},"tlsTrustMode":{"type":"string","enum":["SYSTEM_DEFAULT","TRUST_ALL","TRUST_PATHS"]},"tlsCaPemPaths":{"type":"array","items":{"type":"string"}},"hmacSecretSet":{"type":"boolean"},"authKind":{"type":"string","enum":["NONE","BEARER","BASIC"]},"allowedEnvironmentIds":{"type":"array","items":{"type":"string","format":"uuid"}},"createdAt":{"type":"string","format":"date-time"},"createdBy":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"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"},"userIdClaim":{"type":"string"},"audience":{"type":"string"},"additionalScopes":{"type":"array","items":{"type":"string"}}}},"UpdateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"UpdateEnvironmentRequest":{"type":"object","properties":{"displayName":{"type":"string"},"production":{"type":"boolean"},"enabled":{"type":"boolean"}}},"JarRetentionRequest":{"type":"object","properties":{"jarRetentionCount":{"type":"integer","format":"int32"}}},"CreateRuleRequest":{"type":"object","properties":{"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"ClaimMappingRule":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"},"createdAt":{"type":"string","format":"date-time"}}},"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"},"instanceId":{"type":"string"},"processorType":{"type":"string"},"applicationId":{"type":"string"},"instanceIds":{"type":"array","items":{"type":"string"}},"offset":{"type":"integer","format":"int32"},"limit":{"type":"integer","format":"int32"},"sortField":{"type":"string"},"sortDir":{"type":"string"},"environment":{"type":"string"}}},"ExecutionSummary":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"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"}},"hasTraceData":{"type":"boolean"},"isReplay":{"type":"boolean"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","errorMessage","executionId","hasTraceData","highlight","instanceId","isReplay","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"]},"CreateAppRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"}}},"AppVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"version":{"type":"integer","format":"int32"},"jarPath":{"type":"string"},"jarChecksum":{"type":"string"},"jarFilename":{"type":"string"},"jarSizeBytes":{"type":"integer","format":"int64"},"detectedRuntimeType":{"type":"string"},"detectedMainClass":{"type":"string"},"uploadedAt":{"type":"string","format":"date-time"}}},"DeployRequest":{"type":"object","properties":{"appVersionId":{"type":"string","format":"uuid"}}},"Deployment":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"appId":{"type":"string","format":"uuid"},"appVersionId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["STOPPED","STARTING","RUNNING","DEGRADED","STOPPING","FAILED"]},"targetState":{"type":"string"},"deploymentStrategy":{"type":"string"},"replicaStates":{"type":"array","items":{"type":"object","additionalProperties":{"type":"object"}}},"deployStage":{"type":"string"},"containerId":{"type":"string"},"containerName":{"type":"string"},"errorMessage":{"type":"string"},"resolvedConfig":{"type":"object","additionalProperties":{"type":"object"}},"deployedAt":{"type":"string","format":"date-time"},"stoppedAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"PromoteRequest":{"type":"object","properties":{"targetEnvironment":{"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"}}},"AlertDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"ruleId":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["PENDING","FIRING","RESOLVED"]},"severity":{"type":"string","enum":["CRITICAL","WARNING","INFO"]},"title":{"type":"string"},"message":{"type":"string"},"firedAt":{"type":"string","format":"date-time"},"ackedAt":{"type":"string","format":"date-time"},"ackedBy":{"type":"string"},"resolvedAt":{"type":"string","format":"date-time"},"readAt":{"type":"string","format":"date-time"},"silenced":{"type":"boolean"},"currentValue":{"type":"number","format":"double"},"threshold":{"type":"number","format":"double"},"context":{"type":"object","additionalProperties":{"type":"object"}}}},"TestEvaluateRequest":{"type":"object"},"TestEvaluateResponse":{"type":"object","properties":{"resultKind":{"type":"string"},"detail":{"type":"string"}}},"RenderPreviewRequest":{"type":"object","properties":{"context":{"type":"object","additionalProperties":{"type":"object"}}}},"RenderPreviewResponse":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"}}},"BulkIdsRequest":{"type":"object","properties":{"instanceIds":{"type":"array","items":{"type":"string","format":"uuid"},"maxItems":500,"minItems":1}},"required":["instanceIds"]},"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"}},"source":{"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"}}},"AlertNotificationDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"alertInstanceId":{"type":"string","format":"uuid"},"webhookId":{"type":"string","format":"uuid"},"outboundConnectionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["PENDING","DELIVERED","FAILED"]},"attempts":{"type":"integer","format":"int32"},"nextAttemptAt":{"type":"string","format":"date-time"},"lastResponseStatus":{"type":"integer","format":"int32"},"lastResponseSnippet":{"type":"string"},"deliveredAt":{"type":"string","format":"date-time"},"createdAt":{"type":"string","format":"date-time"}}},"ReplayRequest":{"type":"object","description":"Request to replay an exchange on an agent","properties":{"routeId":{"type":"string","description":"Camel route ID to replay on"},"body":{"type":"string","description":"Message body for the replayed exchange"},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Message headers for the replayed exchange"},"originalExchangeId":{"type":"string","description":"Exchange ID of the original execution being replayed (for audit trail)"}},"required":["routeId"]},"ReplayResponse":{"type":"object","description":"Result of a replay command","properties":{"status":{"type":"string","description":"Replay outcome: SUCCESS or FAILURE"},"message":{"type":"string","description":"Human-readable result message"},"data":{"type":"string","description":"Structured result data from the agent (JSON)"}}},"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"]},"HeartbeatRequest":{"type":"object","properties":{"routeStates":{"type":"object","additionalProperties":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}},"environmentId":{"type":"string"}}},"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":{"instanceId":{"type":"string"},"applicationId":{"type":"string","default":"default"},"environmentId":{"type":"string","default":"default"},"version":{"type":"string"},"routeIds":{"type":"array","items":{"type":"string"}},"capabilities":{"type":"object","additionalProperties":{"type":"object"}}},"required":["instanceId"]},"AgentRegistrationResponse":{"type":"object","description":"Agent registration result with JWT tokens and SSE endpoint","properties":{"instanceId":{"type":"string"},"sseEndpoint":{"type":"string"},"heartbeatIntervalMs":{"type":"integer","format":"int64"},"serverPublicKey":{"type":"string"},"accessToken":{"type":"string"},"refreshToken":{"type":"string"}},"required":["accessToken","instanceId","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"}}},"SetPasswordRequest":{"type":"object","properties":{"password":{"type":"string","minLength":1}}},"CreateRoleRequest":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"scope":{"type":"string"}}},"OutboundConnectionTestResult":{"type":"object","properties":{"status":{"type":"integer","format":"int32"},"latencyMs":{"type":"integer","format":"int64"},"responseSnippet":{"type":"string"},"tlsProtocol":{"type":"string"},"tlsCipherSuite":{"type":"string"},"peerCertificateSubject":{"type":"string"},"peerCertificateExpiresAtEpochMs":{"type":"integer","format":"int64"},"error":{"type":"string"}}},"OidcTestResult":{"type":"object","description":"OIDC provider connectivity test result","properties":{"status":{"type":"string"},"authorizationEndpoint":{"type":"string"}},"required":["authorizationEndpoint","status"]},"UpdateLicenseRequest":{"type":"object","properties":{"token":{"type":"string"}}},"CreateGroupRequest":{"type":"object","properties":{"name":{"type":"string"},"parentGroupId":{"type":"string","format":"uuid"}}},"CreateEnvironmentRequest":{"type":"object","properties":{"slug":{"type":"string"},"displayName":{"type":"string"},"production":{"type":"boolean"}}},"TestRequest":{"type":"object","properties":{"rules":{"type":"array","items":{"$ref":"#/components/schemas/TestRuleRequest"}},"claims":{"type":"object","additionalProperties":{"type":"object"}}}},"TestRuleRequest":{"type":"object","properties":{"id":{"type":"string"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"},"priority":{"type":"integer","format":"int32"}}},"MatchedRuleResponse":{"type":"object","properties":{"ruleId":{"type":"string"},"priority":{"type":"integer","format":"int32"},"claim":{"type":"string"},"matchType":{"type":"string"},"matchValue":{"type":"string"},"action":{"type":"string"},"target":{"type":"string"}}},"TestResponse":{"type":"object","properties":{"matchedRules":{"type":"array","items":{"$ref":"#/components/schemas/MatchedRuleResponse"}},"effectiveRoles":{"type":"array","items":{"type":"string"}},"effectiveGroups":{"type":"array","items":{"type":"string"}},"fallback":{"type":"boolean"}}},"ExecutionDetail":{"type":"object","properties":{"executionId":{"type":"string"},"routeId":{"type":"string"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"environment":{"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"},"inputProperties":{"type":"string"},"outputProperties":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"traceId":{"type":"string"},"spanId":{"type":"string"}},"required":["applicationId","attributes","correlationId","diagramContentHash","durationMs","endTime","environment","errorCategory","errorMessage","errorStackTrace","errorType","exchangeId","executionId","inputBody","inputHeaders","inputProperties","instanceId","outputBody","outputHeaders","outputProperties","processors","rootCauseMessage","rootCauseType","routeId","spanId","startTime","status","traceId"]},"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"},"errorMessage":{"type":"string"},"errorStackTrace":{"type":"string"},"attributes":{"type":"object","additionalProperties":{"type":"string"}},"iteration":{"type":"integer","format":"int32"},"iterationSize":{"type":"integer","format":"int32"},"loopIndex":{"type":"integer","format":"int32"},"loopSize":{"type":"integer","format":"int32"},"splitIndex":{"type":"integer","format":"int32"},"splitSize":{"type":"integer","format":"int32"},"multicastIndex":{"type":"integer","format":"int32"},"resolvedEndpointUri":{"type":"string"},"errorType":{"type":"string"},"errorCategory":{"type":"string"},"rootCauseType":{"type":"string"},"rootCauseMessage":{"type":"string"},"errorHandlerType":{"type":"string"},"circuitBreakerState":{"type":"string"},"fallbackTriggered":{"type":"boolean"},"filterMatched":{"type":"boolean"},"duplicateMessage":{"type":"boolean"},"hasTraceData":{"type":"boolean"},"children":{"type":"array","items":{"$ref":"#/components/schemas/ProcessorNode"}}},"required":["attributes","children","circuitBreakerState","duplicateMessage","durationMs","endTime","errorCategory","errorHandlerType","errorMessage","errorStackTrace","errorType","fallbackTriggered","filterMatched","hasTraceData","iteration","iterationSize","loopIndex","loopSize","multicastIndex","processorId","processorType","resolvedEndpointUri","rootCauseMessage","rootCauseType","splitIndex","splitSize","startTime","status"]},"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"},"slaCompliance":{"type":"number","format":"double"}},"required":["activeCount","avgDurationMs","failedCount","p99LatencyMs","prevAvgDurationMs","prevFailedCount","prevP99LatencyMs","prevTotalCount","slaCompliance","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"]},"PunchcardCell":{"type":"object","properties":{"weekday":{"type":"integer","format":"int32"},"hour":{"type":"integer","format":"int32"},"totalCount":{"type":"integer","format":"int64"},"failedCount":{"type":"integer","format":"int64"}}},"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"},"fromEndpointUri":{"type":"string","description":"The from() endpoint URI, e.g. 'direct:processOrder'"},"routeState":{"type":"string","description":"Operational state of the route: stopped, suspended, or null (started/default)"}},"required":["exchangeCount","fromEndpointUri","lastSeen","routeId","routeState"]},"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"}},"slaCompliance":{"type":"number","format":"double"}},"required":["appId","avgDurationMs","errorRate","exchangeCount","p99DurationMs","routeId","slaCompliance","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"]},"LogEntryResponse":{"type":"object","description":"Application log entry","properties":{"timestamp":{"type":"string","description":"Log timestamp (ISO-8601)"},"level":{"type":"string","description":"Log level (INFO, WARN, ERROR, DEBUG, TRACE)"},"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)"},"exchangeId":{"type":"string","description":"Camel exchange ID (if present)"},"instanceId":{"type":"string","description":"Agent instance ID"},"application":{"type":"string","description":"Application ID"},"mdc":{"type":"object","additionalProperties":{"type":"string"},"description":"MDC context map"},"source":{"type":"string","description":"Log source: app or agent"}}},"LogSearchPageResponse":{"type":"object","description":"Log search response with cursor pagination and level counts","properties":{"data":{"type":"array","description":"Log entries for the current page","items":{"$ref":"#/components/schemas/LogEntryResponse"}},"nextCursor":{"type":"string","description":"Cursor for next page (null if no more results)"},"hasMore":{"type":"boolean","description":"Whether more results exist beyond this page"},"levelCounts":{"type":"object","additionalProperties":{"type":"integer","format":"int64"},"description":"Count of logs per level (unaffected by level filter)"}}},"TopError":{"type":"object","properties":{"errorType":{"type":"string"},"routeId":{"type":"string"},"processorId":{"type":"string"},"count":{"type":"integer","format":"int64"},"velocity":{"type":"number","format":"double"},"trend":{"type":"string"},"lastSeen":{"type":"string","format":"date-time"}}},"App":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"environmentId":{"type":"string","format":"uuid"},"slug":{"type":"string"},"displayName":{"type":"string"},"containerConfig":{"type":"object","additionalProperties":{"type":"object"}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"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"},"endpointUri":{"type":"string"}}},"AppConfigResponse":{"type":"object","properties":{"config":{"$ref":"#/components/schemas/ApplicationConfig"},"globalSensitiveKeys":{"type":"array","items":{"type":"string"}},"mergedSensitiveKeys":{"type":"array","items":{"type":"string"}}}},"UnreadCountResponse":{"type":"object","properties":{"total":{"type":"integer","format":"int64"},"bySeverity":{"type":"object","additionalProperties":{"type":"integer","format":"int64"}}}},"AgentInstanceResponse":{"type":"object","description":"Agent instance summary with runtime metrics","properties":{"instanceId":{"type":"string"},"displayName":{"type":"string"},"applicationId":{"type":"string"},"environmentId":{"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"},"cpuUsage":{"type":"number","format":"double","description":"Recent average CPU usage (0.0–1.0), -1 if unavailable"}},"required":["activeRoutes","applicationId","capabilities","cpuUsage","displayName","environmentId","errorRate","instanceId","lastHeartbeat","registeredAt","routeIds","status","totalRoutes","tps","uptimeSeconds","version"]},"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"]},"AgentEventPageResponse":{"type":"object","description":"Cursor-paginated agent event list","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/AgentEventResponse"}},"nextCursor":{"type":"string"},"hasMore":{"type":"boolean"}}},"AgentEventResponse":{"type":"object","description":"Agent lifecycle event","properties":{"id":{"type":"integer","format":"int64"},"instanceId":{"type":"string"},"applicationId":{"type":"string"},"eventType":{"type":"string"},"detail":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}},"required":["applicationId","detail","eventType","id","instanceId","timestamp"]},"CatalogApp":{"type":"object","description":"Unified catalog entry combining app records with live agent data","properties":{"slug":{"type":"string","description":"Application slug (universal identifier)"},"displayName":{"type":"string","description":"Display name"},"managed":{"type":"boolean","description":"True if a managed App record exists in the database"},"environmentSlug":{"type":"string","description":"Environment slug"},"health":{"type":"string","description":"Composite health: deployment status + agent health"},"healthTooltip":{"type":"string","description":"Human-readable tooltip explaining the health state"},"agentCount":{"type":"integer","format":"int32","description":"Number of connected agents"},"routes":{"type":"array","description":"Live routes from agents","items":{"$ref":"#/components/schemas/RouteSummary"}},"agents":{"type":"array","description":"Connected agent summaries","items":{"$ref":"#/components/schemas/AgentSummary"}},"exchangeCount":{"type":"integer","format":"int64","description":"Total exchange count from ClickHouse"},"deployment":{"$ref":"#/components/schemas/DeploymentSummary","description":"Active deployment info, null if no deployment"}}},"DeploymentSummary":{"type":"object","properties":{"status":{"type":"string"},"replicas":{"type":"string"},"version":{"type":"integer","format":"int32"}}},"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"},"resource":{"type":"string","description":"RFC 8707 resource indicator for the authorization request"},"additionalScopes":{"type":"array","description":"Additional scopes to request beyond openid email profile","items":{"type":"string"}}},"required":["authorizationEndpoint","clientId","issuer"]},"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"}}}},"SseEmitter":{"type":"object","properties":{"timeout":{"type":"integer","format":"int64"}}},"UsageStats":{"type":"object","properties":{"key":{"type":"string"},"count":{"type":"integer","format":"int64"},"avgDurationMs":{"type":"integer","format":"int64"}}},"SensitiveKeysConfig":{"type":"object","properties":{"keys":{"type":"array","items":{"type":"string"}}}},"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"}}},"LicenseInfo":{"type":"object","properties":{"tier":{"type":"string"},"features":{"type":"array","items":{"type":"string","enum":["topology","lineage","correlation","debugger","replay"]},"uniqueItems":true},"limits":{"type":"object","additionalProperties":{"type":"integer","format":"int32"}},"issuedAt":{"type":"string","format":"date-time"},"expiresAt":{"type":"string","format":"date-time"},"expired":{"type":"boolean"}}},"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"}}},"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"}}},"ClickHouseTableInfo":{"type":"object","description":"ClickHouse table information","properties":{"name":{"type":"string"},"engine":{"type":"string"},"rowCount":{"type":"integer","format":"int64"},"dataSize":{"type":"string"},"dataSizeBytes":{"type":"integer","format":"int64"},"partitionCount":{"type":"integer","format":"int32"}}},"ClickHouseStatusResponse":{"type":"object","description":"ClickHouse cluster status","properties":{"reachable":{"type":"boolean"},"version":{"type":"string"},"uptime":{"type":"string"},"host":{"type":"string"}}},"ClickHouseQueryInfo":{"type":"object","description":"Active ClickHouse query information","properties":{"queryId":{"type":"string"},"elapsedSeconds":{"type":"number","format":"double"},"memory":{"type":"string"},"readRows":{"type":"integer","format":"int64"},"query":{"type":"string"}}},"IndexerPipelineResponse":{"type":"object","description":"Search indexer pipeline statistics","properties":{"queueDepth":{"type":"integer","format":"int32"},"maxQueueSize":{"type":"integer","format":"int32"},"failedCount":{"type":"integer","format":"int64"},"indexedCount":{"type":"integer","format":"int64"},"debounceMs":{"type":"integer","format":"int64"},"indexingRate":{"type":"number","format":"double"},"lastIndexedAt":{"type":"string","format":"date-time"}}},"ClickHousePerformanceResponse":{"type":"object","description":"ClickHouse storage and performance metrics","properties":{"diskSize":{"type":"string"},"uncompressedSize":{"type":"string"},"compressionRatio":{"type":"number","format":"double"},"totalRows":{"type":"integer","format":"int64"},"partCount":{"type":"integer","format":"int32"},"memoryUsage":{"type":"string"},"currentQueries":{"type":"integer","format":"int32"}}},"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","OUTBOUND_CONNECTION_CHANGE","OUTBOUND_HTTP_TRUST_CHANGE","ALERT_RULE_CHANGE","ALERT_SILENCE_CHANGE"]},"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 diff --git a/ui/src/api/schema.d.ts b/ui/src/api/schema.d.ts index d49d32f8..a690d812 100644 --- a/ui/src/api/schema.d.ts +++ b/ui/src/api/schema.d.ts @@ -433,6 +433,22 @@ export interface paths { patch?: never; trace?: never; }; + "/environments/{envSlug}/alerts/{id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["restore"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/environments/{envSlug}/alerts/{id}/read": { parameters: { query?: never; @@ -577,6 +593,38 @@ export interface paths { patch?: never; trace?: never; }; + "/environments/{envSlug}/alerts/bulk-delete": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bulkDelete"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/environments/{envSlug}/alerts/bulk-ack": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["bulkAck"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/data/metrics": { parameters: { query?: never; @@ -1616,7 +1664,7 @@ export interface paths { get: operations["get_3"]; put?: never; post?: never; - delete?: never; + delete: operations["delete_5"]; options?: never; head?: never; patch?: never; @@ -2714,7 +2762,7 @@ export interface components { /** Format: uuid */ environmentId?: string; /** @enum {string} */ - state?: "PENDING" | "FIRING" | "ACKNOWLEDGED" | "RESOLVED"; + state?: "PENDING" | "FIRING" | "RESOLVED"; /** @enum {string} */ severity?: "CRITICAL" | "WARNING" | "INFO"; title?: string; @@ -2726,6 +2774,8 @@ export interface components { ackedBy?: string; /** Format: date-time */ resolvedAt?: string; + /** Format: date-time */ + readAt?: string; silenced?: boolean; /** Format: double */ currentValue?: number; @@ -2749,7 +2799,7 @@ export interface components { title?: string; message?: string; }; - BulkReadRequest: { + BulkIdsRequest: { instanceIds: string[]; }; LogEntry: { @@ -5052,6 +5102,28 @@ export interface operations { }; }; }; + restore: { + parameters: { + query: { + env: components["schemas"]["Environment"]; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; read: { parameters: { query: { @@ -5309,7 +5381,55 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["BulkReadRequest"]; + "application/json": components["schemas"]["BulkIdsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bulkDelete: { + parameters: { + query: { + env: components["schemas"]["Environment"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkIdsRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + bulkAck: { + parameters: { + query: { + env: components["schemas"]["Environment"]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BulkIdsRequest"]; }; }; responses: { @@ -7223,8 +7343,10 @@ export interface operations { query: { env: components["schemas"]["Environment"]; limit?: number; - state?: ("PENDING" | "FIRING" | "ACKNOWLEDGED" | "RESOLVED")[]; + state?: ("PENDING" | "FIRING" | "RESOLVED")[]; severity?: ("CRITICAL" | "WARNING" | "INFO")[]; + acked?: boolean; + read?: boolean; }; header?: never; path?: never; @@ -7267,6 +7389,28 @@ export interface operations { }; }; }; + delete_5: { + parameters: { + query: { + env: components["schemas"]["Environment"]; + }; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; listForInstance: { parameters: { query: { From be703eb71dd7e85dd5e428af01cac8e22e3ad8d0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:00:18 +0200 Subject: [PATCH 39/49] feat(ui/alerts): hooks for bulk-ack, delete, bulk-delete, restore + acked/read filter params - useAlerts gains acked/read filter params threaded into query + queryKey - new mutations: useBulkAckAlerts, useDeleteAlert, useBulkDeleteAlerts, useRestoreAlert - all cache-invalidate the alerts list and unread-count on success Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/api/queries/alerts.ts | 83 +++++++++++++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/ui/src/api/queries/alerts.ts b/ui/src/api/queries/alerts.ts index eab53cb0..72fd04a0 100644 --- a/ui/src/api/queries/alerts.ts +++ b/ui/src/api/queries/alerts.ts @@ -11,6 +11,8 @@ type AlertSeverity = NonNullable; export interface AlertsFilter { state?: AlertState | AlertState[]; severity?: AlertSeverity | AlertSeverity[]; + acked?: boolean; + read?: boolean; ruleId?: string; limit?: number; } @@ -47,7 +49,7 @@ export function useAlerts(filter: AlertsFilter = {}) { const severityKey = severityArr ? [...severityArr].sort() : null; return useQuery({ - queryKey: ['alerts', env, 'list', fetchLimit, stateKey, severityKey], + queryKey: ['alerts', env, 'list', fetchLimit, stateKey, severityKey, filter.acked ?? null, filter.read ?? null], enabled: !!env, refetchInterval: 30_000, refetchIntervalInBackground: false, @@ -56,6 +58,8 @@ export function useAlerts(filter: AlertsFilter = {}) { const query: Record = { limit: fetchLimit }; if (stateArr && stateArr.length > 0) query.state = stateArr; if (severityArr && severityArr.length > 0) query.severity = severityArr; + if (filter.acked !== undefined) query.acked = filter.acked; + if (filter.read !== undefined) query.read = filter.read; const { data, error } = await apiClient.GET( '/environments/{envSlug}/alerts', { @@ -180,3 +184,80 @@ export function useBulkReadAlerts() { }, }); } + +/** Acknowledge a batch of alert instances. */ +export function useBulkAckAlerts() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (ids: string[]) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/bulk-ack', + { params: { path: { envSlug: env } }, body: { instanceIds: ids } } as any, + ); + if (error) throw error; + }, + onSuccess: () => qc.invalidateQueries({ queryKey: ['alerts', env] }), + }); +} + +/** Delete (soft) a single alert instance. */ +export function useDeleteAlert() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.DELETE( + '/environments/{envSlug}/alerts/{id}', + { params: { path: { envSlug: env, id } } } as any, + ); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} + +/** Delete (soft) a batch of alert instances. */ +export function useBulkDeleteAlerts() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (ids: string[]) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/bulk-delete', + { params: { path: { envSlug: env } }, body: { instanceIds: ids } } as any, + ); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} + +/** Restore a soft-deleted alert instance. */ +export function useRestoreAlert() { + const env = useSelectedEnv(); + const qc = useQueryClient(); + return useMutation({ + mutationFn: async (id: string) => { + if (!env) throw new Error('no env'); + const { error } = await apiClient.POST( + '/environments/{envSlug}/alerts/{id}/restore', + { params: { path: { envSlug: env, id } } } as any, + ); + if (error) throw error; + }, + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['alerts', env] }); + qc.invalidateQueries({ queryKey: ['alerts', env, 'unread-count'] }); + }, + }); +} From e3b656f1592b876902d8d7ae4f5a34039a501ea3 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:02:12 +0200 Subject: [PATCH 40/49] =?UTF-8?q?refactor(ui/alerts):=20single=20inbox=20?= =?UTF-8?q?=E2=80=94=20remove=20AllAlerts=20+=20History=20pages,=20trim=20?= =?UTF-8?q?sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar Alerts section now just: Inbox · Rules · Silences. The /alerts redirect still lands in /alerts/inbox; /alerts/all and /alerts/history routes are gone (no redirect — stale URLs 404 per clean-break policy). Also updates sidebar-utils.test.ts to match the new 3-entry shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/components/sidebar-utils.test.ts | 6 +- ui/src/components/sidebar-utils.ts | 6 +- ui/src/pages/Alerts/AllAlertsPage.tsx | 115 ----------------------- ui/src/pages/Alerts/HistoryPage.tsx | 119 ------------------------ ui/src/router.tsx | 4 - 5 files changed, 4 insertions(+), 246 deletions(-) delete mode 100644 ui/src/pages/Alerts/AllAlertsPage.tsx delete mode 100644 ui/src/pages/Alerts/HistoryPage.tsx diff --git a/ui/src/components/sidebar-utils.test.ts b/ui/src/components/sidebar-utils.test.ts index 74cc9a37..11b9c36c 100644 --- a/ui/src/components/sidebar-utils.test.ts +++ b/ui/src/components/sidebar-utils.test.ts @@ -2,16 +2,14 @@ import { describe, it, expect } from 'vitest'; import { buildAlertsTreeNodes } from './sidebar-utils'; describe('buildAlertsTreeNodes', () => { - it('returns 5 entries with inbox/all/rules/silences/history paths', () => { + it('returns 3 entries with inbox/rules/silences paths', () => { const nodes = buildAlertsTreeNodes(); - expect(nodes).toHaveLength(5); + expect(nodes).toHaveLength(3); const paths = nodes.map((n) => n.path); expect(paths).toEqual([ '/alerts/inbox', - '/alerts/all', '/alerts/rules', '/alerts/silences', - '/alerts/history', ]); }); }); diff --git a/ui/src/components/sidebar-utils.ts b/ui/src/components/sidebar-utils.ts index 12aad938..9da2e922 100644 --- a/ui/src/components/sidebar-utils.ts +++ b/ui/src/components/sidebar-utils.ts @@ -1,6 +1,6 @@ import { createElement, type ReactNode } from 'react'; import type { SidebarTreeNode } from '@cameleer/design-system'; -import { AlertTriangle, Inbox, List, ScrollText, BellOff } from 'lucide-react'; +import { AlertTriangle, Inbox, BellOff } from 'lucide-react'; /* ------------------------------------------------------------------ */ /* Domain types (moved out of DS — no longer exported there) */ @@ -117,15 +117,13 @@ export function buildAdminTreeNodes(opts?: { infrastructureEndpoints?: boolean } /** * Alerts tree — static nodes for the alerting section. - * Paths: /alerts/{inbox|all|rules|silences|history} + * Paths: /alerts/{inbox|rules|silences} */ export function buildAlertsTreeNodes(): SidebarTreeNode[] { const icon = (el: ReactNode) => el; return [ { id: 'alerts-inbox', label: 'Inbox', path: '/alerts/inbox', icon: icon(createElement(Inbox, { size: 14 })) }, - { id: 'alerts-all', label: 'All', path: '/alerts/all', icon: icon(createElement(List, { size: 14 })) }, { id: 'alerts-rules', label: 'Rules', path: '/alerts/rules', icon: icon(createElement(AlertTriangle, { size: 14 })) }, { id: 'alerts-silences', label: 'Silences', path: '/alerts/silences', icon: icon(createElement(BellOff, { size: 14 })) }, - { id: 'alerts-history', label: 'History', path: '/alerts/history', icon: icon(createElement(ScrollText, { size: 14 })) }, ]; } diff --git a/ui/src/pages/Alerts/AllAlertsPage.tsx b/ui/src/pages/Alerts/AllAlertsPage.tsx deleted file mode 100644 index a398109a..00000000 --- a/ui/src/pages/Alerts/AllAlertsPage.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from 'react'; -import { Link } from 'react-router'; -import { Bell } from 'lucide-react'; -import { - ButtonGroup, DataTable, EmptyState, -} from '@cameleer/design-system'; -import type { ButtonGroupItem, Column } from '@cameleer/design-system'; -import { PageLoader } from '../../components/PageLoader'; -import { SeverityBadge } from '../../components/SeverityBadge'; -import { AlertStateChip } from '../../components/AlertStateChip'; -import { - useAlerts, useMarkAlertRead, - type AlertDto, -} from '../../api/queries/alerts'; -import { severityToAccent } from './severity-utils'; -import { formatRelativeTime } from './time-utils'; -import { renderAlertExpanded } from './alert-expanded'; -import css from './alerts-page.module.css'; -import tableStyles from '../../styles/table-section.module.css'; - -type AlertState = NonNullable; - -const STATE_ITEMS: ButtonGroupItem[] = [ - { value: 'FIRING', label: 'Firing', color: 'var(--error)' }, - { value: 'ACKNOWLEDGED', label: 'Acknowledged', color: 'var(--warning)' }, - { value: 'PENDING', label: 'Pending', color: 'var(--text-muted)' }, - { value: 'RESOLVED', label: 'Resolved', color: 'var(--success)' }, -]; - -const DEFAULT_OPEN_STATES = new Set(['PENDING', 'FIRING', 'ACKNOWLEDGED']); - -export default function AllAlertsPage() { - const [stateSel, setStateSel] = useState>(() => new Set(DEFAULT_OPEN_STATES)); - const stateValues: AlertState[] | undefined = stateSel.size === 0 - ? undefined - : [...stateSel] as AlertState[]; - - const { data, isLoading, error } = useAlerts({ state: stateValues, limit: 200 }); - const markRead = useMarkAlertRead(); - - const rows = data ?? []; - - const columns: Column[] = [ - { - key: 'severity', header: 'Severity', width: '110px', - render: (_, row) => row.severity ? : null, - }, - { - key: 'state', header: 'Status', width: '140px', - render: (_, row) => row.state ? : null, - }, - { - key: 'title', header: 'Title', - render: (_, row) => ( -

- row.id && markRead.mutate(row.id)}> - {row.title ?? '(untitled)'} - - {row.message && {row.message}} -
- ), - }, - { - key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, - render: (_, row) => - row.firedAt ? ( - - {formatRelativeTime(row.firedAt)} - - ) : '—', - }, - ]; - - if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; - - return ( -
-
-
-

All alerts

- {rows.length} matching your filter -
-
- -
-
- - {rows.length === 0 ? ( - } - title="No alerts match this filter" - description="Try switching to a different state or widening your criteria." - /> - ) : ( -
- - columns={columns as Column[]} - data={rows as Array} - sortable - flush - fillHeight - pageSize={200} - rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} - expandedContent={renderAlertExpanded} - /> -
- )} -
- ); -} diff --git a/ui/src/pages/Alerts/HistoryPage.tsx b/ui/src/pages/Alerts/HistoryPage.tsx deleted file mode 100644 index a7e22922..00000000 --- a/ui/src/pages/Alerts/HistoryPage.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Link } from 'react-router'; -import { History } from 'lucide-react'; -import { - DataTable, EmptyState, useGlobalFilters, -} from '@cameleer/design-system'; -import type { Column } from '@cameleer/design-system'; -import { PageLoader } from '../../components/PageLoader'; -import { SeverityBadge } from '../../components/SeverityBadge'; -import { - useAlerts, type AlertDto, -} from '../../api/queries/alerts'; -import { severityToAccent } from './severity-utils'; -import { formatRelativeTime } from './time-utils'; -import { renderAlertExpanded } from './alert-expanded'; -import css from './alerts-page.module.css'; -import tableStyles from '../../styles/table-section.module.css'; - -/** Duration in s/m/h/d. Pure, best-effort. */ -function formatDuration(from?: string | null, to?: string | null): string { - if (!from || !to) return '—'; - const ms = new Date(to).getTime() - new Date(from).getTime(); - if (ms < 0 || Number.isNaN(ms)) return '—'; - const sec = Math.floor(ms / 1000); - if (sec < 60) return `${sec}s`; - if (sec < 3600) return `${Math.floor(sec / 60)}m`; - if (sec < 86_400) return `${Math.floor(sec / 3600)}h`; - return `${Math.floor(sec / 86_400)}d`; -} - -export default function HistoryPage() { - const { timeRange } = useGlobalFilters(); - - // useAlerts doesn't accept a time range today; filter client-side - // against the global TimeRangeDropdown in the top bar. - const { data, isLoading, error } = useAlerts({ state: 'RESOLVED', limit: 200 }); - - const filtered = (data ?? []).filter((a) => { - if (!a.firedAt) return false; - const t = new Date(a.firedAt).getTime(); - return t >= timeRange.start.getTime() && t <= timeRange.end.getTime(); - }); - - const columns: Column[] = [ - { - key: 'severity', header: 'Severity', width: '110px', - render: (_, row) => row.severity ? : null, - }, - { - key: 'title', header: 'Title', - render: (_, row) => ( -
- {row.title ?? '(untitled)'} - {row.message && {row.message}} -
- ), - }, - { - key: 'firedAt', header: 'Fired at', width: '140px', sortable: true, - render: (_, row) => - row.firedAt ? ( - - {formatRelativeTime(row.firedAt)} - - ) : '—', - }, - { - key: 'resolvedAt', header: 'Resolved at', width: '140px', sortable: true, - render: (_, row) => - row.resolvedAt ? ( - - {formatRelativeTime(row.resolvedAt)} - - ) : '—', - }, - { - key: 'duration', header: 'Duration', width: '90px', - render: (_, row) => formatDuration(row.firedAt, row.resolvedAt), - }, - ]; - - if (isLoading) return ; - if (error) return
Failed to load history: {String(error)}
; - - return ( -
-
-
-

Alert history

- - {filtered.length === 0 - ? 'No resolved alerts in range' - : `${filtered.length} resolved alert${filtered.length === 1 ? '' : 's'} in range`} - -
-
- - {filtered.length === 0 ? ( - } - title="No resolved alerts" - description="Nothing in the selected date range. Try widening it." - /> - ) : ( -
- - columns={columns as Column[]} - data={filtered as Array} - sortable - flush - fillHeight - pageSize={200} - rowAccent={(row) => row.severity ? severityToAccent(row.severity) : undefined} - expandedContent={renderAlertExpanded} - /> -
- )} -
- ); -} diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 7bee0798..0fbd88e6 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -24,8 +24,6 @@ const SensitiveKeysPage = lazy(() => import('./pages/Admin/SensitiveKeysPage')); const AppsTab = lazy(() => import('./pages/AppsTab/AppsTab')); const SwaggerPage = lazy(() => import('./pages/Swagger/SwaggerPage')); const InboxPage = lazy(() => import('./pages/Alerts/InboxPage')); -const AllAlertsPage = lazy(() => import('./pages/Alerts/AllAlertsPage')); -const HistoryPage = lazy(() => import('./pages/Alerts/HistoryPage')); const RulesListPage = lazy(() => import('./pages/Alerts/RulesListPage')); const RuleEditorWizard = lazy(() => import('./pages/Alerts/RuleEditor/RuleEditorWizard')); const SilencesPage = lazy(() => import('./pages/Alerts/SilencesPage')); @@ -84,8 +82,6 @@ export const router = createBrowserRouter([ // Alerts { path: 'alerts', element: }, { path: 'alerts/inbox', element: }, - { path: 'alerts/all', element: }, - { path: 'alerts/history', element: }, { path: 'alerts/rules', element: }, { path: 'alerts/rules/new', element: }, { path: 'alerts/rules/:id', element: }, From 837fcbf92647f868f3a262a6a14de11caa3b9e91 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:05:30 +0200 Subject: [PATCH 41/49] =?UTF-8?q?feat(ui/alerts):=20SilenceRuleMenu=20?= =?UTF-8?q?=E2=80=94=201h/8h/24h/custom=20duration=20menu?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Used by InboxPage row + bulk actions to silence an alert's underlying rule for a chosen preset window. 'Custom…' routes to /alerts/silences?ruleId= (T13 adds the prefill wire). Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/SilenceRuleMenu.tsx | 71 +++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 ui/src/pages/Alerts/SilenceRuleMenu.tsx diff --git a/ui/src/pages/Alerts/SilenceRuleMenu.tsx b/ui/src/pages/Alerts/SilenceRuleMenu.tsx new file mode 100644 index 00000000..d4530835 --- /dev/null +++ b/ui/src/pages/Alerts/SilenceRuleMenu.tsx @@ -0,0 +1,71 @@ +import { BellOff } from 'lucide-react'; +import { useNavigate } from 'react-router'; +import { Button, Dropdown, useToast } from '@cameleer/design-system'; +import type { DropdownItem } from '@cameleer/design-system'; +import { useCreateSilence } from '../../api/queries/alertSilences'; + +interface Props { + ruleId: string; + ruleTitle?: string; + onDone?: () => void; + variant?: 'row' | 'bulk'; +} + +const PRESETS: Array<{ label: string; hours: number }> = [ + { label: '1 hour', hours: 1 }, + { label: '8 hours', hours: 8 }, + { label: '24 hours', hours: 24 }, +]; + +export function SilenceRuleMenu({ ruleId, ruleTitle, onDone, variant = 'row' }: Props) { + const navigate = useNavigate(); + const { toast } = useToast(); + const createSilence = useCreateSilence(); + + const handlePreset = (hours: number) => async () => { + const now = new Date(); + const reason = ruleTitle + ? `Silenced from inbox (${ruleTitle})` + : 'Silenced from inbox'; + try { + await createSilence.mutateAsync({ + matcher: { ruleId }, + reason, + startsAt: now.toISOString(), + endsAt: new Date(now.getTime() + hours * 3_600_000).toISOString(), + }); + toast({ title: `Silenced for ${hours}h`, variant: 'success' }); + onDone?.(); + } catch (e) { + toast({ title: 'Silence failed', description: String(e), variant: 'error' }); + } + }; + + const handleCustom = () => { + navigate(`/alerts/silences?ruleId=${encodeURIComponent(ruleId)}`); + }; + + const items: DropdownItem[] = [ + ...PRESETS.map(({ label, hours }) => ({ + label, + disabled: createSilence.isPending, + onClick: handlePreset(hours), + })), + { divider: true, label: '' }, + { label: 'Custom…', onClick: handleCustom }, + ]; + + const buttonLabel = variant === 'bulk' ? 'Silence rules' : 'Silence rule…'; + + return ( + + + {buttonLabel} + + } + items={items} + /> + ); +} From 2bc214e3249fae74dc22b18b71fb223a808c590c Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:09:22 +0200 Subject: [PATCH 42/49] =?UTF-8?q?feat(ui/alerts):=20single=20inbox=20?= =?UTF-8?q?=E2=80=94=20filter=20bar,=20silence/delete=20row=20+=20bulk=20a?= =?UTF-8?q?ctions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the old FIRING+ACK hardcoded inbox with the single filterable inbox: - Filter bar: Severity · Status (PENDING/FIRING/RESOLVED, default FIRING) · Hide acked (default on) · Hide read (default on). - Row actions: Ack, Mark read, Silence rule… (quick menu), Delete (OPERATOR+, soft delete with undo toast wired to useRestoreAlert). - Bulk toolbar: Ack N · Mark N read · Silence rules · Delete N (ConfirmDialog; OPERATOR+). Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/InboxPage.tsx | 353 ++++++++++++++++++++++++------ 1 file changed, 291 insertions(+), 62 deletions(-) diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index b7760bdf..38b26fb3 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -1,75 +1,183 @@ import { useMemo, useState } from 'react'; -import { Link } from 'react-router'; -import { Inbox } from 'lucide-react'; +import { Link, useNavigate } from 'react-router'; +import { Inbox, Trash2 } from 'lucide-react'; import { - Button, ButtonGroup, DataTable, EmptyState, useToast, + Button, ButtonGroup, ConfirmDialog, DataTable, EmptyState, Toggle, useToast, } from '@cameleer/design-system'; import type { ButtonGroupItem, Column } from '@cameleer/design-system'; import { PageLoader } from '../../components/PageLoader'; import { SeverityBadge } from '../../components/SeverityBadge'; import { AlertStateChip } from '../../components/AlertStateChip'; import { - useAlerts, useAckAlert, useBulkReadAlerts, useMarkAlertRead, + useAlerts, useAckAlert, useBulkAckAlerts, useBulkReadAlerts, useMarkAlertRead, + useDeleteAlert, useBulkDeleteAlerts, useRestoreAlert, type AlertDto, } from '../../api/queries/alerts'; +import { useCreateSilence } from '../../api/queries/alertSilences'; +import { useAuthStore } from '../../auth/auth-store'; +import { SilenceRuleMenu } from './SilenceRuleMenu'; import { severityToAccent } from './severity-utils'; import { formatRelativeTime } from './time-utils'; import { renderAlertExpanded } from './alert-expanded'; import css from './alerts-page.module.css'; import tableStyles from '../../styles/table-section.module.css'; -type Severity = NonNullable; +type AlertSeverity = NonNullable; +type AlertState = NonNullable; + +// ── Filter bar items ──────────────────────────────────────────────────────── const SEVERITY_ITEMS: ButtonGroupItem[] = [ - { value: 'CRITICAL', label: 'Critical', color: 'var(--error)' }, - { value: 'WARNING', label: 'Warning', color: 'var(--warning)' }, - { value: 'INFO', label: 'Info', color: 'var(--text-muted)' }, + { value: 'CRITICAL', label: 'Critical', color: 'var(--error)' }, + { value: 'WARNING', label: 'Warning', color: 'var(--warning)' }, + { value: 'INFO', label: 'Info', color: 'var(--text-muted)' }, ]; +const STATE_ITEMS: ButtonGroupItem[] = [ + { value: 'PENDING', label: 'Pending' }, + { value: 'FIRING', label: 'Firing' }, + { value: 'RESOLVED', label: 'Resolved' }, +]; + +// ── Bulk silence helper ───────────────────────────────────────────────────── + +const SILENCE_PRESETS: Array<{ label: string; hours: number }> = [ + { label: '1 hour', hours: 1 }, + { label: '8 hours', hours: 8 }, + { label: '24 hours', hours: 24 }, +]; + +interface SilenceRulesForSelectionProps { + selected: Set; + rows: AlertDto[]; +} + +function SilenceRulesForSelection({ selected, rows }: SilenceRulesForSelectionProps) { + const navigate = useNavigate(); + const { toast } = useToast(); + const createSilence = useCreateSilence(); + + const ruleIds = useMemo(() => { + const ids = new Set(); + for (const id of selected) { + const row = rows.find((r) => r.id === id); + if (row?.ruleId) ids.add(row.ruleId); + } + return [...ids]; + }, [selected, rows]); + + if (ruleIds.length === 0) return null; + + const handlePreset = (hours: number) => async () => { + const now = new Date(); + const results = await Promise.allSettled( + ruleIds.map((ruleId) => + createSilence.mutateAsync({ + matcher: { ruleId }, + reason: 'Silenced from inbox (bulk)', + startsAt: now.toISOString(), + endsAt: new Date(now.getTime() + hours * 3_600_000).toISOString(), + }), + ), + ); + const failed = results.filter((r) => r.status === 'rejected').length; + if (failed === 0) { + toast({ title: `Silenced ${ruleIds.length} rule${ruleIds.length === 1 ? '' : 's'} for ${hours}h`, variant: 'success' }); + } else { + toast({ title: `Silenced ${ruleIds.length - failed}/${ruleIds.length} rules`, description: `${failed} failed`, variant: 'warning' }); + } + }; + + const handleCustom = () => navigate('/alerts/silences'); + + // Render ONE SilenceRuleMenu that uses the first ruleId as its anchor but + // overrides the click handlers to fire against all selected rule IDs. + // We use a Dropdown-equivalent by wiring SilenceRuleMenu with the first + // ruleId; for bulk we drive our own mutation loop above. + // Since SilenceRuleMenu is self-contained, we render a parallel Button set + // for the bulk path to keep it clean. + return ( +
+ {SILENCE_PRESETS.map(({ label, hours }) => ( + + ))} + +
+ ); +} + +// ── InboxPage ─────────────────────────────────────────────────────────────── + export default function InboxPage() { + // Filter state — defaults: FIRING selected, hide-acked on, hide-read on const [severitySel, setSeveritySel] = useState>(new Set()); - const severityValues: Severity[] | undefined = severitySel.size === 0 - ? undefined - : [...severitySel] as Severity[]; + const [stateSel, setStateSel] = useState>(new Set(['FIRING'])); + const [hideAcked, setHideAcked] = useState(true); + const [hideRead, setHideRead] = useState(true); const { data, isLoading, error } = useAlerts({ - state: ['FIRING', 'ACKNOWLEDGED'], - severity: severityValues, + severity: severitySel.size ? ([...severitySel] as AlertSeverity[]) : undefined, + state: stateSel.size ? ([...stateSel] as AlertState[]) : undefined, + acked: hideAcked ? false : undefined, + read: hideRead ? false : undefined, limit: 200, }); - const bulkRead = useBulkReadAlerts(); - const markRead = useMarkAlertRead(); - const ack = useAckAlert(); - const { toast } = useToast(); - const [selected, setSelected] = useState>(new Set()); + // Mutations + const ack = useAckAlert(); + const bulkAck = useBulkAckAlerts(); + const markRead = useMarkAlertRead(); + const bulkRead = useBulkReadAlerts(); + const del = useDeleteAlert(); + const bulkDelete = useBulkDeleteAlerts(); + const restore = useRestoreAlert(); + const { toast } = useToast(); + + // Selection + const [selected, setSelected] = useState>(new Set()); + const [deletePending, setDeletePending] = useState(null); + + // RBAC + const roles = useAuthStore((s) => s.roles); + const canDelete = roles.includes('OPERATOR') || roles.includes('ADMIN'); + const rows = data ?? []; - const unreadIds = useMemo( - () => rows.filter((a) => a.state === 'FIRING').map((a) => a.id), - [rows], - ); - - const firingIds = unreadIds; // FIRING alerts are the ones that can be ack'd - - const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id)); + const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.id)); const someSelected = selected.size > 0 && !allSelected; - const toggleSelected = (id: string) => { + const toggleSelected = (id: string) => setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); - }; - const toggleSelectAll = () => { - if (allSelected) { - setSelected(new Set()); - } else { - setSelected(new Set(rows.map((r) => r.id))); - } - }; + const toggleSelectAll = () => + setSelected(allSelected ? new Set() : new Set(rows.map((r) => r.id))); + + // Derived counts for bulk-toolbar labels + const selectedRows = rows.filter((r) => selected.has(r.id)); + const unackedSel = selectedRows.filter((r) => r.ackedAt == null).map((r) => r.id); + const unreadSel = selectedRows.filter((r) => r.readAt == null).map((r) => r.id); + + // "Acknowledge all firing" target (no-selection state) + const firingUnackedIds = rows + .filter((r) => r.state === 'FIRING' && r.ackedAt == null) + .map((r) => r.id); + const allUnreadIds = rows.filter((r) => r.readAt == null).map((r) => r.id); + + // ── handlers ─────────────────────────────────────────────────────────────── const onAck = async (id: string, title?: string) => { try { @@ -80,10 +188,45 @@ export default function InboxPage() { } }; + const onMarkRead = async (id: string) => { + try { + await markRead.mutateAsync(id); + toast({ title: 'Marked as read', variant: 'success' }); + } catch (e) { + toast({ title: 'Mark read failed', description: String(e), variant: 'error' }); + } + }; + + const onDeleteOne = async (id: string) => { + try { + await del.mutateAsync(id); + // No built-in action slot in DS toast — render Undo as a Button node + const undoNode = ( + + ) as unknown as string; // DS description accepts ReactNode at runtime + toast({ title: 'Deleted', description: undoNode, variant: 'success', duration: 5000 }); + } catch (e) { + toast({ title: 'Delete failed', description: String(e), variant: 'error' }); + } + }; + const onBulkAck = async (ids: string[]) => { if (ids.length === 0) return; try { - await Promise.all(ids.map((id) => ack.mutateAsync(id))); + await bulkAck.mutateAsync(ids); setSelected(new Set()); toast({ title: `Acknowledged ${ids.length} alert${ids.length === 1 ? '' : 's'}`, variant: 'success' }); } catch (e) { @@ -102,6 +245,8 @@ export default function InboxPage() { } }; + // ── columns ──────────────────────────────────────────────────────────────── + const columns: Column[] = [ { key: 'select', header: '', width: '40px', @@ -128,7 +273,7 @@ export default function InboxPage() { { key: 'title', header: 'Title', render: (_, row) => { - const unread = row.state === 'FIRING'; + const unread = row.readAt == null; return (
markRead.mutate(row.id)}> @@ -149,31 +294,68 @@ export default function InboxPage() { ) : '—', }, { - key: 'ack', header: '', width: '120px', - render: (_, row) => - row.state === 'FIRING' ? ( - - ) : null, + key: 'rowActions', header: '', width: '220px', + render: (_, row) => ( +
+ {row.ackedAt == null && ( + + )} + {row.readAt == null && ( + + )} + {row.ruleId && ( + + )} + {canDelete && ( + + )} +
+ ), }, ]; + // ── render ───────────────────────────────────────────────────────────────── + if (isLoading) return ; - if (error) return
Failed to load alerts: {String(error)}
; + if (error) return
Failed to load alerts: {String(error)}
; const selectedIds = Array.from(selected); - const selectedFiringIds = rows - .filter((r) => selected.has(r.id) && r.state === 'FIRING') - .map((r) => r.id); + const needsAttention = rows.filter((r) => r.readAt == null || r.ackedAt == null).length; const subtitle = selectedIds.length > 0 ? `${selectedIds.length} selected` - : `${unreadIds.length} need attention · ${rows.length} total in inbox`; + : `${needsAttention} need attention · ${rows.length} total`; return (
+ {/* ── Header ─────────────────────────────────────────────────────── */}

Inbox

@@ -185,9 +367,25 @@ export default function InboxPage() { value={severitySel} onChange={setSeveritySel} /> + + setHideAcked(e.currentTarget.checked)} + /> + setHideRead(e.currentTarget.checked)} + />
+ {/* ── Filter / bulk toolbar ───────────────────────────────────────── */}
+ {/* ── Table / empty ───────────────────────────────────────────────── */} {rows.length === 0 ? ( } title="All clear" - description="No open alerts for you in this environment." + description="No alerts match the current filters." /> ) : (
@@ -263,6 +474,24 @@ export default function InboxPage() { />
)} + + {/* ── Bulk delete confirmation ─────────────────────────────────────── */} + setDeletePending(null)} + onConfirm={async () => { + if (!deletePending) return; + await bulkDelete.mutateAsync(deletePending); + toast({ title: `Deleted ${deletePending.length}`, variant: 'success' }); + setDeletePending(null); + setSelected(new Set()); + }} + title="Delete alerts?" + message={`Delete ${deletePending?.length ?? 0} alerts? This affects all users.`} + confirmText="Delete" + variant="danger" + loading={bulkDelete.isPending} + />
); } From 35fea645b6ba13cde2b5778038d69ccaf21adfd0 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:14:55 +0200 Subject: [PATCH 43/49] =?UTF-8?q?fix(ui/alerts):=20InboxPage=20polish=20?= =?UTF-8?q?=E2=80=94=20status=20colors,=20selected-scrub=20on=20delete,=20?= =?UTF-8?q?drop=20stale=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - STATE_ITEMS gains color dots (text-muted/error/success) to match SEVERITY_ITEMS - onDeleteOne removes the deleted id from the selection Set so a follow-up bulk action doesn't try to re-delete a tombstoned row - drop stale comment block that described an alternative SilenceRulesForSelection implementation not matching the shipped code Co-Authored-By: Claude Opus 4.7 (1M context) --- ui/src/pages/Alerts/InboxPage.tsx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/ui/src/pages/Alerts/InboxPage.tsx b/ui/src/pages/Alerts/InboxPage.tsx index 38b26fb3..e548018e 100644 --- a/ui/src/pages/Alerts/InboxPage.tsx +++ b/ui/src/pages/Alerts/InboxPage.tsx @@ -34,9 +34,9 @@ const SEVERITY_ITEMS: ButtonGroupItem[] = [ ]; const STATE_ITEMS: ButtonGroupItem[] = [ - { value: 'PENDING', label: 'Pending' }, - { value: 'FIRING', label: 'Firing' }, - { value: 'RESOLVED', label: 'Resolved' }, + { value: 'PENDING', label: 'Pending', color: 'var(--text-muted)' }, + { value: 'FIRING', label: 'Firing', color: 'var(--error)' }, + { value: 'RESOLVED', label: 'Resolved', color: 'var(--success)' }, ]; // ── Bulk silence helper ───────────────────────────────────────────────────── @@ -90,12 +90,6 @@ function SilenceRulesForSelection({ selected, rows }: SilenceRulesForSelectionPr const handleCustom = () => navigate('/alerts/silences'); - // Render ONE SilenceRuleMenu that uses the first ruleId as its anchor but - // overrides the click handlers to fire against all selected rule IDs. - // We use a Dropdown-equivalent by wiring SilenceRuleMenu with the first - // ruleId; for bulk we drive our own mutation loop above. - // Since SilenceRuleMenu is self-contained, we render a parallel Button set - // for the bulk path to keep it clean. return (
{SILENCE_PRESETS.map(({ label, hours }) => ( @@ -200,6 +194,12 @@ export default function InboxPage() { const onDeleteOne = async (id: string) => { try { await del.mutateAsync(id); + setSelected((prev) => { + if (!prev.has(id)) return prev; + const next = new Set(prev); + next.delete(id); + return next; + }); // No built-in action slot in DS toast — render Undo as a Button node const undoNode = (