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
@@ -30,7 +30,7 @@ Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
||||
| Phase 4 — Kiosk-PWA im Gerätehaus | fertig |
|
||||
| Phase 5 — Atemschutz (FwDV 7) | fertig |
|
||||
| Phase 6 — Kartenmodule | fertig |
|
||||
| Phase 7 — Benachrichtigungen und Termine | offen |
|
||||
| Phase 7 — Benachrichtigungen und Termine | fertig |
|
||||
| Phase 8 — Lokale KI | offen |
|
||||
| Phase 9 — Kassenverwaltung | offen |
|
||||
|
||||
@@ -785,6 +785,194 @@ Verteiler.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 — Benachrichtigungen und Termine
|
||||
|
||||
Der Satz, der über der ganzen Phase steht und der auch die Technik darunter
|
||||
bestimmt: **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.
|
||||
|
||||
### Der Terminkalender
|
||||
|
||||
Übungsabende, Lehrgänge, Arbeitsdienste, Sitzungen. Drei Entscheidungen prägen
|
||||
ihn, und alle drei kommen aus dem Alltag einer Wehr:
|
||||
|
||||
**Anmeldung ist die Ausnahme, nicht die Regel.** Beim normalen Übungsabend
|
||||
meldet sich niemand an, man kommt oder man kommt nicht. Erst beim Lehrgang mit
|
||||
zwölf Plätzen und beim Ausflug mit Busbestellung wird eine Rückmeldung
|
||||
verlangt.
|
||||
|
||||
**Abgesagt, nicht gelöscht.** Ein Termin, der ohne ein Wort verschwindet, führt
|
||||
dazu, dass am Dienstagabend jemand vor dem verschlossenen Gerätehaus steht.
|
||||
Eine Absage braucht einen Grund, bleibt im Kalender stehen und erreicht die,
|
||||
die zugesagt hatten — nicht die ganze Wehr.
|
||||
|
||||
**Die Warteliste rückt von selbst nach.** Wer bei belegten Plätzen zusagt,
|
||||
landet auf der Warteliste; sagt jemand ab, rückt der Erste nach und erfährt es.
|
||||
Sonst führt jemand die Liste von Hand, und das tut niemand zuverlässig.
|
||||
|
||||
Serientermine werden **ausgeschrieben**, nicht als Regel gespeichert: jeder
|
||||
Abend ist ein eigener Datensatz. Der Grund ist derselbe Alltag — 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. Mit einer Regel wäre jede
|
||||
dieser Ausnahmen ein Sonderfall. Vor dem Anlegen steht in der Maske, wie viele
|
||||
Termine daraus werden.
|
||||
|
||||
Die Anwesenheitsliste zeigt **alle aktiven Mitglieder**, nicht nur die
|
||||
Angemeldeten: auf einem Blatt zum Abhaken muss jeder stehen. Zusage und
|
||||
Anwesenheit sind getrennt — wer zugesagt hat und nicht kam, steht genau so in
|
||||
der Liste.
|
||||
|
||||
### Der Kalender fürs Handy
|
||||
|
||||
Eine Adresse, die man im Handy oder im Mailprogramm abonniert; die Termine
|
||||
stehen dann neben allem anderen, ohne dass jemand FLORIAN öffnen muss. Das ist
|
||||
der Kanal mit der größten Wirkung pro Zeile Code in dieser Phase.
|
||||
|
||||
**Bewusst kein CalDAV.** Das ist ein Schreib-Lese-Protokoll mit
|
||||
Rechteverwaltung und Konfliktauflösung; gebraucht wird hier nur Lesen, und dafür
|
||||
genügt eine Adresse, die eine ICS-Datei ausliefert. Jeder Kalender kann das.
|
||||
|
||||
Die Adresse ist der Schlüssel — Kalenderprogramme können sich nicht anmelden,
|
||||
sie rufen stumpf eine URL ab. Daraus folgt:
|
||||
|
||||
- Gespeichert wird nur der SHA-256 des Tokens; die Adresse steht **genau
|
||||
einmal** auf dem Bildschirm.
|
||||
- Jeder Zugang lässt sich einzeln widerrufen. Ein verlorenes Handy kostet einen
|
||||
Klick, nicht ein neues Passwort für alle.
|
||||
- Ausgeliefert werden **nur Termine** — keine Anmeldelisten, keine Personen
|
||||
außer der Leitung, keine Mängel, keine Einsätze.
|
||||
- Wer aus der Wehr austritt, verliert das Abonnement.
|
||||
|
||||
### Die vier Kanäle
|
||||
|
||||
| Kanal | Läuft über | Bekommt |
|
||||
|---|---|---|
|
||||
| **E-Mail** (SMTP) | den eigenen bzw. den Mailserver der Gemeinde | den ausführlichen Text |
|
||||
| **ntfy** (selbst gehostet) | den eigenen Server, Profil `push` | den ausführlichen Text |
|
||||
| **Telegram** | einen fremden Server | nur die Kurzfassung |
|
||||
| **Web-Push** | den Push-Dienst des Browserherstellers | nur die Kurzfassung |
|
||||
|
||||
Die Spalte ganz rechts ist keine Bequemlichkeit, sondern die Regel: was das Netz
|
||||
der Wehr verlässt, bekommt einen Hinweis ohne Namen, Gerätebezeichnungen und
|
||||
Einsatzdetails — „3 Prüfungen fällig", nicht welche. In der Oberfläche steht das
|
||||
neben jedem externen Kanal, und die Vorlagenmaske sagt es noch einmal, bevor
|
||||
jemand einen Namen in eine Telegram-Vorlage tippt.
|
||||
|
||||
**ntfy** ist der interessanteste Weg für eine Wehr: ein Container, eine App im
|
||||
Store, kein Konto, keine Telefonnummer, kein Telefonbuchabgleich. Man abonniert
|
||||
ein Thema — einen frei gewählten Namen — und bekommt Meldungen darauf. Läuft
|
||||
der Server im Gerätehaus, verlässt keine Nachricht das Haus. Der Preis: ein
|
||||
Thema ist nur so geheim wie sein Name, deshalb schlägt FLORIAN Namen mit
|
||||
Zufallsanteil vor.
|
||||
|
||||
```bash
|
||||
docker compose --profile push up -d # ntfy startet auf Port 8086
|
||||
npm run vapid # Schlüsselpaar für Web-Push
|
||||
```
|
||||
|
||||
### Warum kein WhatsApp
|
||||
|
||||
Es gibt keine `WHATSAPP_`-Variablen, und das ist eine Entscheidung, keine
|
||||
Lücke. Drei Gründe, in dieser Reihenfolge:
|
||||
|
||||
**Datenschutz.** Der einzige zulässige Weg für eine Organisation ist die
|
||||
WhatsApp Business Platform. Nachrichten laufen über Server von Meta, und die
|
||||
Wehr müsste einen Vertrag zur Auftragsverarbeitung schließen — für eine
|
||||
Freiwillige Feuerwehr, deren Daten ausdrücklich im Gerätehaus bleiben sollen,
|
||||
ist das der falsche Weg. Die gewöhnliche App scheidet ohnehin aus: sie gleicht
|
||||
das Telefonbuch ab und gibt damit die Kontakte aller Mitglieder weiter.
|
||||
|
||||
**Kosten.** Abgerechnet wird je Konversation. Eine Wehr mit fünfzig Mitgliedern
|
||||
und zwei Terminerinnerungen im Monat zahlt dauerhaft für etwas, das über den
|
||||
eigenen Mailserver nichts kostet.
|
||||
|
||||
**Aufwand.** Business-Konto, verifizierte Telefonnummer, genehmigte
|
||||
Nachrichtenvorlagen, ein Meta-Entwicklerkonto. Das ist mehr Verwaltung als der
|
||||
gesamte Rest dieser Phase.
|
||||
|
||||
Wer seine Wehr trotzdem über WhatsApp erreichen will, hat den Weg schon: die
|
||||
Gruppe, die es ohnehin gibt. Ein Link zum Termin hineinkopiert kostet nichts
|
||||
und verrät niemandem etwas.
|
||||
|
||||
### Der digitale Dienstausweis
|
||||
|
||||
Ein QR-Code auf dem Handy, den man an der Absperrung, beim Lehrgang oder bei
|
||||
einer Kontrolle vorzeigt. Wer ihn scannt, landet auf einer Prüfseite und sieht
|
||||
Name, Wehr, Funktion, Foto und die gültigen Qualifikationen — **ohne sich
|
||||
anzumelden**, denn wer prüft, hat keinen Zugang zu FLORIAN.
|
||||
|
||||
- **Signiert, nicht gespeichert.** HMAC über Person, Wehr und Zeitstempel.
|
||||
Keine Tabelle, kein Aufräumjob.
|
||||
- **Fünf Minuten gültig.** Er wird vorgezeigt, abgelesen und weggesteckt; ein
|
||||
abfotografierter Code ist danach wertlos. Die Prüfseite nennt deshalb den
|
||||
Zeitpunkt der Ausstellung — ein Ausweis, der „gültig" sagt, ohne zu sagen
|
||||
seit wann, ist wertlos.
|
||||
- **Er öffnet nichts.** Die Prüfseite zeigt an, sie lässt nicht hinein. Wer den
|
||||
Code hat, bekommt keinen Zugang zu einem einzigen Datensatz.
|
||||
- Was **nicht** darauf steht: Anschrift, Geburtsdatum, Telefonnummer,
|
||||
Mitgliedsnummer. Ein Ausweis weist aus; er ist keine Personalakte.
|
||||
- Ein gültig signierter Code für jemanden, der ausgetreten ist, wird abgewiesen.
|
||||
|
||||
### Was von selbst benachrichtigt
|
||||
|
||||
Sechs Anlässe, an die vorhandenen Abläufe angehängt:
|
||||
|
||||
| Anlass | Wann | An wen |
|
||||
|---|---|---|
|
||||
| Prüfung fällig | im Erinnerungslauf | wer es abonniert hat |
|
||||
| Mangel gemeldet | sofort | wer es abonniert hat |
|
||||
| Qualifikation läuft ab | 90, 30 und 7 Tage vorher | die Person selbst |
|
||||
| Neuer Termin | beim Anlegen | wer es abonniert hat |
|
||||
| Terminerinnerung | 7 Tage und 1 Tag vorher | Angemeldete, sonst Abonnenten |
|
||||
| Einsatzbericht freigegeben | bei der Freigabe | wer es abonniert hat |
|
||||
|
||||
Die Sammelmail an den zuständigen Gerätewart bleibt, wie sie war — sie geht an
|
||||
den, der zuständig ist, unabhängig davon, ob er irgendwo ein Kreuzchen gesetzt
|
||||
hat. Ein Prüftermin ist eine Pflicht, kein Abo. Wer sie ohnehin bekommt, wird
|
||||
von der Benachrichtigung ausgenommen und hat sie damit nicht doppelt.
|
||||
|
||||
Erinnert wird um sieben Uhr morgens, nicht nachts: eine Push-Meldung um drei Uhr
|
||||
weckt Leute, die am nächsten Tag arbeiten müssen. Zugestellt wird dagegen jede
|
||||
Minute — eine Absage am Dienstagabend muss ankommen, solange sie noch etwas
|
||||
nützt.
|
||||
|
||||
**Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird nur eine
|
||||
Zeile geschrieben; der Worker schickt sie weg und vermerkt, wie es ausging.
|
||||
Sonst hinge das Speichern eines Termins am Antwortverhalten von Telegram. Eine
|
||||
fehlgeschlagene Zustellung bleibt sichtbar stehen und wird nicht nach Frist
|
||||
abgeräumt — sie ist der Hinweis darauf, dass ein Kanal kaputt ist, und der
|
||||
verliert seinen Wert nicht durch Zeitablauf.
|
||||
|
||||
Der Probeknopf steht direkt an jedem Zustellweg und läuft durch dieselbe
|
||||
Maschinerie wie alles andere. Ohne ihn merkt jemand erst in drei Wochen, dass
|
||||
sein ntfy-Thema einen Tippfehler hat — nämlich dann, wenn die Nachricht wichtig
|
||||
gewesen wäre.
|
||||
|
||||
### Zusätzliche Umgebungsvariablen
|
||||
|
||||
| Variable | Vorgabe | Bedeutung |
|
||||
|---|---|---|
|
||||
| `NTFY_ENABLED` | `false` | Push über den eigenen ntfy |
|
||||
| `NTFY_URL` | — | Adresse des eigenen ntfy, z. B. `http://ntfy:80` |
|
||||
| `NTFY_TOKEN` | — | nur bei eingeschaltetem Zugriffsschutz |
|
||||
| `NTFY_PORT` | `8086` | Port des ntfy-Containers (Profil `push`) |
|
||||
| `WEBPUSH_ENABLED` | `false` | Push in den Browser |
|
||||
| `WEBPUSH_PUBLIC_KEY` | — | aus `npm run vapid`; **ins Backup** |
|
||||
| `WEBPUSH_PRIVATE_KEY` | — | dito |
|
||||
| `WEBPUSH_KONTAKT` | `APP_URL` | Kontaktweg für den Push-Dienst |
|
||||
| `BENACHRICHTIGUNG_LAUF` | `0 7 * * *` | wann erinnert wird |
|
||||
| `TERMIN_ERINNERUNG_TAGE` | `7,1` | Vorlauf der Terminerinnerungen |
|
||||
| `BENACHRICHTIGUNG_VERSUCHE` | `3` | Zustellversuche, bevor es stehen bleibt |
|
||||
| `BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE` | `90` | Frist für **zugestellte** Nachrichten |
|
||||
|
||||
Ein neuer öffentlicher VAPID-Schlüssel macht alle bestehenden Push-Abos
|
||||
wertlos — jeder müsste Push von Hand neu erlauben, und niemand merkt es, die
|
||||
Meldungen bleiben einfach aus. Beide Schlüssel gehören deshalb ins Backup.
|
||||
|
||||
---
|
||||
|
||||
## Befehle
|
||||
|
||||
```bash
|
||||
@@ -798,6 +986,7 @@ npm run db:migrate # ausstehende Migrationen anwenden
|
||||
npm run db:seed # Beispielwehr einspielen
|
||||
npm run db:studio # Prisma Studio
|
||||
npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen
|
||||
npm run vapid # VAPID-Schlüsselpaar für Web-Push erzeugen
|
||||
```
|
||||
|
||||
## Dienste und Ports (Entwicklung)
|
||||
@@ -836,9 +1025,12 @@ tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren.
|
||||
Schriften verwendet, die auf dem Gerät vorhanden sind.
|
||||
- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`,
|
||||
`AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`).
|
||||
- Externe Dienste (Mail, Telegram, LLM) sitzen hinter Schnittstellen in
|
||||
`src/lib/integrations/` und sind einzeln abschaltbar. Ohne Freischaltung
|
||||
wird nur ins Log geschrieben.
|
||||
- Externe Dienste (Mail, Telegram, ntfy, Web-Push, LLM) sitzen hinter
|
||||
Schnittstellen in `src/lib/integrations/` und sind einzeln abschaltbar. Ohne
|
||||
Freischaltung wird nur ins Log geschrieben.
|
||||
- Über Kanäle, die das Netz der Wehr verlassen (Telegram, Web-Push), gehen nur
|
||||
Kurzfassungen ohne Namen, Gerätebezeichnungen und Einsatzdetails.
|
||||
- Kein WhatsApp — die Begründung steht oben unter „Warum kein WhatsApp".
|
||||
|
||||
## Sicherheitshinweis
|
||||
|
||||
|
||||
Reference in New Issue
Block a user