# Benachrichtigungen einrichten Laufzettel meldet vier Dinge, und nur diese vier: | Anlass | Wer bekommt es | |---|---| | Ein Lauf wird jemandem **zugewiesen** | der Zugewiesene | | Ein Lauf ist **überfällig** | der Zugewiesene, **einmal** je Lauf | | Ein Lauf wird **abgeschlossen** | der Zugewiesene und wer ihn angelegt hat | | Ein **Verzeichnisabgleich schlägt fehl** | alle aktiven Administratoren | **Niemand bekommt Post über das, was er selbst gerade getan hat.** Das ist der häufigste Grund, Benachrichtigungen ganz abzuschalten, und deshalb fällt der Handelnde immer aus der Empfängerliste. > **In keiner Nachricht steht ein Schrittinhalt.** Titel, Status, Gerät, Person, Frist und ein Link — > mehr nicht. Kein Wert, keine Notiz, kein Befehl, und damit auch kein `{{generiertes_passwort}}`. > Das Passwort entsteht bei jeder Anzeige neu und wird nirgends gespeichert; eine Mail dagegen liegt > für immer in einem Postfach, oft auf einem Server, der nicht im Haus steht. ## Was gebraucht wird - **`APP_URL`**, damit der Link in der Mail irgendwohin führt. Fehlt sie, gehen die Meldungen trotzdem hinaus — die Einstellungsseite sagt es dann deutlich. - **Ein SMTP-Server** für Mail, **oder** eine Webhook-Adresse, **oder** beides. Ohne eines von beiden wird nichts eingereiht; es entstehen also keine Zeilen, die nie ankommen. ## Mail über SMTP Unter **Admin → Einstellungen → Benachrichtigungen**: | Feld | Was hineingehört | |---|---| | Host | `mail.firma.de` — der Mailserver des Hauses | | Port | `587` für StartTLS, `465` für TLS von Anfang an, `25` nur im eigenen Netz | | Benutzer | leer lassen, wenn der Server dem Rechner ohne Anmeldung traut | | Passwort | wird verschlüsselt gespeichert und **nie wieder angezeigt** | | Absender | `laufzettel@firma.de` — muss der Server akzeptieren | | TLS verlangen | an, außer der Server kann es wirklich nicht | Dann **Speichern**, dann **Testmail schicken**. Die Testmail geht **direkt** hinaus und nicht über die Warteschlange: ein Fehler erscheint sofort und im Klartext, statt eine Minute später in einer Liste. **Das Passwortfeld ist leer, obwohl ein Passwort gesetzt ist** — das ist Absicht. Leer heißt „unverändert lassen“. Sonst löschte jedes Speichern der übrigen Felder das Passwort mit. ### Wenn die Testmail nicht ankommt | Meldung | Ursache | |---|---| | `greeting never received` | `secure` passt nicht zum Port. Auf 587 gehört StartTLS, nicht TLS von Anfang an — Laufzettel entscheidet das selbst am Port, aber ein Server, der auf 465 StartTLS erwartet, ist falsch konfiguriert. | | `535 Authentication failed` | Benutzer oder Passwort stimmen nicht. Manche Server wollen die vollständige Mailadresse als Benutzernamen. | | `550 relay not permitted` | Der Server nimmt von dieser Maschine keine Mail für fremde Domains an. Entweder den Rechner freischalten oder sich anmelden. | | `self signed certificate` | Der Mailserver hat ein eigenes Zertifikat. Entweder ein richtiges einrichten oder TLS für diesen Server abschalten — im eigenen Netz ist das zweite vertretbar. | | `ECONNREFUSED` | Port falsch, oder eine Firewall dazwischen. | ## Webhook Dieselben vier Anlässe als `POST` mit JSON-Rumpf: ```json { "anlass": "LAUF_ABGESCHLOSSEN", "betreff": "Laufzettel abgeschlossen: „PC Neuinstallation – PC-001“", "link": "https://laufzettel.firma.de/laeufe/cmu…", "laufId": "cmu…", "verzeichnisquelleId": null, "zeitpunkt": "2026-10-05T08:00:00.000Z" } ``` `anlass` steht vorn, damit sich ohne Auspacken filtern lässt. Auch hier: **keine Schrittinhalte.** Als erfolgreich gilt nur eine Antwort mit `2xx`. Alles andere ist ein Fehlschlag und wird wiederholt. ### Die Signatur nachrechnen Ist ein Geheimnis gesetzt, trägt jede Anfrage den Kopf: ``` X-Laufzettel-Signatur: t=1762334400,v1=9f86d08188... ``` Signiert wird `.` mit HMAC-SHA256. **Der Zeitstempel ist Teil der signierten Nachricht**, nicht Beiwerk — stünde er nur daneben, könnte ihn jeder ändern, ohne die Signatur zu brechen, und eine abgefangene Nachricht ließe sich in einem Jahr noch einspielen. So prüft die Gegenseite (Node, aber in jeder Sprache dasselbe): ```js import { createHmac, timingSafeEqual } from "node:crypto"; function pruefen(geheimnis, rumpf, kopf) { const teile = Object.fromEntries(kopf.split(",").map((s) => s.split("="))); const alter = Math.abs(Math.floor(Date.now() / 1000) - Number(teile.t)); if (alter > 300) return false; // älter als fünf Minuten: ablehnen const erwartet = createHmac("sha256", geheimnis).update(`${teile.t}.${rumpf}`).digest("hex"); // Zeitkonstant vergleichen, nicht mit === return timingSafeEqual(Buffer.from(erwartet), Buffer.from(teile.v1)); } ``` Drei Dinge, die dabei schiefgehen: 1. **Den Rumpf vorher geparst und wieder serialisiert.** Dann stimmt die Signatur nicht mehr — `JSON.stringify` setzt Leerzeichen und Reihenfolge anders. Signiert wird der **rohe** Rumpf, also vor jeder Verarbeitung lesen. 2. **Mit `===` verglichen.** Das bricht beim ersten Unterschied ab; aus der Dauer ließe sich die richtige Signatur Zeichen für Zeichen erraten. `timingSafeEqual` oder das Äquivalent benutzen. 3. **Nur „zu alt“ geprüft.** Eine Nachricht mit Zeitstempel im Jahr 2040 wäre dann für immer gültig. Der Abstand gehört in **beide** Richtungen geprüft. Ohne Geheimnis fehlt die Kopfzeile ganz, statt dass eine leere Signatur Sicherheit vortäuscht. ## Die Warteschlange **Eingereiht wird sofort, gesendet nicht.** Ein hängender Mailserver darf kein Abhaken blockieren: der Dienst schreibt eine Zeile, und ein Zeitplan im selben Prozess schickt sie — jede Minute, kein zweiter Dienst, kein Systemd-Timer. Ein Fehlschlag wird wiederholt, mit wachsendem Abstand: nach **1, 5, 30 und 120 Minuten**. Beim fünften Fehlschlag gibt Laufzettel auf und lässt die Zeile als **aufgegeben** stehen. > **Eine stille Warteschlange ist schlimmer als keine.** Unter **Admin → Zustellungen** steht, was > hinausgegangen ist und was nicht — mit Grund. Sieh dort hin, wenn jemand sagt, er bekomme keine > Mails: fast immer ist das SMTP-Passwort abgelaufen, und ohne diese Seite merkt es wochenlang > niemand, weil jeder Lauf weiter funktioniert. 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. ### Mehrere Instanzen Läuft Laufzettel hinter einem Lastverteiler mehrfach, nimmt genau eine Instanz je Durchgang die Warteschlange — über eine Sperre in der Datenbank, dieselbe Mechanik wie beim Verzeichnisabgleich. Ohne sie bekäme jeder Empfänger jede Mail doppelt. Dafür ist nichts einzurichten. ## Je Person abschalten Unter **Admin → Benutzer** schaltet der **Mail**-Knopf die Benachrichtigungen einer Person ab. Das gilt auch für das eigene Konto — sich selbst zu deaktivieren wäre ein Eigentor, seine eigene Post abzustellen ist der häufigste Wunsch überhaupt. Wer abgeschaltet hat, bekommt keine Zeile und keinen Fehler: es wird gar nichts eingereiht. Ein deaktiviertes Konto ebenso. ## Was nicht gemeldet wird - **Ein Kommentar oder ein angehängtes Foto.** Das sieht die Live-Aktualisierung in der Abarbeitungsansicht sofort; eine Mail dafür wäre Lärm. - **Ein einzelner abgehakter Schritt.** Dasselbe. - **Ein archivierter Lauf.** Archivieren ist eine Aufräumarbeit, keine Nachricht. Mehr Anlässe wären ein Benachrichtigungsbaukasten, und der ist eine Phase für sich. Diese vier decken ab, was im Betrieb wirklich fehlt. ## Prüfen, dass es läuft ```bash # Steht etwas in der Warteschlange, das nicht hinausgeht? docker compose exec -T postgres psql -U laufzettel laufzettel -tAc \ "SELECT zustand, count(*) FROM \"Zustellung\" GROUP BY zustand" ``` Nativ dasselbe über `sudo -u postgres psql -d laufzettel -tAc "…"`. Steht dort etwas unter `AUFGEGEBEN`, schau unter **Admin → Zustellungen** nach dem Grund. Alles unter `OFFEN` oder `FEHLGESCHLAGEN` regelt der Zeitplan von selbst — es sei denn, es steht dort seit Stunden, dann läuft der Prozess nicht oder `ZEITPLAN_AKTIV` ist auf `false`.