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.
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
/termine — zusagen, absagen, einen Termin anlegen. Bei einer Serie:
steht die Zahl da, bevor man drückt?
Maschinistenlehrgang — acht Plätze, elf Anmeldungen. Warteliste
sichtbar und nummeriert?
Waldfest — abgesagt mit Grund, durchgestrichen statt verschwunden.
Anwesenheitsliste an einem Termin: stehen alle Mitglieder drin?
/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.
Kalenderadresse erzeugen und im Handy eintragen. Sie wird nur einmal
angezeigt — das ist Absicht.
/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?
/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:
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.
npm run lint hat keine ESLint-Konfiguration und tut deshalb nichts.
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.
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)
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>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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
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.
derselbe Tag im nächsten Monat, und der 31. Januar landet im Februar auf dem
28. — nicht im März.
Erst beim Lehrgang mit zwölf Plätzen.
nach und erfährt es.
einer Serie. Benachrichtigt werden die Angemeldeten, nicht die ganze Wehr.
Abhaken muss jeder stehen. Zusage und Anwesenheit sind getrennt.
Vier Zustellwege
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_EXTERNan einer Stelle, wird im Test für jeden externen Kanalgeprüft und in der Oberfläche zweimal gesagt.
lib/integrations/mail.tsverschickt jetzt tatsächlich (nodemailer mitVerbindungspool) statt den Platzhalter aus Phase 0 zu loggen. Neu dazu:
ntfy.tsundwebpush.ts, beide hinter demselben Vertrag und einzelnabschaltbar.
Kalenderabonnement
ICS-Feed unter
/api/kalender/<token>.ics. Die Adresse steht genau einmalauf 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
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.
dem Termin, kein Kontingent von null Plätzen, keine negative Versuchszahl.
Was zu testen ist
/termine— zusagen, absagen, einen Termin anlegen. Bei einer Serie:steht die Zahl da, bevor man drückt?
sichtbar und nummeriert?
/benachrichtigungen— einen Weg einrichten und den Probeknopfdrücken. Ohne eingerichteten Kanal in der
.envbleibt es beim Log; fürntfy:
docker compose --profile push up -d, dannNTFY_ENABLED=trueundNTFY_URL=http://localhost:8086.angezeigt — das ist Absicht.
/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?
/ausweis— QR mit dem Handy scannen. Prüfseite ohne Anmeldungerreichbar? Nach fünf Minuten abgelehnt?
Nachweise
npx tsc --noEmit— ohne Ausgabenpm run test— 422 Tests grün (56 neue)npx playwright test— 184 grün, davon 17 neue; 2 flaky (bekannte Musteraus Phase 5/6, beim zweiten Anlauf grün)
npm run build— 50 Routennpm run db:seed— 19 Termine mit Warteliste und abgehakter Anwesenheitkommt korrekt an
Aufgefallen bei der Arbeit
Drei Dinge, die eigene Issues verdienen — ich lege keine an:
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.
npm run linthat keine ESLint-Konfiguration und tut deshalb nichts.<p>enthält<div>) auf derEinsatz-Detailseite aus Phase 3.
Dazu eine Kleinigkeit, die ich gleich mitbehoben habe: das Skript
npm run screenshotslief bei kaltem.nextin eine Anmeldung, die ins Leereging — die Seite war noch nicht hydriert, das Formular schickte sich als
gewöhnliches GET ab. Wird jetzt erkannt und wiederholt.
🤖 Generated with Claude Code