SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
4.0 KB · 95 lines typescript
Raw Blame History
1import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm';2import type { Database } from './client.js';3import { systemAlerts } from './schema/ext-ops.js';45export type AlertSeverity = 'info' | 'warn' | 'critical';6export type AlertStatus = 'open' | 'acknowledged' | 'resolved';78export interface AlertInput {9  kind: string;10  severity: AlertSeverity;11  connectorId?: string | null;12  message: string;13  detail?: Record<string, unknown>;14}1516export interface SystemAlert {17  id: number;18  kind: string;19  severity: string;20  connectorId: string | null;21  message: string;22  detail: Record<string, unknown>;23  firstSeenAt: Date;24  lastSeenAt: Date;25  count: number;26  status: string;27  resolvedAt: Date | null;28}2930const ACTIVE: AlertStatus[] = ['open', 'acknowledged'];3132/**33 * Raise (or bump) an internal alert (CLAUDE.md §170). Deduplicated on kind + connector + message34 * among open/acknowledged rows: a repeat increments `count`, refreshes `last_seen_at`, `severity`35 * and `detail`. Returns the alert id. Inside failure handlers use `raiseAlertSafe`: an alert36 * failure must never mask the original error.37 */38export async function raiseAlert(db: Database, input: AlertInput): Promise<number> {39  const connectorId = input.connectorId ?? null;40  const detail = input.detail ?? {};41  const where = and(eq(systemAlerts.kind, input.kind), eq(systemAlerts.message, input.message), connectorId === null ? isNull(systemAlerts.connectorId) : eq(systemAlerts.connectorId, connectorId), inArray(systemAlerts.status, ACTIVE));42  const [existing] = await db.select({ id: systemAlerts.id }).from(systemAlerts).where(where).orderBy(desc(systemAlerts.lastSeenAt)).limit(1);43  if (existing) {44    await db45      .update(systemAlerts)46      .set({ count: sql`${systemAlerts.count} + 1`, lastSeenAt: new Date(), severity: input.severity, detail })47      .where(eq(systemAlerts.id, existing.id));48    return existing.id;49  }50  const [row] = await db.insert(systemAlerts).values({ kind: input.kind, severity: input.severity, connectorId, message: input.message, detail }).returning({ id: systemAlerts.id });51  return row!.id;52}5354/** Like `raiseAlert` but swallows (and returns) the error — for use inside failure handlers. */55export async function raiseAlertSafe(db: Database, input: AlertInput): Promise<number | Error> {56  try {57    return await raiseAlert(db, input);58  } catch (e) {59    return e as Error;60  }61}6263/**64 * Resolve open/acknowledged alerts of one kind (or several) for a connector — or for all65 * connectors when `connectorId` is undefined; `null` targets alerts without a connector.66 * Returns the number of rows resolved.67 */68export async function resolveAlerts(db: Database, kind: string | string[], connectorId?: string | null): Promise<number> {69  const kinds = Array.isArray(kind) ? kind : [kind];70  if (!kinds.length) return 0;71  const conds = [inArray(systemAlerts.kind, kinds), inArray(systemAlerts.status, ACTIVE)];72  if (connectorId === null) conds.push(isNull(systemAlerts.connectorId));73  else if (connectorId !== undefined) conds.push(eq(systemAlerts.connectorId, connectorId));74  const rows = await db75    .update(systemAlerts)76    .set({ status: 'resolved', resolvedAt: new Date() })77    .where(and(...conds))78    .returning({ id: systemAlerts.id });79  return rows.length;80}8182/** Open (and optionally acknowledged) alerts, most severe and most recent first. */83export async function listAlerts(db: Database, opts: { status?: AlertStatus | 'active'; connectorId?: string; limit?: number } = {}): Promise<SystemAlert[]> {84  const status = opts.status ?? 'active';85  const conds = status === 'active' ? [inArray(systemAlerts.status, ACTIVE)] : [eq(systemAlerts.status, status)];86  if (opts.connectorId) conds.push(eq(systemAlerts.connectorId, opts.connectorId));87  const rows = await db88    .select()89    .from(systemAlerts)90    .where(and(...conds))91    .orderBy(sql`CASE ${systemAlerts.severity} WHEN 'critical' THEN 0 WHEN 'warn' THEN 1 ELSE 2 END`, desc(systemAlerts.lastSeenAt))92    .limit(opts.limit ?? 200);93  return rows as SystemAlert[];94}95