Phase 7: Benachrichtigungen und Termine #8

Merged
florian.wilde merged 1 commits from feat/phase-7-benachrichtigungen into main 2026-09-04 07:19:14 +00:00
Owner

Der Jahresplan der Wehr und die Wege, auf denen die Mannschaft davon erfährt.

Version 0.8.0


Der Satz, der über allem steht

Niemand wird ungefragt benachrichtigt. Wer nichts einträgt, bekommt nichts.
Ein neu eingerichteter Zustellweg hat eine leere Ereignisliste, es gibt keinen
Weg, jemand anderem einen Kanal einzutragen — auch nicht für den Kommandanten —,
und der Seed legt bewusst keine an, obwohl das die Beispieldaten hübscher
machen würde.

Terminkalender

  • Dienst, Übung, Lehrgang, Arbeitsdienst, Veranstaltung, Sitzung.
  • Serientermine werden ausgeschrieben, nicht als Regel gespeichert: jeder
    Abend ist ein eigener Datensatz. Sonst wäre der ausgefallene Übungsabend im
    August ein Sonderfall im Code statt eine Zeile in der Datenbank. Vor dem
    Anlegen steht in der Maske, wie viele Termine daraus werden.
  • Gerechnet wird auf dem Kalender, nicht in Millisekunden: „monatlich" heißt
    derselbe Tag im nächsten Monat, und der 31. Januar landet im Februar auf dem
    28. — nicht im März.
  • Rückmeldung ist die Ausnahme. Beim Übungsabend meldet sich niemand an.
    Erst beim Lehrgang mit zwölf Plätzen.
  • Warteliste, die von selbst nachrückt. Sagt jemand ab, rückt der Erste
    nach und erfährt es.
  • Abgesagt statt gelöscht, mit Grund, wahlweise für alle folgenden Termine
    einer Serie. Benachrichtigt werden die Angemeldeten, nicht die ganze Wehr.
  • Anwesenheitsliste über alle aktiven Mitglieder — auf einem Blatt zum
    Abhaken muss jeder stehen. Zusage und Anwesenheit sind getrennt.

Vier Zustellwege

Kanal Läuft über Bekommt
E-Mail (SMTP) den eigenen Mailserver den ausführlichen Text
ntfy (selbst gehostet) den eigenen Server den ausführlichen Text
Telegram einen fremden Server nur die Kurzfassung
Web-Push den Push-Dienst des Browserherstellers nur die Kurzfassung

Die rechte Spalte ist die Regel, nicht die Bequemlichkeit: was das Netz der
Wehr verlässt, bekommt „3 Prüfungen fällig", nicht welche. Das steht in
KANAL_EXTERN an einer Stelle, wird im Test für jeden externen Kanal
geprüft und in der Oberfläche zweimal gesagt.

lib/integrations/mail.ts verschickt jetzt tatsächlich (nodemailer mit
Verbindungspool) statt den Platzhalter aus Phase 0 zu loggen. Neu dazu:
ntfy.ts und webpush.ts, beide hinter demselben Vertrag und einzeln
abschaltbar.

Kalenderabonnement

ICS-Feed unter /api/kalender/<token>.ics. Die Adresse steht genau einmal
auf dem Bildschirm, gespeichert wird nur ihr SHA-256, jeder Zugang ist einzeln
widerrufbar, und wer austritt, verliert ihn. Ausgeliefert werden nur Termine.

Bewusst kein CalDAV: das ist ein Schreib-Lese-Protokoll mit
Rechteverwaltung und Konfliktauflösung; gebraucht wird Lesen, und dafür genügt
eine Adresse, die jeder Kalender abonnieren kann.

Digitaler Dienstausweis

Signierter QR, fünf Minuten gültig, Prüfseite ohne Anmeldung — wer an der
Absperrung prüft, hat keinen Zugang zu FLORIAN. Die Seite 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 er nicht als Anmeldemarke
aus Phase 4 durchgeht. Ein gültig signierter Code für jemanden, der ausgetreten
ist, wird abgewiesen.

Warum kein WhatsApp

