feat(benachrichtigung): E-Mail und Webhook bei Drift und Ablauf (#028)

Drift, ablaufende und abgelaufene Schluessel, unerreichbare Hosts und
geaenderte Host-Keys sollen auffallen, ohne dass jemand taeglich in die
Oberflaeche schaut. Versand per SMTP (Microsoft 365, STARTTLS) und per
Webhook mit HMAC-Signatur, eingestellt unter /einstellungen.

Die Anlaesse liest der Worker aus der Datenbank, statt sie im Abgleich
auszuloesen: der Abgleich-Ablauf bleibt unberuehrt, und ein verpasster
Lauf holt sich der naechste. Ein Versandprotokoll (Zustellung) mit
Anlass-Fingerabdruck verhindert Doppelmeldungen und haelt Fehlschlaege
fuer begrenzte Wiederholungen fest. Taeglich eine Zusammenfassung,
geaenderte Host-Keys im Fuenf-Minuten-Takt sofort.

Versand und Testnachricht laufen im Worker, damit der Webprozess keinen
Weg zu Mailserver oder Webhook-Ziel braucht. Passwort und Geheimnis
liegen nur verschluesselt in der Datenbank; die Seite zeigt "gesetzt".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:54:43 +02:00
co-authored by Claude Opus 5.5
parent 83bd9e0922
commit 8f5b31c0cb
26 changed files with 3243 additions and 12 deletions
@@ -0,0 +1,36 @@
-- #028: Benachrichtigungen. Versandprotokoll gegen Doppelmeldungen und eine Vorgangsart fuer
-- die Testnachricht (Versand laeuft im Worker).
-- CreateEnum
CREATE TYPE "MeldungsKanal" AS ENUM ('EMAIL', 'WEBHOOK');
-- CreateEnum
CREATE TYPE "ZustellErgebnis" AS ENUM ('GESENDET', 'FEHLGESCHLAGEN');
-- AlterEnum
ALTER TYPE "VorgangsArt" ADD VALUE 'BENACHRICHTIGUNG_TEST';
-- CreateTable
CREATE TABLE "Zustellung" (
"id" TEXT NOT NULL,
"anlass" TEXT NOT NULL,
"kanal" "MeldungsKanal" NOT NULL,
"ergebnis" "ZustellErgebnis" NOT NULL,
"fehler" TEXT,
"versuche" INTEGER NOT NULL DEFAULT 1,
"naechsterVersuchAm" TIMESTAMP(3),
"sendung" TEXT NOT NULL,
"betreff" TEXT NOT NULL,
"zeitpunkt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Zustellung_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "Zustellung_zeitpunkt_idx" ON "Zustellung"("zeitpunkt");
-- CreateIndex
CREATE INDEX "Zustellung_sendung_idx" ON "Zustellung"("sendung");
-- CreateIndex
CREATE UNIQUE INDEX "Zustellung_anlass_kanal_key" ON "Zustellung"("anlass", "kanal");