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:
co-authored by
Claude Opus 5
parent
c779266450
commit
0b8874da26
+165
-1
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user