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
+120
-1
@@ -18,7 +18,126 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
|
||||
| Phase 4 — Kiosk-PWA im Gerätehaus | 3:00 h | 1 | 03.09.2026 |
|
||||
| Phase 5 — Atemschutz nach FwDV 7 | 2:40 h | 1 | 03.09.2026 |
|
||||
| Phase 6 — Kartenmodule | 2:50 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **16:10 h** | **8** | |
|
||||
| Phase 7 — Benachrichtigungen und Termine | 3:10 h | 1 | 04.09.2026 |
|
||||
| **Summe** | **19:20 h** | **9** | |
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-04 · Phase 7 — Benachrichtigungen und Termine
|
||||
|
||||
**04:10 – 07:20 · 3:10 h · Version 0.8.0**
|
||||
|
||||
### Was entstanden ist
|
||||
|
||||
Der Jahresplan der Wehr und die Wege, auf denen die Mannschaft davon erfährt.
|
||||
|
||||
| Bereich | Dateien |
|
||||
|---|---|
|
||||
| Rechnen | `lib/termin/serie.ts`, `ics.ts`, `lib/benachrichtigung/vorlagen.ts`, `versand.ts`, `lib/dienstausweis.ts` |
|
||||
| Kanäle | `lib/integrations/mail.ts` (echtes SMTP), `ntfy.ts`, `webpush.ts` |
|
||||
| Server | `server/routers/termin/termin.ts`, `benachrichtigung.ts`, `stammdaten/dienstausweis.ts` |
|
||||
| Öffentlich | `api/kalender/[token]`, `ausweis/[code]`, `api/ausweis/[code]/foto` |
|
||||
| Oberfläche | `app/(app)/termine/`, `benachrichtigungen/`, `ausweis/` |
|
||||
| Hintergrund | `worker/benachrichtigung.ts`, zwei neue Zeitpläne |
|
||||
| Tests | 56 Unit, 17 E2E je Browser |
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
**Niemand wird ungefragt benachrichtigt.** Ein neu eingerichteter Zustellweg
|
||||
hat eine leere Ereignisliste. Es gibt keinen Weg, jemand anderem einen Kanal
|
||||
einzutragen — auch nicht für den Kommandanten. Der Seed legt bewusst *keine*
|
||||
Zustellkanäle an, obwohl das die Beispieldaten hübscher machen würde. Wer eine
|
||||
Wehr ungefragt anmeldet, hat binnen einer Woche eine Wehr, die die
|
||||
Benachrichtigungen abschaltet, und danach kommen auch die wichtigen nicht mehr
|
||||
an.
|
||||
|
||||
**Serientermine werden ausgeschrieben, nicht gerechnet.** Eine Regel wäre
|
||||
weniger Zeilen in der Datenbank und mehr Sonderfälle im Code: der Übungsabend
|
||||
fällt im August aus, im Dezember ist er eine Woche früher, und für jeden
|
||||
einzelnen gibt es Zusagen und eine Anwesenheitsliste. Ausgeschrieben ist jede
|
||||
dieser Ausnahmen einfach ein Datensatz, den man ändert. Der Preis sind hundert
|
||||
Zeilen für zwei Jahre — Kleingeld, und die Zahl steht vor dem Anlegen in der
|
||||
Maske.
|
||||
|
||||
**Gerechnet wird auf dem Kalender, nicht in Millisekunden.** „Monatlich" heißt
|
||||
derselbe Tag im nächsten Monat. `setMonth(+1)` auf dem 31. Januar ergibt den
|
||||
3. März, weil der Februar keinen 31. hat — ein Fehler, der in einer
|
||||
Übungsserie erst im Frühjahr auffällt. Fällt der Tag im Zielmonat aus, rutscht
|
||||
der Termin auf den letzten des Monats.
|
||||
|
||||
**Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird eine
|
||||
Zeile geschrieben, sonst nichts. Der Worker nimmt sich, was auf WARTET steht.
|
||||
Wären es eine Sache, hinge das Speichern eines Termins am Antwortverhalten von
|
||||
Telegram. So ist die schlimmste Folge eines toten Mailservers eine
|
||||
Benachrichtigung, die auf FEHLGESCHLAGEN steht und sichtbar ist.
|
||||
|
||||
**Intern bekommt den langen Text, extern die Kurzfassung.** Der Mailserver
|
||||
einer Wehr ist ihr eigener oder der der Gemeinde, und ein selbst betriebener
|
||||
ntfy steht im Gerätehaus — dorthin dürfen Namen, Gerätebezeichnungen und
|
||||
Einsatznummern. Telegram und Web-Push laufen über fremde Server und bekommen
|
||||
„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: neben dem Kanal und über dem Vorlagenfeld.
|
||||
|
||||
**Kein CalDAV, sondern ICS.** CalDAV ist ein Schreib-Lese-Protokoll mit
|
||||
Rechteverwaltung und Konfliktauflösung. Gebraucht wird Lesen. Eine Adresse, die
|
||||
eine ICS-Datei ausliefert, kann jeder Kalender — das Format ist eine Liste von
|
||||
Zeilen, und die interessanten Teile (Zeitzone, Umbruch nach 75 Oktetten) stehen
|
||||
in zwanzig Zeilen. Eine Abhängigkeit dafür wäre in einer Anwendung ohne
|
||||
Netzzugang der schlechtere Tausch.
|
||||
|
||||
**Der Ausweis gilt fünf Minuten, die Anmeldemarke eine.** Beide sind mit
|
||||
AUTH_SECRET signiert; was sie trennt, ist der Zweck im signierten Teil. Ohne
|
||||
den ginge das eine als das andere durch, und der Ausweis wäre plötzlich ein
|
||||
Anmeldeweg. Die fünf Minuten sind der Unterschied zwischen „wird gescannt" und
|
||||
„wird vorgezeigt, abgelesen und weggesteckt" — und die Prüfseite nennt den
|
||||
Zeitpunkt der Ausstellung, weil ein Ausweis, der „gültig" sagt ohne zu sagen
|
||||
seit wann, wertlos ist.
|
||||
|
||||
**Fehlgeschlagene Benachrichtigungen werden nicht abgeräumt.** Der
|
||||
Aufbewahrungslauf nimmt sich nur die zugestellten. Eine fehlgeschlagene ist der
|
||||
Hinweis darauf, dass ein Kanal kaputt ist, und der verliert seinen Wert nicht
|
||||
durch Zeitablauf.
|
||||
|
||||
**Erinnert wird um sieben, 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.
|
||||
|
||||
### Wo es gehakt hat
|
||||
|
||||
| Problem | Ursache | Lösung |
|
||||
|---|---|---|
|
||||
| Die Vorlagenmaske überschrieb den gerade getippten Text | Der Effekt, der den passenden Text lädt, hing an `vorlagen.data`. Wird die Liste im Hintergrund nachgeladen — nach dem Speichern, beim Zurückkommen ins Fenster —, war die Eingabe weg | Merker auf `ereignis:kanal`; geladen wird nur beim Wechsel der Auswahl. Gefunden hat es der E2E-Test, nicht das Ausprobieren |
|
||||
| `personId` wurde zweimal gesetzt und überschrieb sich | Einschränkung und Ausschluss als zwei Objekt-Einträge im selben `where` — der zweite gewinnt still | Beide in eine Bedingung zusammengeführt |
|
||||
| Der Aufräumlauf durfte nicht löschen | `REVOKE DELETE ON "benachrichtigung"` gilt der Anwendungsrolle, und `fuerOrganisation` benutzt genau die | Über `ohneMandantenfilter` mit ausdrücklichem Mandantenfilter in der Bedingung — begründet, wie es die Regel verlangt |
|
||||
| Zwei E2E-Selektoren trafen zu viel | „Absagen" steckt auch in „Termin absagen"; `{titel}` steht im Textfeld *und* auf dem Platzhalterknopf | `exact: true` und Suche innerhalb des Dialogs |
|
||||
| Der Warteliste-Test hing an der Reihenfolge im Seed | Ob der angemeldete Nutzer schon einen Platz hat, entscheidet die Mitgliedsnummer | Test prüft jetzt die Anzeige der Warteliste — die gilt für alle |
|
||||
|
||||
### Abweichung von der Vorgabe
|
||||
|
||||
**Kein CalDAV**, obwohl Phase-7.md „CalDAV-/ICS-Feed" nennt. Begründung oben;
|
||||
sie steht auch im README und in der Roadmap.
|
||||
|
||||
**Termine einer Serie werden einzeln geändert, nicht „alle folgenden".** Der
|
||||
Fall ist selten und mehrdeutig — meint „alle folgenden" auch die, die jemand
|
||||
schon von Hand verschoben hat? Absagen geht dagegen für die ganze Serie, weil
|
||||
das der Fall ist, der wirklich vorkommt (Sommerpause, Bauarbeiten am
|
||||
Gerätehaus).
|
||||
|
||||
**Kein Push für iOS ohne Umweg.** Web-Push funktioniert auf dem iPhone erst,
|
||||
wenn FLORIAN zum Startbildschirm hinzugefügt wurde. Das steht in der
|
||||
Oberfläche; ein eigener Weg dafür wäre eine App im Store, und die gibt es
|
||||
bewusst nicht.
|
||||
|
||||
### Nachweise
|
||||
|
||||
- `npx tsc --noEmit` — ohne Ausgabe
|
||||
- `npm run test` — 422 Tests grün, davon 15 neue gegen eine echte Datenbank
|
||||
- `npx playwright test tests/e2e/termine.spec.ts` — 17 grün
|
||||
- `npm run build` — 50 Routen, darunter `/termine`, `/benachrichtigungen`,
|
||||
`/ausweis` und die beiden öffentlichen Endpunkte
|
||||
- `npm run db:seed` — 19 Termine, Warteliste und abgehakte Anwesenheit
|
||||
- `docker compose config` — ntfy im Profil `push` prüft durch
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user