import type { AlertRuleRequest, AlertRuleResponse, AlertCondition, } from '../../../api/queries/alertRules'; import type { ConditionKind, Severity, TargetKind } from '../enums'; export type WizardStep = 'scope' | 'condition' | 'trigger' | 'notify' | 'review'; export const WIZARD_STEPS: WizardStep[] = ['scope', 'condition', 'trigger', 'notify', 'review']; export interface FormState { name: string; description: string; severity: Severity; enabled: boolean; // Scope (radio: env-wide | app | route | agent) scopeKind: 'env' | 'app' | 'route' | 'agent'; appSlug: string; routeId: string; agentId: string; conditionKind: ConditionKind; condition: Partial; evaluationIntervalSeconds: number; forDurationSeconds: number; reNotifyMinutes: number; notificationTitleTmpl: string; notificationMessageTmpl: string; webhooks: Array<{ outboundConnectionId: string; bodyOverride: string; headerOverrides: Array<{ key: string; value: string }>; }>; targets: Array<{ kind: TargetKind; targetId: string }>; } export function initialForm(existing?: AlertRuleResponse): FormState { if (!existing) { return { name: '', description: '', severity: 'WARNING', enabled: true, scopeKind: 'env', appSlug: '', routeId: '', agentId: '', conditionKind: 'ROUTE_METRIC', // Pre-populate a valid ROUTE_METRIC default so a rule can be saved without // the user needing to fill in every condition field. Values chosen to be // sane for "error rate" alerts on almost any route. condition: { kind: 'ROUTE_METRIC', scope: {}, metric: 'ERROR_RATE', comparator: 'GT', threshold: 0.05, windowSeconds: 300, } as unknown as Partial, evaluationIntervalSeconds: 60, forDurationSeconds: 0, reNotifyMinutes: 60, notificationTitleTmpl: '{{rule.name}} is firing', notificationMessageTmpl: 'Alert {{alert.id}} fired at {{alert.firedAt}}', webhooks: [], targets: [], }; } const scope = ((existing.condition as { scope?: { appSlug?: string; routeId?: string; agentId?: string } } | undefined)?.scope) ?? {}; const scopeKind: FormState['scopeKind'] = scope.agentId ? 'agent' : scope.routeId ? 'route' : scope.appSlug ? 'app' : 'env'; return { name: existing.name ?? '', description: existing.description ?? '', severity: (existing.severity ?? 'WARNING') as FormState['severity'], enabled: existing.enabled ?? true, scopeKind, appSlug: scope.appSlug ?? '', routeId: scope.routeId ?? '', agentId: scope.agentId ?? '', conditionKind: (existing.conditionKind ?? 'ROUTE_METRIC') as ConditionKind, condition: (existing.condition ?? { kind: existing.conditionKind }) as Partial, evaluationIntervalSeconds: existing.evaluationIntervalSeconds ?? 60, forDurationSeconds: existing.forDurationSeconds ?? 0, reNotifyMinutes: existing.reNotifyMinutes ?? 60, notificationTitleTmpl: existing.notificationTitleTmpl ?? '{{rule.name}} is firing', notificationMessageTmpl: existing.notificationMessageTmpl ?? 'Alert {{alert.id}} fired at {{alert.firedAt}}', webhooks: (existing.webhooks ?? []).map((w) => ({ outboundConnectionId: (w.outboundConnectionId ?? '') as string, bodyOverride: w.bodyOverride ?? '', headerOverrides: Object.entries((w.headerOverrides ?? {}) as Record) .map(([key, value]) => ({ key, value })), })), targets: (existing.targets ?? []).map((t) => ({ kind: (t.kind ?? 'USER') as TargetKind, targetId: t.targetId ?? '', })), }; } export function toRequest(f: FormState): AlertRuleRequest { const scope: Record = {}; if (f.scopeKind === 'app' || f.scopeKind === 'route' || f.scopeKind === 'agent') scope.appSlug = f.appSlug || undefined; if (f.scopeKind === 'route') scope.routeId = f.routeId || undefined; if (f.scopeKind === 'agent') scope.agentId = f.agentId || undefined; const condition = { ...(f.condition as Record), kind: f.conditionKind, scope } as unknown as AlertCondition; return { name: f.name, description: f.description || undefined, severity: f.severity, enabled: f.enabled, conditionKind: f.conditionKind, condition, evaluationIntervalSeconds: f.evaluationIntervalSeconds, forDurationSeconds: f.forDurationSeconds, reNotifyMinutes: f.reNotifyMinutes, notificationTitleTmpl: f.notificationTitleTmpl, notificationMessageTmpl: f.notificationMessageTmpl, webhooks: f.webhooks.map((w) => ({ outboundConnectionId: w.outboundConnectionId, bodyOverride: w.bodyOverride || undefined, headerOverrides: Object.fromEntries(w.headerOverrides.filter((h) => h.key.trim()).map((h) => [h.key.trim(), h.value])), })), targets: f.targets.map((t) => ({ kind: t.kind, targetId: t.targetId })), } as AlertRuleRequest; } export function validateStep(step: WizardStep, f: FormState): string[] { const errs: string[] = []; if (step === 'scope') { if (!f.name.trim()) errs.push('Name is required.'); if (f.scopeKind !== 'env' && !f.appSlug.trim()) errs.push('App is required for app/route/agent scope.'); if (f.scopeKind === 'route' && !f.routeId.trim()) errs.push('Route id is required for route scope.'); if (f.scopeKind === 'agent' && !f.agentId.trim()) errs.push('Agent id is required for agent scope.'); } if (step === 'condition') { if (!f.conditionKind) errs.push('Condition kind is required.'); if (f.conditionKind === 'EXCHANGE_MATCH') { const c = f.condition as Record; if (!c.fireMode) { errs.push('Fire mode is required.'); } else if (c.fireMode === 'PER_EXCHANGE') { if (c.perExchangeLingerSeconds == null) { errs.push('Linger seconds is required for PER_EXCHANGE.'); } } else if (c.fireMode === 'COUNT_IN_WINDOW') { if (c.threshold == null) errs.push('Threshold is required for COUNT_IN_WINDOW.'); if (c.windowSeconds == null) errs.push('Window (seconds) is required for COUNT_IN_WINDOW.'); } } } if (step === 'trigger') { if (f.evaluationIntervalSeconds < 5) errs.push('Evaluation interval must be \u2265 5 s.'); if (f.forDurationSeconds < 0) errs.push('For-duration must be \u2265 0.'); if (f.reNotifyMinutes < 0) errs.push('Re-notify cadence must be \u2265 0.'); } if (step === 'notify') { if (!f.notificationTitleTmpl.trim()) errs.push('Notification title template is required.'); if (!f.notificationMessageTmpl.trim()) errs.push('Notification message template is required.'); } return errs; }