# #034 M21 – Benachrichtigungen per Mail und Webhook **Status:** erledigt **Im Tracker:** [#54](https://git.wilde-server.de/florian.wilde/Laufzettel/issues/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 - [x] **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. - [x] **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. - [x] **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 - [x] Modell `Zustellung`: Ereignis, Empfänger, Weg (MAIL | WEBHOOK), Versuche, letzter Fehler, Zustand; mit Index auf das, was der Zeitplan abholt - [x] SMTP-Einstellungen unter Admin → Einstellungen: Host, Port, TLS, Benutzer, Passwort (verschlüsselt, nie in einer Antwort), Absender, „Testmail schicken“ - [x] Webhook: Ziel-URL, Geheimnis, **HMAC-SHA256 über den Rumpf** im Kopf `X-Laufzettel-Signatur`, dazu ein Zeitstempel gegen Wiedereinspielen - [x] Wiederholung mit wachsendem Abstand (1, 5, 30, 120 Minuten), dann aufgeben und sichtbar machen. Eine stille Warteschlange ist schlimmer als keine - [x] Überfälligkeit erkennt der Zeitplan, der schon läuft (`node-cron` im Prozess) — kein zweiter Dienst - [x] Je Benutzer abschaltbar; ein Benutzer ohne Mailadresse bekommt keine Mail und keinen Fehler - [x] Admin: die letzten Zustellungen mit Zustand und Grund - [x] **`{{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** - [x] `docs/betrieb/`: SMTP einrichten, Webhook prüfen, Signatur nachrechnen - [x] Anleitung Kapitel 07 ## Akzeptanzkriterien - [x] Test gegen einen echten SMTP-Fänger im Container (mailpit): zugewiesen, überfällig, abgeschlossen, Abgleich fehlgeschlagen — vier Mails, richtige Empfänger - [x] Test gegen einen lokalen Empfänger: die Signatur lässt sich mit dem Geheimnis nachrechnen, mit einem falschen nicht - [x] Test: eine fehlgeschlagene Zustellung wird wiederholt und ist danach sichtbar - [x] Test: zwei gleichzeitige Durchgänge schicken nichts zweimal - [x] Test: in **keiner** Nachricht steht ein erzeugtes Passwort — über alle Rumpftexte geprüft, wie in #012 - [x] 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.