Im README ausführlich begründet, kurz: die Nachrichten liefen über Server von
Meta und verlangten einen Vertrag zur Auftragsverarbeitung, abgerechnet wird je
Konversation, und die gewöhnliche App gleicht das Telefonbuch aller Mitglieder
ab. Für eine Wehr, deren Daten im Gerätehaus bleiben sollen, ist das der
falsche Weg. Wer sie trotzdem so erreichen will, hat den Weg schon — die
Gruppe, die es ohnehin gibt.

Sicherheit

  • RLS und Mandantenfilter für alle sechs neuen Tabellen.
  • REVOKE DELETE ON "benachrichtigung": wer wann worüber informiert wurde,
    lässt sich nicht über die Oberfläche wegräumen. Die Aufbewahrungsfrist läuft
    über den Systemzugang — und nimmt sich nur die zugestellten.
    Fehlgeschlagene bleiben, sie zeigen einen kaputten Kanal an.
  • CHECKs in der Datenbank: kein Ende vor dem Beginn, kein Anmeldeschluss nach
    dem Termin, kein Kontingent von null Plätzen, keine negative Versuchszahl.

Was zu testen ist

  1. /termine — zusagen, absagen, einen Termin anlegen. Bei einer Serie:
    steht die Zahl da, bevor man drückt?
  2. Maschinistenlehrgang — acht Plätze, elf Anmeldungen. Warteliste
    sichtbar und nummeriert?
  3. Waldfest — abgesagt mit Grund, durchgestrichen statt verschwunden.
  4. Anwesenheitsliste an einem Termin: stehen alle Mitglieder drin?
  5. /benachrichtigungen — einen Weg einrichten und den Probeknopf
    drücken
    . Ohne eingerichteten Kanal in der .env bleibt es beim Log; für
    ntfy: docker compose --profile push up -d, dann NTFY_ENABLED=true und
    NTFY_URL=http://localhost:8086.
  6. Kalenderadresse erzeugen und im Handy eintragen. Sie wird nur einmal
    angezeigt — das ist Absicht.
  7. /benachrichtigungen/vorlagen — einen Tippfehler wie {tippfehler}
    eintragen und speichern: wird er abgewiesen und die erlaubten aufgelistet?
    Bei Telegram/Web-Push: steht die Warnung über dem Feld?
  8. /ausweis — QR mit dem Handy scannen. Prüfseite ohne Anmeldung
    erreichbar? Nach fünf Minuten abgelehnt?

Nachweise

  • npx tsc --noEmit — ohne Ausgabe
  • npm run test — 422 Tests grün (56 neue)
  • npx playwright test — 184 grün, davon 17 neue; 2 flaky (bekannte Muster
    aus Phase 5/6, beim zweiten Anlauf grün)
  • npm run build — 50 Routen
  • npm run db:seed — 19 Termine mit Warteliste und abgehakter Anwesenheit
  • ntfy-Rauchtest gegen den laufenden Container: Nachricht mit Umlaut im Titel
    kommt korrekt an

Aufgefallen bei der Arbeit

Drei Dinge, die eigene Issues verdienen — ich lege keine an:

  1. Die E2E-Suite schreibt in den Demo-Mandanten. Vierte Phase in Folge, und
    es kostet inzwischen bei jeder Phase Zeit: nach diesem Lauf standen 94
    Atemschutz-Überwachungen und 49 Geräte im Seed-Bestand, und alle 72
    Screenshots
    unterscheiden sich dadurch. Die Tests bräuchten einen eigenen
    Mandanten oder ein Aufräumen danach.
  2. npm run lint hat keine ESLint-Konfiguration und tut deshalb nichts.
  3. React meldet verschachteltes DOM (<p> enthält <div>) auf der
    Einsatz-Detailseite aus Phase 3.

Dazu eine Kleinigkeit, die ich gleich mitbehoben habe: das Skript
npm run screenshots lief bei kaltem .next in eine Anmeldung, die ins Leere
ging — die Seite war noch nicht hydriert, das Formular schickte sich als
gewöhnliches GET ab. Wird jetzt erkannt und wiederholt.

🤖 Generated with Claude Code

