Files
chlova/orchestrator/src/alerts/sender.ts
T
Kantin-Petit 0a2eb203ee feat: module alertes (sender fail-soft + builders digest/J-1) (v0.16.0)
AlertEvent sans secret ; HttpAlertSender (POST webhook n8n, best-effort)
+ NullAlertSender ; buildDailyDigest / selectExpiringSoon purs. 6 tests.
Rattrape l'entrée CHANGELOG 0.15.1 omise.

Palier de risque : reversible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 01:50:18 +02:00

48 lines
1.6 KiB
TypeScript

import type { Logger } from "pino";
import type { AlertEvent, AlertSender } from "./types.js";
/**
* Envoie les alertes à un webhook n8n (qui envoie le mail). Fail-soft : une
* panne d'alerte ne doit jamais casser la boucle agent ni le gatekeeper — on
* logge et on continue.
*/
export class HttpAlertSender implements AlertSender {
constructor(
private readonly webhookUrl: string,
private readonly logger: Logger,
private readonly timeoutMs = 10_000,
) {}
async send(event: AlertEvent): Promise<void> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
try {
const res = await fetch(this.webhookUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ source: "chlova", ts: Date.now(), ...event }),
signal: controller.signal,
});
if (!res.ok) {
this.logger.warn({ kind: event.kind, status: res.status }, "alerte: HTTP non-OK");
}
} catch (err) {
// Jamais propagé : l'alerte est best-effort.
this.logger.warn(
{ kind: event.kind, err: err instanceof Error ? err.message : String(err) },
"alerte: envoi échoué (best-effort)",
);
} finally {
clearTimeout(timer);
}
}
}
/** Sender inactif : aucun webhook configuré → on logge seulement (fail-safe). */
export class NullAlertSender implements AlertSender {
constructor(private readonly logger: Logger) {}
async send(event: AlertEvent): Promise<void> {
this.logger.info({ alert: event }, "alerte (non envoyée : pas de webhook)");
}
}