Files
Laufzettel/docs/issues/034-benachrichtigungen.md
T
Florian WildeandClaude Opus 5 2d3325ef20
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
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>
2026-10-05 17:22:20 +02:00

7.2 KiB
Raw Blame History

#034 M21 – Benachrichtigungen per Mail und Webhook

Status: erledigt Im Tracker: #54 Meilenstein: v0.2.0 Labels: phase-2, backend, frontend, infra Geschätzt: 10 h Tatsächlich: 3 h 30 min

Ziel

Ein zugewiesener Lauf fällt heute nur auf, wenn jemand die Übersicht öffnet. Ein überfälliger überhaupt nicht. Vier Ereignisse sind es wert, gemeldet zu werden: zugewiesen, überfällig, abgeschlossen, Verzeichnisabgleich fehlgeschlagen.

Zu entscheiden

  • Nur diese vier Ereignisse, je Empfänger abschaltbar — Empfehlung. Ein allgemeiner Benachrichtigungsbaukasten ist eine Phase für sich; vier Ereignisse decken ab, was im Betrieb wirklich fehlt.
  • Mail über SMTP, keine Dienstanbindung. Eine selbst gehostete Anwendung soll an den Mailserver des Hauses können, und das ist SMTP. Zugangsdaten verschlüsselt wie das LDAP-Bind-Passwort.
  • Zustellung in einer Warteschlange in der Datenbank, nicht im Anfrage-Pfad. Ein hängender Mailserver darf kein Abhaken blockieren. Dieselbe Sperre wie beim Abgleich (AbgleichSperre) verhindert, dass zwei Instanzen dasselbe zweimal schicken.

Aufgaben

  • Modell Zustellung: Ereignis, Empfänger, Weg (MAIL | WEBHOOK), Versuche, letzter Fehler, Zustand; mit Index auf das, was der Zeitplan abholt
  • SMTP-Einstellungen unter Admin → Einstellungen: Host, Port, TLS, Benutzer, Passwort (verschlüsselt, nie in einer Antwort), Absender, „Testmail schicken“
  • Webhook: Ziel-URL, Geheimnis, HMAC-SHA256 über den Rumpf im Kopf X-Laufzettel-Signatur, dazu ein Zeitstempel gegen Wiedereinspielen
  • Wiederholung mit wachsendem Abstand (1, 5, 30, 120 Minuten), dann aufgeben und sichtbar machen. Eine stille Warteschlange ist schlimmer als keine
  • Überfälligkeit erkennt der Zeitplan, der schon läuft (node-cron im Prozess) — kein zweiter Dienst
  • Je Benutzer abschaltbar; ein Benutzer ohne Mailadresse bekommt keine Mail und keinen Fehler
  • Admin: die letzten Zustellungen mit Zustand und Grund
  • {{generiertes_passwort}} kommt in keine Nachricht. Eine Mail mit dem Lauf-Inhalt wäre genau die Lücke, die das Projekt an jeder anderen Stelle geschlossen hat. Die Nachricht enthält Titel, Status und einen Link — keine Schrittinhalte
  • docs/betrieb/: SMTP einrichten, Webhook prüfen, Signatur nachrechnen
  • Anleitung Kapitel 07

Akzeptanzkriterien

  • Test gegen einen echten SMTP-Fänger im Container (mailpit): zugewiesen, überfällig, abgeschlossen, Abgleich fehlgeschlagen — vier Mails, richtige Empfänger
  • Test gegen einen lokalen Empfänger: die Signatur lässt sich mit dem Geheimnis nachrechnen, mit einem falschen nicht
  • Test: eine fehlgeschlagene Zustellung wird wiederholt und ist danach sichtbar
  • Test: zwei gleichzeitige Durchgänge schicken nichts zweimal
  • Test: in keiner Nachricht steht ein erzeugtes Passwort — über alle Rumpftexte geprüft, wie in #012
  • Test: ein Benutzer mit abgeschalteter Benachrichtigung bekommt nichts

