/** * Alert engine — runs every rule against every port. Called by the * BullMQ recurring job 'alerts-evaluate' every 5 minutes; exposed as a * function so integration tests can drive it without a worker. */ import { logger } from '@/lib/logger'; import { db } from '@/lib/db'; import { ports } from '@/lib/db/schema/ports'; import { reconcileAlertsForPort } from './alerts.service'; import { RULE_REGISTRY, listRuleIds } from './alert-rules'; export interface EngineRunSummary { portsScanned: number; rulesEvaluated: number; errors: Array<{ portId: string; ruleId: string; message: string }>; } /** Evaluate every rule for every port, upsert + auto-resolve. */ export async function runAlertEngine(): Promise { const allPorts = await db.select({ id: ports.id, slug: ports.slug }).from(ports); return runAlertEngineForPorts(allPorts.map((p) => p.id)); } /** Same engine scoped to a specific list of port IDs (used by tests + the * per-port webhook trigger). */ export async function runAlertEngineForPorts(portIds: string[]): Promise { const ruleIds = listRuleIds(); const errors: EngineRunSummary['errors'] = []; for (const portId of portIds) { for (const ruleId of ruleIds) { try { const candidates = await RULE_REGISTRY[ruleId](portId); await reconcileAlertsForPort(portId, ruleId, candidates); } catch (err) { const message = err instanceof Error ? err.message : String(err); logger.warn({ portId, ruleId, err }, 'alert rule evaluator failed'); errors.push({ portId, ruleId, message }); } } } return { portsScanned: portIds.length, rulesEvaluated: portIds.length * ruleIds.length, errors, }; }