Der Jahresplan der Wehr und die Wege, auf denen die Mannschaft davon erfährt. **Version 0.8.0** --- ## Der Satz, der über allem steht **Niemand wird ungefragt benachrichtigt.** Wer nichts einträgt, bekommt nichts. Ein neu eingerichteter Zustellweg hat eine leere Ereignisliste, es gibt keinen Weg, jemand anderem einen Kanal einzutragen — auch nicht für den Kommandanten —, und der Seed legt bewusst *keine* an, obwohl das die Beispieldaten hübscher machen würde. ## Terminkalender - Dienst, Übung, Lehrgang, Arbeitsdienst, Veranstaltung, Sitzung. - **Serientermine werden ausgeschrieben**, nicht als Regel gespeichert: jeder Abend ist ein eigener Datensatz. Sonst wäre der ausgefallene Übungsabend im August ein Sonderfall im Code statt eine Zeile in der Datenbank. Vor dem Anlegen steht in der Maske, wie viele Termine daraus werden. - Gerechnet wird auf dem Kalender, nicht in Millisekunden: „monatlich" heißt derselbe Tag im nächsten Monat, und der 31. Januar landet im Februar auf dem 28. — nicht im März. - **Rückmeldung ist die Ausnahme.** Beim Übungsabend meldet sich niemand an. Erst beim Lehrgang mit zwölf Plätzen. - **Warteliste, die von selbst nachrückt.** Sagt jemand ab, rückt der Erste nach und erfährt es. - **Abgesagt statt gelöscht**, mit Grund, wahlweise für alle folgenden Termine einer Serie. Benachrichtigt werden die Angemeldeten, nicht die ganze Wehr. - Anwesenheitsliste über **alle aktiven Mitglieder** — auf einem Blatt zum Abhaken muss jeder stehen. Zusage und Anwesenheit sind getrennt. ## Vier Zustellwege | Kanal | Läuft über | Bekommt | |---|---|---| | E-Mail (SMTP) | den eigenen Mailserver | den ausführlichen Text | | ntfy (selbst gehostet) | den eigenen Server | den ausführlichen Text | | Telegram | einen fremden Server | nur die Kurzfassung | | Web-Push | den Push-Dienst des Browserherstellers | nur die Kurzfassung | Die rechte Spalte ist die Regel, nicht die Bequemlichkeit: was das Netz der Wehr verlässt, bekommt „3 Prüfungen fällig", nicht welche. Das steht in `KANAL_EXTERN` an einer Stelle, wird im Test für *jeden* externen Kanal geprüft und in der Oberfläche zweimal gesagt. `lib/integrations/mail.ts` verschickt jetzt tatsächlich (nodemailer mit Verbindungspool) statt den Platzhalter aus Phase 0 zu loggen. Neu dazu: `ntfy.ts` und `webpush.ts`, beide hinter demselben Vertrag und einzeln abschaltbar. ## Kalenderabonnement ICS-Feed unter `/api/kalender/<token>.ics`. Die Adresse steht **genau einmal** auf dem Bildschirm, gespeichert wird nur ihr SHA-256, jeder Zugang ist einzeln widerrufbar, und wer austritt, verliert ihn. Ausgeliefert werden nur Termine. **Bewusst kein CalDAV**: das ist ein Schreib-Lese-Protokoll mit Rechteverwaltung und Konfliktauflösung; gebraucht wird Lesen, und dafür genügt eine Adresse, die jeder Kalender abonnieren kann. ## Digitaler Dienstausweis Signierter QR, fünf Minuten gültig, Prüfseite ohne Anmeldung — wer an der Absperrung prüft, hat keinen Zugang zu FLORIAN. Die Seite 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 er nicht als Anmeldemarke aus Phase 4 durchgeht. Ein gültig signierter Code für jemanden, der ausgetreten ist, wird abgewiesen. ## Warum kein WhatsApp Im README ausführlich begründet, kurz: die Nachrichten liefen über Server von Meta und verlangten einen Vertrag zur Auftragsverarbeitung, abgerechnet wird je Konversation, und die gewöhnliche App gleicht das Telefonbuch aller Mitglieder ab. Für eine Wehr, deren Daten im Gerätehaus bleiben sollen, ist das der falsche Weg. Wer sie trotzdem so erreichen will, hat den Weg schon — die Gruppe, die es ohnehin gibt. ## Sicherheit - RLS und Mandantenfilter für alle sechs neuen Tabellen. - `REVOKE DELETE ON "benachrichtigung"`: wer wann worüber informiert wurde, lässt sich nicht über die Oberfläche wegräumen. Die Aufbewahrungsfrist läuft über den Systemzugang — und nimmt sich **nur die zugestellten**. Fehlgeschlagene bleiben, sie zeigen einen kaputten Kanal an. - CHECKs in der Datenbank: kein Ende vor dem Beginn, kein Anmeldeschluss nach dem Termin, kein Kontingent von null Plätzen, keine negative Versuchszahl. ## Was zu testen ist 1. **`/termine`** — zusagen, absagen, einen Termin anlegen. Bei einer Serie: steht die Zahl da, bevor man drückt? 2. **Maschinistenlehrgang** — acht Plätze, elf Anmeldungen. Warteliste sichtbar und nummeriert? 3. **Waldfest** — abgesagt mit Grund, durchgestrichen statt verschwunden. 4. **Anwesenheitsliste** an einem Termin: stehen *alle* Mitglieder drin? 5. **`/benachrichtigungen`** — einen Weg einrichten und **den Probeknopf drücken**. Ohne eingerichteten Kanal in der `.env` bleibt es beim Log; für ntfy: `docker compose --profile push up -d`, dann `NTFY_ENABLED=true` und `NTFY_URL=http://localhost:8086`. 6. **Kalenderadresse erzeugen** und im Handy eintragen. Sie wird nur einmal angezeigt — das ist Absicht. 7. **`/benachrichtigungen/vorlagen`** — einen Tippfehler wie `{tippfehler}` eintragen und speichern: wird er abgewiesen und die erlaubten aufgelistet? Bei Telegram/Web-Push: steht die Warnung über dem Feld? 8. **`/ausweis`** — QR mit dem Handy scannen. Prüfseite ohne Anmeldung erreichbar? Nach fünf Minuten abgelehnt? ## Nachweise - `npx tsc --noEmit` — ohne Ausgabe - `npm run test` — **422 Tests grün** (56 neue) - `npx playwright test` — 184 grün, davon 17 neue; 2 flaky (bekannte Muster aus Phase 5/6, beim zweiten Anlauf grün) - `npm run build` — 50 Routen - `npm run db:seed` — 19 Termine mit Warteliste und abgehakter Anwesenheit - ntfy-Rauchtest gegen den laufenden Container: Nachricht mit Umlaut im Titel kommt korrekt an ## Aufgefallen bei der Arbeit Drei Dinge, die eigene Issues verdienen — ich lege keine an: 1. **Die E2E-Suite schreibt in den Demo-Mandanten.** Vierte Phase in Folge, und es kostet inzwischen bei jeder Phase Zeit: nach diesem Lauf standen 94 Atemschutz-Überwachungen und 49 Geräte im Seed-Bestand, und **alle 72 Screenshots** unterscheiden sich dadurch. Die Tests bräuchten einen eigenen Mandanten oder ein Aufräumen danach. 2. **`npm run lint` hat keine ESLint-Konfiguration** und tut deshalb nichts. 3. **React meldet verschachteltes DOM** (`<p>` enthält `<div>`) auf der Einsatz-Detailseite aus Phase 3. Dazu eine Kleinigkeit, die ich gleich mitbehoben habe: das Skript `npm run screenshots` lief bei kaltem `.next` in eine Anmeldung, die ins Leere ging — die Seite war noch nicht hydriert, das Formular schickte sich als gewöhnliches GET ab. Wird jetzt erkannt und wiederholt. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
florian.wilde added 1 commit 2026-09-04 06:41:03 +00:00
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>
florian.wilde merged commit 12243f177d into main 2026-09-04 07:19:14 +00:00
florian.wilde deleted branch feat/phase-7-benachrichtigungen 2026-09-04 07:19:14 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/FireAdministration#8