Beim Umsetzen dazugekommen

  • Die Sperre wurde aus dem Abgleich herausgezogen. mitAbgleichSperre lag in services/abgleich.ts; die Warteschlange braucht dieselbe Mechanik, und zwei Fassungen davon wären die schlechteste aller Lösungen. Sie heißt jetzt mitSperre in services/sperre.ts. Die Tabelle behält den Namen AbgleichSperre — ein Umbenennen wäre eine Migration, die nichts verbessert, und der Schlüssel sagt ohnehin, wer sperrt.
  • Niemand bekommt Post über die eigene Tat. Steht nicht im Issue, ist aber der häufigste Grund, Benachrichtigungen ganz abzuschalten. Der Handelnde fällt immer aus der Empfängerliste.
  • Die Mandantengrenze gilt auch für Empfänger. Eine Meldung trägt Lauftitel und Gerätenamen; ohne diese Prüfung wäre die Benachrichtigung der eine Weg nach draußen, den sichtbareKundenIds nicht abdeckt. Ein Benutzer, der den Kunden des Laufs nicht sehen darf, bekommt keine Mail — mit eigenem Test.
  • Die Einmaligkeit der Überfälligkeitsmeldung liegt in der Datenbank, als partieller eindeutiger Index. Der Zeitplan sieht jede Minute nach; ohne ihn bekäme der Zuständige für einen Lauf, der über das Wochenende steht, 2880 Mails — und jede einzelne davon wäre für sich korrekt, kein Test rot. Gegenprobe gefahren: ohne den Index werden zwei Tests rot.
  • Fünf Versuche, nicht vier. Die vier Wartezeiten aus dem Issue (1, 5, 30, 120 Minuten) liegen zwischen den Versuchen — der fünfte Fehlschlag gibt auf. Meine erste Fassung indizierte um eins verschoben und wartete nach dem ersten Fehlschlag fünf Minuten; der Test hat es sofort gezeigt.
  • Ein eigener Zeitplan, nicht derselbe wie der Abgleich. Der Mailversand darf nicht warten, weil gerade ein großes Verzeichnis gelesen wird. Zwei Takte sind billig.
  • mailpit gehört ins Tor vor dem Commit. Ohne den Container überspringt die Suite 3 Tests (gemessen: 36 statt 33) — genau die, die den echten SMTP-Dialog prüfen. CLAUDE.md, das README und scripts/pruefe-doku.mjs nennen ihn jetzt, und die Prüfung ist gegengeprobt: ohne ihn im Block wird pnpm pruefe:doku rot.
  • it.runIf(mailpitDa) wird beim Einlesen der Datei ausgewertet. Das Flag stand in beforeAll und kam zu spät: der erste Lauf meldete „3 skipped“, war grün und hatte den Mailversand nie geprüft. Jetzt wird auf Modulebene geprüft, wie bei datenbankVerfuegbar.
  • pnpm wollte eine Mindestalter-Sperre umgehen. pnpm add nodemailer trug von sich aus minimumReleaseAgeExclude: nodemailer@10.0.15 ein — die Version war vom selben Tag. Das ist kein Formfehler, sondern genau die Warnung, vor der die Regel schützt: ein frisches Release eines Mailpakets ungeprüft einzubauen. Gepinnt ist jetzt 10.0.10 vom 14. September, die Ausnahme ist weg, und die Konvention steht in CLAUDE.md.
  • Zwei Knöpfe „Speichern“ auf einer Seite. Beim Durchklicken aufgefallen: die Einstellungsseite hatte zwei gleich benannte. Der neue heißt jetzt „Benachrichtigungen speichern“ — für einen Menschen genauso verwirrend wie für einen Test.
  • Die Testmail geht direkt hinaus, nicht über die Warteschlange. Ein Knopf, der „eingereiht“ meldet und den Fehler eine Minute später woanders zeigt, prüft nichts.
  • Der Mail-Schalter gilt auch für das eigene Konto, anders als „aktiv“: sich selbst zu deaktivieren wäre ein Eigentor, seine eigene Post abzustellen der häufigste Wunsch überhaupt. Eine Profilseite, auf der das jeder selbst könnte, fehlt noch — sie kommt mit der Selbstauskunft (#035). Bis dahin stellt ein Administrator es um, und so steht es in der Anleitung.
  • Im gebauten Stand wirklich gefahren: SMTP eingetragen, Testmail bei mailpit angekommen, ein überfälliger Lauf angelegt — und der Zeitplan hat die Meldung von selbst verschickt, ohne Zutun. Dazu geprüft: das Passwort steht nicht im Quelltext der Seite, Speichern ohne Passwortfeld behält es, die Zustellungsliste zeigt keinen Rumpf, und 390 px ohne waagerechtes Scrollen auf allen drei Seiten.