Phase 7: Benachrichtigungen und Termine

Terminkalender mit Serienterminen, Selbstanmeldung, Warteliste und
Anwesenheitsliste. Vier Zustellwege hinter Schnittstellen: echtes SMTP,
ntfy (selbst gehostet), Telegram und Web-Push. Kalenderabonnement als
ICS-Feed, digitaler Dienstausweis mit signiertem QR und Pruefseite.

Niemand wird ungefragt benachrichtigt: ein neuer Zustellweg hat eine leere
Ereignisliste, und der Seed legt bewusst keine an. Was das Netz der Wehr
verlaesst, bekommt nur die Kurzfassung ohne Namen und Details.

Kein CalDAV (Lesen genuegt, ICS kann jeder Kalender) und kein WhatsApp
(Verarbeitung ueber Meta, Kosten je Konversation, Telefonbuchabgleich) --
beides im README begruendet.

56 neue Unit-Tests, 17 neue E2E-Tests, Version 0.8.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-04 08:39:38 +02:00
co-authored by Claude Opus 5
parent c779266450
commit 0b8874da26
134 changed files with 9340 additions and 123 deletions
+165 -1
View File
@@ -8,7 +8,171 @@ Die Regeln dazu stehen in [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md).
## [Unveröffentlicht]
Nächster Schritt ist Phase 7 (Benachrichtigungen und Termine).
Nächster Schritt ist Phase 8 (Lokale KI).
---
## [0.8.0] — 2026-09-04
Phase 7: Benachrichtigungen und Termine. Ab hier weiß die Mannschaft, wann der
nächste Dienst ist — ohne dass jemand eine Rundmail tippt.
**Niemand wird ungefragt benachrichtigt.** Wer nichts einträgt, bekommt nichts.
Es gibt keine Voreinstellung, die stillschweigend alle anmeldet: ein neu
eingerichteter Zustellweg hat eine leere Ereignisliste, und erst wer ankreuzt,
wofür er ihn will, bekommt etwas.
### Hinzugefügt
**Datenmodell**
- `Termin` mit Art, Ort, Beginn und Ende, Leitung, Platzbegrenzung,
Anmeldeschluss und Serienkennung. Abgesagt wird mit Grund und Zeitpunkt —
**gelöscht wird nicht**, wer zugesagt hat, soll den Grund sehen.
- `Terminanmeldung` mit Zu-, Absage oder Warteliste, dem Grund einer Absage und
der Anwesenheit als eigenem Feld. Zusage und Anwesenheit sind bewusst
getrennt: wer zugesagt hat und nicht kam, steht genau so in der Liste.
- `Zustellkanal` — ein Datensatz je Person und Weg, mit der Adresse dieses Wegs
und der Liste der Ereignisse, für die er benutzt wird. Leer bedeutet: für
nichts.
- `Benachrichtigung` — steht **vor** dem Versand in der Tabelle, nicht danach.
Damit ist nachvollziehbar, wer wann worüber informiert wurde und wer nicht,
weil sein Kanal nicht erreichbar war.
- `Benachrichtigungsvorlage` je Ereignis und Kanal, `Kalenderzugang` mit
SHA-256 des Tokens.
**Terminkalender**
- Liste mit Filter nach Art und „nur meine", Rückmeldung direkt aus der Zeile.
- Serientermine werden **ausgeschrieben**, nicht als Regel gespeichert: jeder
Abend ist ein eigener Datensatz. Sonst wäre jeder ausgefallene Übungsabend
ein Sonderfall. Vor dem Anlegen steht in der Maske, wie viele Termine daraus
werden.
- Kalenderrechnung statt Millisekunden: „monatlich" heißt derselbe Tag im
nächsten Monat, und der 31. Januar landet im Februar auf dem 28. — nicht im
März, wie es passiert, wenn man stur Monate addiert.
- **Warteliste, die von selbst nachrückt.** Wer bei belegten Plätzen zusagt,
landet auf ihr; sagt jemand ab, rückt der Erste nach und erfährt es.
- Absage mit Grund, wahlweise für alle folgenden Termine einer Serie.
Vergangene bleiben unberührt. Benachrichtigt werden die Angemeldeten, nicht
die ganze Wehr.
- Anwesenheitsliste über **alle aktiven Mitglieder**, nicht nur die
Angemeldeten — auf einem Blatt zum Abhaken muss jeder stehen. Zum Abhaken am
Tablet oder zum Ausdrucken.
**Kalenderabonnement**
- ICS-Feed nach RFC 5545 unter `/api/kalender/<token>.ics`. Die Termine der
Wehr stehen damit im Handy neben allem anderen.
- **Bewusst kein CalDAV**: das ist ein Schreib-Lese-Protokoll mit
Rechteverwaltung und Konfliktauflösung. Gebraucht wird nur Lesen.
- Die Adresse steht genau einmal auf dem Bildschirm; gespeichert wird nur ihr
Hash. Jeder Zugang lässt sich einzeln widerrufen, und wer austritt, verliert
ihn.
- Ausgeliefert werden nur Termine — keine Anmeldelisten, keine Personen außer
der Leitung.
**Vier Zustellwege, hinter Schnittstellen**
- **SMTP** löst den Platzhalter aus Phase 0 ab: nodemailer mit
Verbindungspool, damit ein Sammellauf mit vierzig Nachrichten Sekunden statt
Minuten braucht.
- **ntfy** (neu, selbst gehostet, Compose-Profil `push`): Push aufs Handy ohne
Konto, ohne Telefonnummer, ohne Telefonbuchabgleich. Läuft der Server im
Gerätehaus, verlässt keine Nachricht das Haus.
- **Web-Push** (neu, VAPID): Push in den Browser ohne App. Schlüsselpaar über
`npm run vapid`.
- **Telegram** aus Phase 0 wird jetzt tatsächlich benutzt.
- Was das Netz der Wehr verlässt (Telegram, Web-Push), bekommt **nur die
Kurzfassung** — „3 Prüfungen fällig", nicht welche. In der Oberfläche steht
das neben jedem externen Kanal.
**Textvorlagen**
- Je Ereignis und Kanal, mit eingebautem Standardtext für alles. Eine Wehr, die
nichts einstellt, bekommt trotzdem brauchbare Nachrichten.
- Platzhalter zum Anklicken, Vorschau mit erfundenen Beispielwerten. Ein
Platzhalter, den es für das Ereignis nicht gibt, wird beim Speichern
abgewiesen — mit der Liste der erlaubten daneben.
- Ein unbekannter Platzhalter bleibt im Text **sichtbar stehen**, statt still
zu verschwinden. Eine Lücke, die niemand bemerkt, ist schlimmer als ein
seltsam aussehender Text, den jemand korrigiert.
**Digitaler Dienstausweis**
- QR-Code auf dem Handy, Prüfseite ohne Anmeldung unter `/ausweis/<code>`.
Sie zeigt Name, Wehr, Funktion, Foto und die gültigen Qualifikationen.
- Signiert statt gespeichert (HMAC über Person, Wehr und Zeitstempel), **fünf
Minuten gültig**. Die Prüfseite nennt den Zeitpunkt der Ausstellung — ein
Ausweis, der „gültig" sagt, ohne zu sagen seit wann, ist wertlos.
- Der Zweck steht im signierten Teil, damit ein Ausweis nicht als Anmeldemarke
aus Phase 4 durchgeht und umgekehrt.
- Ein gültig signierter Code für jemanden, der ausgetreten ist, wird
abgewiesen. Das Passbild ist über denselben Code erreichbar und damit genauso
lange gültig.
**Was von selbst benachrichtigt**
- Prüfung fällig, Mangel gemeldet, Qualifikation läuft ab (90/30/7 Tage
vorher), neuer Termin, Terminerinnerung (7 und 1 Tag vorher), Einsatzbericht
freigegeben.
- **Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird nur
eine Zeile geschrieben; der Worker schickt sie weg. Sonst hinge das Speichern
eines Termins am Antwortverhalten von Telegram.
- Erinnert wird um sieben Uhr morgens, zugestellt jede Minute: eine
Push-Meldung um drei Uhr weckt Leute, die am nächsten Tag arbeiten müssen —
eine Absage am Dienstagabend muss dagegen ankommen, solange sie noch etwas
nützt.
- Die Sammelmail an den zuständigen Gerätewart bleibt, wie sie war. Wer sie
ohnehin bekommt, wird von der zusätzlichen Benachrichtigung ausgenommen und
hat sie damit nicht doppelt.
### Geändert
- `lib/integrations/mail.ts` verschickt tatsächlich, statt eine Meldung ins Log
zu schreiben. Ohne `MAIL_ENABLED` bleibt es beim Log.
- Neue Rechte: `termin.lesen` und `benachrichtigung.eigene` als Grundrechte
(jeder sieht den Kalender und richtet seine eigenen Wege ein — das ist keine
Sache des Kommandanten), `termin.bearbeiten`, `termin.anwesenheit` und
`benachrichtigung.vorlagen` bei Kommandant, Schriftführer und Ausbilder.
- `DIENSTGRAD_TEXT` liegt jetzt in `lib/stammdaten/texte.ts` statt zweimal fast
gleich in zwei Seiten.
- Der Service Worker nimmt Push-Meldungen an und öffnet beim Antippen ein schon
offenes FLORIAN, statt einen zwölften Tab aufzumachen.
- Der Aufbewahrungslauf räumt zugestellte Benachrichtigungen nach Frist ab.
**Fehlgeschlagene bleiben** — sie zeigen einen kaputten Kanal an, und das
verliert seinen Wert nicht durch Zeitablauf.
### Behoben
- Die Vorlagenmaske überschrieb den gerade getippten Text, sobald die Liste im
Hintergrund nachgeladen wurde (nach dem Speichern, beim Zurückkommen ins
Fenster). Gefunden hat es der E2E-Test, nicht das Ausprobieren.
### Sicherheit
- Row Level Security und Mandantenfilter für alle sechs neuen Tabellen.
- `REVOKE DELETE ON "benachrichtigung"` für die Anwendungsrolle: wer wann
worüber informiert wurde, lässt sich nicht über die Oberfläche wegräumen.
Die Aufbewahrungsfrist läuft über den Systemzugang.
- Prüfbedingungen in der Datenbank: kein Ende vor dem Beginn, kein
Anmeldeschluss nach dem Termin, kein Kontingent von null Plätzen, keine
negative Versuchszahl.
- Kalendertoken nur als SHA-256; ein Push-Abo, das der Dienst als tot meldet,
wird stillgelegt statt gelöscht, damit die Person es in ihren Einstellungen
sieht und neu erlauben kann.
- **Kein WhatsApp** — Begründung im README unter „Warum kein WhatsApp":
Verarbeitung über Meta-Server, Kosten je Konversation, Telefonbuchabgleich.
### Tests
- 56 neue Unit-Tests: Serienrechnung (Monatsenden, Schaltjahr, Kappung),
ICS-Erzeugung (Maskierung, Zeilenumbruch nach 75 Oktetten, CRLF, Absage),
Textbausteine (Kurzfassung für externe Kanäle, unbekannte Platzhalter),
Dienstausweis (Frist, Fälschung, Verwechslung mit der Anmeldemarke) und die
Datenbankregeln der neuen Tabellen.
- 17 neue E2E-Tests für Kalender, Rückmeldung, Warteliste, Absage,
Anwesenheitsliste, Kalenderabonnement, Vorlagen und Prüfseite.
---