/** * Observed health per module, derived from live alerts and enabled monitors. * * Extracted from `module list` because it now has a second consumer. The web * console shows the same column, and celilo has already decided how this is * computed: `nextModuleState()` documents why observation must NOT drive a * module's lifecycle state, since letting a recurring check write it would * redefine VERIFIED from "someone verified this" to "it was up recently" and * churn it on every transient failure. * * So this stays a derivation and never a stored field, and both consumers read * it here rather than each growing a copy. */ import { eq } from 'drizzle-orm'; import type { DbClient } from '../../db/client'; import { modules as modulesTable, monitors } from '../../db/schema'; import { moduleHealthCell } from './format'; import { loadAllLiveAlerts, summariseByModule } from './store'; /** The structured form. `cell` is the display string the CLI column prints. */ export interface ObservedHealth { cell: string; monitored: boolean; firingCount: number; suppressed: boolean; } /** * Observed health per module: `ok`, `N firing`, `suppressed`, or * `not observed`. * * Only computed for DEPLOYED modules — a module still being imported has * nothing to observe, and reporting it as unwatched would be noise rather * than a finding. */ export function loadObservedHealth(db: DbClient): Map { return new Map([...loadObservedHealthDetail(db)].map(([id, h]) => [id, h.cell])); } /** * The same derivation, with the inputs kept rather than collapsed into a * string. The console needs the firing COUNT and the not-observed case as * separate facts: it renders them differently, and parsing them back out of a * terminal column would be a second, worse implementation. */ export function loadObservedHealthDetail(db: DbClient): Map { const monitored = new Set( db .select({ target: monitors.target }) .from(monitors) .where(eq(monitors.enabled, true)) .all() .map((m) => m.target), ); const byModule = summariseByModule(loadAllLiveAlerts(db)); const result = new Map(); for (const module of db.select().from(modulesTable).all()) { if (module.state !== 'INSTALLED' && module.state !== 'VERIFIED') continue; const summary = byModule.get(module.id) ?? []; const input = { monitored: monitored.has(module.id), firingCount: summary.filter((s) => !s.suppressed).length, suppressed: summary.length > 0 && summary.every((s) => s.suppressed), }; result.set(module.id, { ...input, cell: moduleHealthCell(input) }); } return result; }