M21 aus Phase 2. Vier Anlässe gehen hinaus: ein Lauf wird jemandem zugewiesen, ist überfällig, wird abgeschlossen, oder ein Verzeichnisabgleich schlägt fehl.
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.
Wie es gebaut ist
Eingereiht im Anfrage-Pfad, gesendet vom Zeitplan. Ein hängender Mailserver darf kein Abhaken blockieren. Der Dienst schreibt eine Zeile in Zustellung, ein eigener Zeitplan im selben Prozess schickt sie — kein zweiter Dienst. Das Einreihen passiert nach der Transaktion und mit abgefangenem Fehler.
Betreff und Rumpf stehen fertig in der Zeile. Eine Meldung sagt, was damals war; würde der Text erst beim Senden gebaut, stünde in einer Mail von heute der Stand von jetzt.
Mail über SMTP, Zugangsdaten verschlüsselt wie das LDAP-Bind-Passwort und nie in einer Antwort. Dazu optional ein Webhook mit HMAC-SHA256-Signatur; der Zeitstempel ist Teil der signierten Nachricht, nicht Beiwerk.
Die Sperre wurde aus dem Abgleich herausgezogen nach services/sperre.ts — die Warteschlange braucht dieselbe Mechanik, und zwei Fassungen davon wären die schlechteste aller Lösungen.
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.
Die Zusage, auf die es ankommt
In keiner Nachricht steht ein Schrittinhalt, und damit kein erzeugtes Passwort. Darüber wacht src/lib/meldungen.ts: die Funktionen dort nehmen nur Kopfdaten, der Typ lässt nichts anderes zu. Zusätzlich geprüft über alle Rumpftexte, die wirklich verschickt wurden — so wie #012 es für die Druckansicht tat. Die Testvorlage hat eigens einen Befehlsschritt mit {{generiertes_passwort}} und New-ADUser, damit etwas zu finden wäre.
Was ohne Datenbank nicht gehalten hätte
2880 Mails. Der Zeitplan sieht jede Minute nach, ob ein Lauf überfällig ist. Ohne den partiellen eindeutigen Index Zustellung_ueberfaellig_einmal bekäme der Zuständige für einen Lauf, der über das Wochenende steht, eine Mail pro Minute — und jede einzelne davon wäre für sich korrekt, kein Test rot. Gegenprobe gefahren: ohne den Index werden zwei Tests rot.
Und die Sperre der Warteschlange, ebenfalls in der Datenbank: hinter einem Lastverteiler laufen zwei Instanzen, und ein let laeuft = false im Modul wüsste nichts von der zweiten. Gegenprobe gefahren: ohne wirksame Sperre wird „nichts zweimal" rot.
pnpm wollte eine Sicherheitsregel umgehen
pnpm add nodemailer trug von sich aus minimumReleaseAgeExclude: nodemailer@10.0.15 in pnpm-workspace.yaml 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 nodemailer@10.0.10 vom 14. September, die Ausnahme ist weg, und die Konvention steht in CLAUDE.md. Aufgefallen beim Durchsehen des Diffs, nicht durch einen Test.
Drei Tests waren stillschweigend übersprungen
it.runIf(mailpitDa) wird beim Einlesen der Datei ausgewertet; mein Flag stand in beforeAll und kam zu spät. Der erste Lauf meldete „3 skipped", war grün — und hatte den echten SMTP-Dialog nie geprüft. Genau das Muster, das ldap in #37 schon einmal gekostet hat.
mailpit steht jetzt im Tor vor dem Commit (CLAUDE.md, README), und scripts/pruefe-doku.mjs erzwingt beide Container namentlich — gegengeprobt: fehlt einer im Block, wird pnpm pruefe:doku rot. Die Zahlen sind gemessen: ohne ldap fehlen 22 Tests, ohne mailpit 3.
1302 Vitest-Tests grün (vorher 1266), davon 36 neue — gegen einen echten SMTP-Fänger (mailpit), nicht gegen eine abgefangene Funktion, und gegen einen echten HTTP-Empfänger für den Webhook
Im gebauten Stand gefahren: SMTP eingetragen, Testmail bei mailpit angekommen, ein überfälliger Lauf angelegt — und der Zeitplan hat die Meldung von selbst verschickt. Dazu geprüft: das Passwort steht nicht im Quelltext der Seite, Speichern ohne Passwortfeld behält es, die Zustellungsliste zeigt keinen Rumpf, der Mail-Schalter wirkt, und 390 px ohne waagerechtes Scrollen auf allen drei Seiten
Alle sechs Akzeptanzkriterien aus docs/issues/034-benachrichtigungen.md sind belegt.
Hinweis zur Reihenfolge
Dieser Zweig sitzt auf feat/53-sse auf, das auf feat/52-anhaenge sitzt. Bitte #59, #60, #61 in dieser Reihenfolge zusammenführen.
M21 aus Phase 2. Vier Anlässe gehen hinaus: ein Lauf wird jemandem **zugewiesen**, ist **überfällig**, wird **abgeschlossen**, oder ein **Verzeichnisabgleich schlägt fehl**.
**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.
## Wie es gebaut ist
- **Eingereiht im Anfrage-Pfad, gesendet vom Zeitplan.** Ein hängender Mailserver darf kein Abhaken blockieren. Der Dienst schreibt eine Zeile in `Zustellung`, ein eigener Zeitplan im selben Prozess schickt sie — kein zweiter Dienst. Das Einreihen passiert nach der Transaktion und mit abgefangenem Fehler.
- **Betreff und Rumpf stehen fertig in der Zeile.** Eine Meldung sagt, was *damals* war; würde der Text erst beim Senden gebaut, stünde in einer Mail von heute der Stand von jetzt.
- **Mail über SMTP**, Zugangsdaten verschlüsselt wie das LDAP-Bind-Passwort und nie in einer Antwort. Dazu optional ein **Webhook** mit HMAC-SHA256-Signatur; der Zeitstempel ist Teil der signierten Nachricht, nicht Beiwerk.
- **Die Sperre wurde aus dem Abgleich herausgezogen** nach `services/sperre.ts` — die Warteschlange braucht dieselbe Mechanik, und zwei Fassungen davon wären die schlechteste aller Lösungen.
- **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.
## Die Zusage, auf die es ankommt
**In keiner Nachricht steht ein Schrittinhalt**, und damit kein erzeugtes Passwort. Darüber wacht `src/lib/meldungen.ts`: die Funktionen dort nehmen **nur** Kopfdaten, der Typ lässt nichts anderes zu. Zusätzlich geprüft über **alle** Rumpftexte, die wirklich verschickt wurden — so wie #012 es für die Druckansicht tat. Die Testvorlage hat eigens einen Befehlsschritt mit `{{generiertes_passwort}}` und `New-ADUser`, damit etwas zu finden wäre.
## Was ohne Datenbank nicht gehalten hätte
**2880 Mails.** Der Zeitplan sieht jede Minute nach, ob ein Lauf überfällig ist. Ohne den partiellen eindeutigen Index `Zustellung_ueberfaellig_einmal` bekäme der Zuständige für einen Lauf, der über das Wochenende steht, eine Mail pro Minute — und **jede einzelne davon wäre für sich korrekt**, kein Test rot. Gegenprobe gefahren: ohne den Index werden zwei Tests rot.
Und die **Sperre** der Warteschlange, ebenfalls in der Datenbank: hinter einem Lastverteiler laufen zwei Instanzen, und ein `let laeuft = false` im Modul wüsste nichts von der zweiten. Gegenprobe gefahren: ohne wirksame Sperre wird „nichts zweimal" rot.
## pnpm wollte eine Sicherheitsregel umgehen
`pnpm add nodemailer` trug von sich aus `minimumReleaseAgeExclude: nodemailer@10.0.15` in `pnpm-workspace.yaml` 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 `nodemailer@10.0.10` vom 14. September, die Ausnahme ist weg, und die Konvention steht in `CLAUDE.md`. Aufgefallen beim Durchsehen des Diffs, nicht durch einen Test.
## Drei Tests waren stillschweigend übersprungen
`it.runIf(mailpitDa)` wird beim **Einlesen** der Datei ausgewertet; mein Flag stand in `beforeAll` und kam zu spät. Der erste Lauf meldete „3 skipped", war grün — und hatte den echten SMTP-Dialog nie geprüft. Genau das Muster, das `ldap` in #37 schon einmal gekostet hat.
`mailpit` steht jetzt im Tor vor dem Commit (`CLAUDE.md`, README), und `scripts/pruefe-doku.mjs` erzwingt beide Container namentlich — gegengeprobt: fehlt einer im Block, wird `pnpm pruefe:doku` rot. Die Zahlen sind **gemessen**: ohne `ldap` fehlen 22 Tests, ohne `mailpit` 3.
## Geprüft
- `pnpm lint`, `typecheck`, `format:pruefen`, `pruefe:doku` grün
- **1302 Vitest-Tests** grün (vorher 1266), davon 36 neue — **gegen einen echten SMTP-Fänger** (mailpit), nicht gegen eine abgefangene Funktion, und gegen einen echten HTTP-Empfänger für den Webhook
- **Im gebauten Stand gefahren:** SMTP eingetragen, Testmail bei mailpit angekommen, ein überfälliger Lauf angelegt — und der Zeitplan hat die Meldung **von selbst** verschickt. Dazu geprüft: das Passwort steht nicht im Quelltext der Seite, Speichern ohne Passwortfeld behält es, die Zustellungsliste zeigt keinen Rumpf, der Mail-Schalter wirkt, und 390 px ohne waagerechtes Scrollen auf allen drei Seiten
Alle sechs Akzeptanzkriterien aus `docs/issues/034-benachrichtigungen.md` sind belegt.
## Hinweis zur Reihenfolge
Dieser Zweig sitzt auf `feat/53-sse` auf, das auf `feat/52-anhaenge` sitzt. **Bitte #59, #60, #61 in dieser Reihenfolge zusammenführen.**
Closes #54
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
M21 aus Phase 2. Vier Anlässe gehen hinaus: ein Lauf wird jemandem zugewiesen, ist überfällig, wird abgeschlossen, oder ein Verzeichnisabgleich schlägt fehl.
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.
Wie es gebaut ist
Zustellung, ein eigener Zeitplan im selben Prozess schickt sie — kein zweiter Dienst. Das Einreihen passiert nach der Transaktion und mit abgefangenem Fehler.services/sperre.ts— die Warteschlange braucht dieselbe Mechanik, und zwei Fassungen davon wären die schlechteste aller Lösungen.sichtbareKundenIdsnicht abdeckt.Die Zusage, auf die es ankommt
In keiner Nachricht steht ein Schrittinhalt, und damit kein erzeugtes Passwort. Darüber wacht
src/lib/meldungen.ts: die Funktionen dort nehmen nur Kopfdaten, der Typ lässt nichts anderes zu. Zusätzlich geprüft über alle Rumpftexte, die wirklich verschickt wurden — so wie #012 es für die Druckansicht tat. Die Testvorlage hat eigens einen Befehlsschritt mit{{generiertes_passwort}}undNew-ADUser, damit etwas zu finden wäre.Was ohne Datenbank nicht gehalten hätte
2880 Mails. Der Zeitplan sieht jede Minute nach, ob ein Lauf überfällig ist. Ohne den partiellen eindeutigen Index
Zustellung_ueberfaellig_einmalbekäme der Zuständige für einen Lauf, der über das Wochenende steht, eine Mail pro Minute — und jede einzelne davon wäre für sich korrekt, kein Test rot. Gegenprobe gefahren: ohne den Index werden zwei Tests rot.Und die Sperre der Warteschlange, ebenfalls in der Datenbank: hinter einem Lastverteiler laufen zwei Instanzen, und ein
let laeuft = falseim Modul wüsste nichts von der zweiten. Gegenprobe gefahren: ohne wirksame Sperre wird „nichts zweimal" rot.pnpm wollte eine Sicherheitsregel umgehen
pnpm add nodemailertrug von sich ausminimumReleaseAgeExclude: nodemailer@10.0.15inpnpm-workspace.yamlein — 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
nodemailer@10.0.10vom 14. September, die Ausnahme ist weg, und die Konvention steht inCLAUDE.md. Aufgefallen beim Durchsehen des Diffs, nicht durch einen Test.Drei Tests waren stillschweigend übersprungen
it.runIf(mailpitDa)wird beim Einlesen der Datei ausgewertet; mein Flag stand inbeforeAllund kam zu spät. Der erste Lauf meldete „3 skipped", war grün — und hatte den echten SMTP-Dialog nie geprüft. Genau das Muster, dasldapin #37 schon einmal gekostet hat.mailpitsteht jetzt im Tor vor dem Commit (CLAUDE.md, README), undscripts/pruefe-doku.mjserzwingt beide Container namentlich — gegengeprobt: fehlt einer im Block, wirdpnpm pruefe:dokurot. Die Zahlen sind gemessen: ohneldapfehlen 22 Tests, ohnemailpit3.Geprüft
pnpm lint,typecheck,format:pruefen,pruefe:dokugrünAlle sechs Akzeptanzkriterien aus
docs/issues/034-benachrichtigungen.mdsind belegt.Hinweis zur Reihenfolge
Dieser Zweig sitzt auf
feat/53-sseauf, das auffeat/52-anhaengesitzt. Bitte #59, #60, #61 in dieser Reihenfolge zusammenführen.Closes #54
🤖 Generated with Claude Code
2d3325ef20toa417426f43Pull request closed