feat(meldungen): Benachrichtigungen per Mail und Webhook (#54)
Ein zugewiesener Lauf fiel bisher nur auf, wenn jemand die Uebersicht oeffnete; ein ueberfaelliger ueberhaupt nicht. Vier Anlaesse gehen jetzt hinaus — zugewiesen, ueberfaellig, abgeschlossen und Verzeichnisabgleich fehlgeschlagen —, je Person abschaltbar. Niemand bekommt Post ueber die eigene Tat: das ist der haeufigste Grund, Benachrichtigungen ganz abzuschalten, und deshalb faellt der Handelnde immer aus der Empfaengerliste. Eingereiht wird im Anfrage-Pfad, gesendet wird es nicht. Ein haengender Mailserver darf kein Abhaken blockieren, also schreibt der Dienst eine Zeile in `Zustellung` und ein eigener Zeitplan schickt sie. Betreff und Rumpf stehen fertig darin: eine Meldung sagt, was *damals* war. In keiner Nachricht steht ein Schrittinhalt, und damit auch kein erzeugtes Passwort. Darueber wacht src/lib/meldungen.ts — die Funktionen dort nehmen nur Kopfdaten, der Typ laesst nichts anderes zu. Geprueft wird es zusaetzlich ueber alle Rumpftexte, die wirklich verschickt wurden, so wie #012 es fuer die Druckansicht tat. Zwei Dinge haetten ohne Datenbank nicht gehalten. Die Einmaligkeit der Ueberfaelligkeitsmeldung liegt in einem partiellen eindeutigen Index: der Zeitplan sieht jede Minute nach, und ohne ihn bekaeme der Zustaendige fuer einen Lauf, der ueber das Wochenende steht, 2880 Mails — jede fuer sich korrekt, kein Test rot. Und die Sperre der Warteschlange liegt in derselben Tabelle wie die des Abgleichs, denn hinter einem Lastverteiler laufen zwei Instanzen. Eine stille Warteschlange ist schlimmer als keine: Verwaltung → Zustellungen zeigt mit Grund, was hinausging und was nicht. Geprueft gegen einen echten SMTP-Faenger (mailpit) statt gegen eine abgefangene Funktion — und `mailpit` gehoert damit ins Tor vor dem Commit, sonst ueberspringt die Suite 3 Tests und sieht gruen aus. Closes #54 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
4aa79c93ff
commit
2d3325ef20
@@ -0,0 +1,143 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- HANDGESCHRIEBEN. Prisma erzeugt hier zusaetzlich Loeschbefehle fuer die
|
||||
-- zusammengesetzten Fremdschluessel (Mandantengrenze), die Trigramm-Indizes
|
||||
-- (Archivsuche), die partiellen eindeutigen Indizes und alle Trigger: es kennt
|
||||
-- sie nicht, weil sie sich im Schema nicht ausdruecken lassen. Diese
|
||||
-- Loeschbefehle sind bewusst gestrichen.
|
||||
--
|
||||
-- tests/datenbankregeln.test.ts prueft, dass alle diese Objekte vorhanden sind –
|
||||
-- ein versehentlich stehengelassener Loeschbefehl faellt dort sofort auf.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Benachrichtigungen per Mail und Webhook (#034).
|
||||
|
||||
CREATE TYPE "Zustellweg" AS ENUM ('MAIL', 'WEBHOOK');
|
||||
CREATE TYPE "Zustellzustand" AS ENUM ('OFFEN', 'GESENDET', 'FEHLGESCHLAGEN', 'AUFGEGEBEN');
|
||||
CREATE TYPE "Meldeanlass" AS ENUM (
|
||||
'LAUF_ZUGEWIESEN',
|
||||
'LAUF_UEBERFAELLIG',
|
||||
'LAUF_ABGESCHLOSSEN',
|
||||
'ABGLEICH_FEHLGESCHLAGEN'
|
||||
);
|
||||
|
||||
-- Je Empfaenger abschaltbar.
|
||||
ALTER TABLE "Benutzer" ADD COLUMN "meldungenAktiv" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- SMTP und Webhook gehoeren zur einen Einstellungszeile.
|
||||
ALTER TABLE "Einstellung"
|
||||
ADD COLUMN "smtpHost" TEXT,
|
||||
ADD COLUMN "smtpPort" INTEGER,
|
||||
ADD COLUMN "smtpTls" BOOLEAN NOT NULL DEFAULT true,
|
||||
ADD COLUMN "smtpBenutzer" TEXT,
|
||||
ADD COLUMN "smtpPasswortVerschluesselt" TEXT,
|
||||
ADD COLUMN "smtpAbsender" TEXT,
|
||||
ADD COLUMN "webhookUrl" TEXT,
|
||||
ADD COLUMN "webhookGeheimnisVerschluesselt" TEXT;
|
||||
|
||||
CREATE TABLE "Zustellung" (
|
||||
"id" TEXT NOT NULL,
|
||||
"anlass" "Meldeanlass" NOT NULL,
|
||||
"weg" "Zustellweg" NOT NULL,
|
||||
"empfaenger" TEXT NOT NULL,
|
||||
"betreff" TEXT NOT NULL,
|
||||
"rumpf" TEXT NOT NULL,
|
||||
"zustand" "Zustellzustand" NOT NULL DEFAULT 'OFFEN',
|
||||
"versuche" INTEGER NOT NULL DEFAULT 0,
|
||||
"naechsterVersuchAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"letzterFehler" TEXT,
|
||||
"gesendetAm" TIMESTAMP(3),
|
||||
"erstelltAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"geaendertAm" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"benutzerId" TEXT,
|
||||
"laufId" TEXT,
|
||||
"verzeichnisquelleId" TEXT,
|
||||
CONSTRAINT "Zustellung_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- Der Index, auf dem der Zeitplan arbeitet: er fragt jede Minute nach offenen
|
||||
-- und faelligen Zeilen. Ohne ihn waere das ein vollstaendiger Durchlauf der
|
||||
-- Tabelle, die mit der Zeit nur waechst.
|
||||
CREATE INDEX "Zustellung_zustand_naechsterVersuchAm_idx"
|
||||
ON "Zustellung"("zustand", "naechsterVersuchAm");
|
||||
CREATE INDEX "Zustellung_erstelltAm_idx" ON "Zustellung"("erstelltAm");
|
||||
CREATE INDEX "Zustellung_laufId_idx" ON "Zustellung"("laufId");
|
||||
CREATE INDEX "Zustellung_benutzerId_idx" ON "Zustellung"("benutzerId");
|
||||
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_benutzerId_fkey" FOREIGN KEY ("benutzerId")
|
||||
REFERENCES "Benutzer"("id") ON DELETE SET NULL ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "Zustellung_laufId_fkey" FOREIGN KEY ("laufId")
|
||||
REFERENCES "Lauf"("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
ADD CONSTRAINT "Zustellung_verzeichnisquelleId_fkey" FOREIGN KEY ("verzeichnisquelleId")
|
||||
REFERENCES "Verzeichnisquelle"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Regeln, die auch dann gelten, wenn jemand an der Service-Schicht vorbei
|
||||
-- schreibt.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Ein Empfaenger ohne Inhalt ist eine Zeile, die nie ankommen kann.
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_empfaenger_nicht_leer"
|
||||
CHECK (length(btrim("empfaenger")) > 0);
|
||||
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_versuche_nicht_negativ" CHECK ("versuche" >= 0);
|
||||
|
||||
-- „Gesendet“ und „wann“ gehoeren zusammen, in beide Richtungen. Dieselbe Form
|
||||
-- wie bei `Lauf_abgeschlossen_mit_zeitpunkt`: ein Zustand ohne Zeitpunkt macht
|
||||
-- jede spaetere Auswertung zur Schaetzung.
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_gesendet_mit_zeitpunkt"
|
||||
CHECK (("zustand" = 'GESENDET') = ("gesendetAm" IS NOT NULL));
|
||||
|
||||
-- Eine Mail geht an eine Person, ein Webhook nicht. Ohne das koennte eine
|
||||
-- Webhook-Zeile an einem Benutzer haengen und in dessen Mailverlauf auftauchen.
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_webhook_ohne_benutzer"
|
||||
CHECK ("weg" <> 'WEBHOOK' OR "benutzerId" IS NULL);
|
||||
|
||||
-- Jeder Anlass haengt an dem, worum es geht: die drei Lauf-Anlaesse an einem
|
||||
-- Lauf, der Abgleich an einer Quelle. Eine Meldung ohne Bezug waere ein Satz
|
||||
-- ohne Gegenstand — und der Link in der Mail zeigte ins Leere.
|
||||
ALTER TABLE "Zustellung"
|
||||
ADD CONSTRAINT "Zustellung_bezug_passt_zum_anlass"
|
||||
CHECK (
|
||||
("anlass" = 'ABGLEICH_FEHLGESCHLAGEN' AND "verzeichnisquelleId" IS NOT NULL AND "laufId" IS NULL)
|
||||
OR ("anlass" <> 'ABGLEICH_FEHLGESCHLAGEN' AND "laufId" IS NOT NULL AND "verzeichnisquelleId" IS NULL)
|
||||
);
|
||||
|
||||
-- Verschluesselt, nicht im Klartext – dieselbe Pruefung wie beim
|
||||
-- LDAP-Bind-Passwort. `v<n>.` ist das Praefix aus `src/lib/geheimnisse.ts`.
|
||||
ALTER TABLE "Einstellung"
|
||||
ADD CONSTRAINT "Einstellung_smtp_passwort_verschluesselt"
|
||||
CHECK ("smtpPasswortVerschluesselt" ~ '^v[0-9]+\.');
|
||||
|
||||
ALTER TABLE "Einstellung"
|
||||
ADD CONSTRAINT "Einstellung_webhook_geheimnis_verschluesselt"
|
||||
CHECK ("webhookGeheimnisVerschluesselt" ~ '^v[0-9]+\.');
|
||||
|
||||
ALTER TABLE "Einstellung"
|
||||
ADD CONSTRAINT "Einstellung_smtp_port_bereich"
|
||||
CHECK ("smtpPort" IS NULL OR ("smtpPort" BETWEEN 1 AND 65535));
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Und die Regel, die der Zeitplan braucht.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
* **Ueberfaellig wird je Lauf und Empfaenger genau einmal gemeldet.**
|
||||
*
|
||||
* Der Zeitplan sieht jede Minute nach. Ohne diesen Index bekaeme der
|
||||
* Zustaendige fuer einen Lauf, der ueber das Wochenende ueberfaellig steht,
|
||||
* 2880 Mails — und zwar ohne dass irgendein Test rot waere, denn jede einzelne
|
||||
* davon ist korrekt. Der Dienst prueft dasselbe noch einmal, nur mit besserer
|
||||
* Meldung.
|
||||
*
|
||||
* Partiell, weil es nur fuer diesen einen Anlass gilt: „abgeschlossen“ darf nach
|
||||
* einem Zurueckholen und erneuten Abschliessen ein zweites Mal hinausgehen, und
|
||||
* „zugewiesen“ bei jeder neuen Zuweisung.
|
||||
*/
|
||||
CREATE UNIQUE INDEX "Zustellung_ueberfaellig_einmal"
|
||||
ON "Zustellung"("laufId", "empfaenger")
|
||||
WHERE "anlass" = 'LAUF_UEBERFAELLIG';
|
||||
Reference in New Issue
Block a user