/** * `celilo alerts ack|silence|resolve` — the operator's side of the ack loop. * * Alerts are addressed by KEY, not id. The key is what `celilo alerts list` * prints and what arrives in a page, so it is the only identifier an operator * ever has in hand. */ import { getDb } from '../../db/client'; import { acknowledgeAlert, findLiveAlertByKey, resolveAlertManually, silenceAlert, } from '../../services/alerting/ack'; import { findPerson, listPeople } from '../../services/alerting/people'; import { parseIntervalMinutes } from '../../services/cadence'; import type { CommandResult } from '../types'; function resolveKey(key: string | undefined) { if (!key) return { error: 'An alert key is required. Run "celilo alerts list" to see them.' }; const db = getDb(); const alert = findLiveAlertByKey(db, key); if (!alert) { return { error: `No live alert with key "${key}".\n\nRun "celilo alerts list" to see current alerts.`, }; } return { db, alert }; } export async function handleAlertsAck( args: string[], flags: Record = {}, ): Promise { const found = resolveKey(args[0]); if (found.error !== undefined) return { success: false, error: found.error }; const { db, alert } = found; // Who is acknowledging matters: it stops escalation AND tells everyone else // who was paged that somebody has it. const who = typeof flags.as === 'string' ? flags.as : undefined; const people = listPeople(db); if (!who) { if (people.length !== 1) { return { success: false, error: `Who is acknowledging? Pass --as .\n\nKnown people: ${people.map((p) => p.name).join(', ') || '(none configured)'}`, }; } } const person = who ? findPerson(db, who) : people[0]; if (!person) return { success: false, error: `No person named "${who}".` }; const result = acknowledgeAlert(db, alert.id, person.id, new Date()); if (!result) return { success: false, error: 'Alert vanished while acknowledging.' }; const broadcast = result.broadcastTo.length ? ` — ${result.broadcastTo.length} other route(s) will be told` : ''; return { success: true, message: `${alert.key} acknowledged by ${person.name}${broadcast}` }; } export async function handleAlertsSilence( args: string[], flags: Record = {}, ): Promise { const found = resolveKey(args[0]); if (found.error !== undefined) return { success: false, error: found.error }; const { db, alert } = found; const forFlag = typeof flags.for === 'string' ? flags.for : '2h'; const minutes = parseIntervalMinutes(forFlag); if (minutes === null) { return { success: false, error: `Invalid --for "${forFlag}". Use a duration like 2h, 30m, 1d.`, }; } const until = new Date(Date.now() + minutes * 60_000); silenceAlert(db, alert.id, until); return { success: true, message: `${alert.key} silenced for ${forFlag} (until ${until.toISOString()})`, }; } export async function handleAlertsResolve(args: string[]): Promise { const found = resolveKey(args[0]); if (found.error !== undefined) return { success: false, error: found.error }; const { db, alert } = found; resolveAlertManually(db, alert.id, new Date()); return { success: true, // Resolve does not override the monitor, and saying so is the whole point // of the message — an operator who thinks it does will trust a cleared // alert that is about to come back. // // What it IS for: you have just fixed something and the check runs every // fifteen minutes. This stops the stale `firing` row escalating at you // while you wait for the next run to agree. message: `${alert.key} resolved. The monitor is still the authority: if the check still fails, this fires again on the next sweep.`, }; }