diff --git a/AGENTS.md b/AGENTS.md index 08b4f635..3ea13f48 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-server** (8524 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** (8527 symbols, 22174 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/CLAUDE.md b/CLAUDE.md index 14b2d32d..77c14ce6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,7 +97,7 @@ When adding, removing, or renaming classes, controllers, endpoints, UI component # GitNexus — Code Intelligence -This project is indexed by GitNexus as **cameleer-server** (8524 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** (8527 symbols, 22174 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/ui/src/api/alerting-enums.ts b/ui/src/api/alerting-enums.ts new file mode 100644 index 00000000..cbd5a344 --- /dev/null +++ b/ui/src/api/alerting-enums.ts @@ -0,0 +1,109 @@ +/** + * Alerting option lists derived from the OpenAPI schema. + * + * Why this module exists: option arrays in condition-form components used to + * be hand-typed string literals, and they drifted silently from the backend + * enums (e.g. P95_LATENCY_MS appeared in the dropdown without a matching + * backend value; LATEST was exposed by the backend but never surfaced in the + * UI). Every dropdown value here is derived from a schema.d.ts union type and + * every label is required via `Record` — TypeScript will refuse to + * compile if the backend adds or removes a value and this file isn't updated. + * + * Workflow when an alerting enum changes on the backend: + * 1. `cd ui && npm run generate-api:live` (or `generate-api` after deploy). + * 2. The `Record<…, string>` maps below fail to type-check for any new or + * removed value. Fix the map. + * 3. Every consumer (`METRIC_OPTIONS`, …) regenerates automatically. + * + * Fields whose backend type is `String` rather than a typed enum (agent state, + * log level, deployment states, exchange filter status, JVM metric names) + * cannot be derived here today — springdoc emits them as open-ended strings. + * Follow-up: add `@Schema(allowableValues = …)` on the relevant Java record + * components so they land in schema.d.ts as unions, then fold them in here. + */ +import type { components } from './schema'; + +type AlertRuleRequest = components['schemas']['AlertRuleRequest']; +type RouteMetricCondition = components['schemas']['RouteMetricCondition']; +type JvmMetricCondition = components['schemas']['JvmMetricCondition']; +type ExchangeMatchCondition = components['schemas']['ExchangeMatchCondition']; +type AlertRuleTarget = components['schemas']['AlertRuleTarget']; + +export type ConditionKind = NonNullable; +export type Severity = NonNullable; +export type RouteMetric = NonNullable; +export type Comparator = NonNullable; +export type JvmAggregation = NonNullable; +export type ExchangeFireMode = NonNullable; +export type TargetKind = NonNullable; + +export interface Option { value: T; label: string } + +function toOptions(labels: Record): Option[] { + return (Object.keys(labels) as T[]).map((value) => ({ value, label: labels[value] })); +} + +// --------------------------------------------------------------------------- +// Label maps — one entry per backend value, TypeScript enforces exhaustiveness. +// --------------------------------------------------------------------------- + +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)', + DEPLOYMENT_STATE: 'Deployment state (FAILED / DEGRADED)', + LOG_PATTERN: 'Log pattern (count of matching logs)', + JVM_METRIC: 'JVM metric (heap, GC, inflight)', +}; + +const SEVERITY_LABELS: Record = { + CRITICAL: 'Critical', + WARNING: 'Warning', + INFO: 'Info', +}; + +const ROUTE_METRIC_LABELS: Record = { + ERROR_RATE: 'Error rate', + P99_LATENCY_MS: 'P99 latency (ms)', + AVG_DURATION_MS: 'Avg duration (ms)', + THROUGHPUT: 'Throughput (msg/s)', + ERROR_COUNT: 'Error count', +}; + +const COMPARATOR_LABELS: Record = { + GT: '>', + GTE: '\u2265', + LT: '<', + LTE: '\u2264', + EQ: '=', +}; + +const JVM_AGGREGATION_LABELS: Record = { + MAX: 'MAX', + MIN: 'MIN', + AVG: 'AVG', + LATEST: 'LATEST', +}; + +const EXCHANGE_FIRE_MODE_LABELS: Record = { + PER_EXCHANGE: 'One alert per matching exchange', + COUNT_IN_WINDOW: 'Threshold: N matches in window', +}; + +const TARGET_KIND_LABELS: Record = { + USER: 'User', + GROUP: 'Group', + ROLE: 'Role', +}; + +// --------------------------------------------------------------------------- +// Exported option arrays (in label-map declaration order). +// --------------------------------------------------------------------------- + +export const CONDITION_KIND_OPTIONS: Option[] = toOptions(CONDITION_KIND_LABELS); +export const SEVERITY_OPTIONS: Option[] = toOptions(SEVERITY_LABELS); +export const ROUTE_METRIC_OPTIONS: Option[] = toOptions(ROUTE_METRIC_LABELS); +export const COMPARATOR_OPTIONS: Option[] = toOptions(COMPARATOR_LABELS); +export const JVM_AGGREGATION_OPTIONS: Option[] = toOptions(JVM_AGGREGATION_LABELS); +export const EXCHANGE_FIRE_MODE_OPTIONS:Option[] = toOptions(EXCHANGE_FIRE_MODE_LABELS); +export const TARGET_KIND_OPTIONS: Option[] = toOptions(TARGET_KIND_LABELS); diff --git a/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx b/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx index a3625e10..50a188d1 100644 --- a/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx +++ b/ui/src/pages/Alerts/RuleEditor/ConditionStep.tsx @@ -6,15 +6,7 @@ import { AgentStateForm } from './condition-forms/AgentStateForm'; import { DeploymentStateForm } from './condition-forms/DeploymentStateForm'; import { LogPatternForm } from './condition-forms/LogPatternForm'; import { JvmMetricForm } from './condition-forms/JvmMetricForm'; - -const KIND_OPTIONS = [ - { 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: '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)' }, -]; +import { CONDITION_KIND_OPTIONS } from '../../../api/alerting-enums'; export function ConditionStep({ form, setForm }: { form: FormState; setForm: (f: FormState) => void }) { const onKindChange = (v: string) => { @@ -35,7 +27,7 @@ export function ConditionStep({ form, setForm }: { form: FormState; setForm: (f: patch({ fireMode: e.target.value })} - options={FIRE_MODES} + options={EXCHANGE_FIRE_MODE_OPTIONS} /> diff --git a/ui/src/pages/Alerts/RuleEditor/condition-forms/JvmMetricForm.tsx b/ui/src/pages/Alerts/RuleEditor/condition-forms/JvmMetricForm.tsx index 59b7bcd9..59d463da 100644 --- a/ui/src/pages/Alerts/RuleEditor/condition-forms/JvmMetricForm.tsx +++ b/ui/src/pages/Alerts/RuleEditor/condition-forms/JvmMetricForm.tsx @@ -1,5 +1,6 @@ import { FormField, Input, Select } from '@cameleer/design-system'; import type { FormState } from '../form-state'; +import { JVM_AGGREGATION_OPTIONS, COMPARATOR_OPTIONS } from '../../../../api/alerting-enums'; export function JvmMetricForm({ form, setForm }: { form: FormState; setForm: (f: FormState) => void }) { const c = form.condition as Record; @@ -19,23 +20,14 @@ export function JvmMetricForm({ form, setForm }: { form: FormState; setForm: (f: patch({ comparator: e.target.value })} - options={[ - { value: 'GT', label: '>' }, - { value: 'GTE', label: '\u2265' }, - { value: 'LT', label: '<' }, - { value: 'LTE', label: '\u2264' }, - ]} + options={COMPARATOR_OPTIONS} /> diff --git a/ui/src/pages/Alerts/RuleEditor/condition-forms/RouteMetricForm.tsx b/ui/src/pages/Alerts/RuleEditor/condition-forms/RouteMetricForm.tsx index 40287172..515e7fa0 100644 --- a/ui/src/pages/Alerts/RuleEditor/condition-forms/RouteMetricForm.tsx +++ b/ui/src/pages/Alerts/RuleEditor/condition-forms/RouteMetricForm.tsx @@ -1,21 +1,6 @@ import { FormField, Input, Select } from '@cameleer/design-system'; import type { FormState } from '../form-state'; - -// Mirrors cameleer-server-core RouteMetric enum — keep in sync. -const METRICS = [ - { value: 'ERROR_RATE', label: 'Error rate' }, - { value: 'P99_LATENCY_MS', label: 'P99 latency (ms)' }, - { value: 'AVG_DURATION_MS', label: 'Avg duration (ms)' }, - { value: 'THROUGHPUT', label: 'Throughput (msg/s)' }, - { value: 'ERROR_COUNT', label: 'Error count' }, -]; - -const COMPARATORS = [ - { value: 'GT', label: '>' }, - { value: 'GTE', label: '\u2265' }, - { value: 'LT', label: '<' }, - { value: 'LTE', label: '\u2264' }, -]; +import { ROUTE_METRIC_OPTIONS, COMPARATOR_OPTIONS } from '../../../../api/alerting-enums'; export function RouteMetricForm({ form, setForm }: { form: FormState; setForm: (f: FormState) => void }) { const c = form.condition as Record; @@ -28,14 +13,14 @@ export function RouteMetricForm({ form, setForm }: { form: FormState; setForm: ( patch({ comparator: e.target.value })} - options={COMPARATORS} + options={COMPARATOR_OPTIONS} /> diff --git a/ui/src/pages/Alerts/RuleEditor/form-state.ts b/ui/src/pages/Alerts/RuleEditor/form-state.ts index 72385302..71176fdb 100644 --- a/ui/src/pages/Alerts/RuleEditor/form-state.ts +++ b/ui/src/pages/Alerts/RuleEditor/form-state.ts @@ -4,6 +4,7 @@ import type { ConditionKind, AlertCondition, } from '../../../api/queries/alertRules'; +import type { Severity, TargetKind } from '../../../api/alerting-enums'; export type WizardStep = 'scope' | 'condition' | 'trigger' | 'notify' | 'review'; export const WIZARD_STEPS: WizardStep[] = ['scope', 'condition', 'trigger', 'notify', 'review']; @@ -11,7 +12,7 @@ export const WIZARD_STEPS: WizardStep[] = ['scope', 'condition', 'trigger', 'not export interface FormState { name: string; description: string; - severity: 'CRITICAL' | 'WARNING' | 'INFO'; + severity: Severity; enabled: boolean; // Scope (radio: env-wide | app | route | agent) @@ -36,7 +37,7 @@ export interface FormState { headerOverrides: Array<{ key: string; value: string }>; }>; - targets: Array<{ kind: 'USER' | 'GROUP' | 'ROLE'; targetId: string }>; + targets: Array<{ kind: TargetKind; targetId: string }>; } export function initialForm(existing?: AlertRuleResponse): FormState { @@ -102,7 +103,7 @@ export function initialForm(existing?: AlertRuleResponse): FormState { .map(([key, value]) => ({ key, value })), })), targets: (existing.targets ?? []).map((t) => ({ - kind: (t.kind ?? 'USER') as 'USER' | 'GROUP' | 'ROLE', + kind: (t.kind ?? 'USER') as TargetKind, targetId: t.targetId ?? '', })), };