Files
FireAdministration/docs/AUFWANDSSCHAETZUNG.md
T
Florian WildeandClaude Opus 5 ff40437a1a Phase 10: Mandantenfaehigkeit
Ein Server, mehrere Wehren. Ein Konto, mehrere Zugehoerigkeiten. Eine
Kreisebene, die zusieht.

Datenmodell und RLS trugen seit Phase 0 mehrere Wehren. Was fehlte, war
alles darueber: bis hierher war die Personenakte zugleich das Anmeldekonto,
und die gehoert genau einer Wehr. Wer in zweien gefuehrt wurde, brauchte
zwei Passwoerter, und die Anmeldung per E-Mail war mehrdeutig.

Neu sind `Benutzer` (das Konto) und `Mitgliedschaft` (die Zugehoerigkeit).
Das Konto traegt bewusst keine Mandantenkennung und kann keine tragen - es
ist der Angelpunkt, an dem ein Mensch zu mehreren Wehren gehoert. Geschuetzt
wird es deshalb ueber Rechte statt ueber RLS, wie `Berechtigung` seit Phase
0. Was es in einer Wehr darf, entscheidet seine Mitgliedschaft.

Die Sitzung behaelt ihre Form und entsteht nur anders - aus der aktiven
Mitgliedschaft statt aus der Person. Von 414 Fundstellen in 61 Dateien
musste dadurch keine angefasst werden. Die eine Ausnahme ist Absicht:
`personId` ist nullbar geworden, weil eine Aufsichtssitzung keine Akte hat,
und der Typechecker hat daraus genau die fuenf Stellen gemacht, an denen
die Kreisaufsicht nichts verloren hat.

Ihre Rechte werden gefiltert statt aufgezaehlt: alles, was `.lesen` heisst,
minus Kasse, Audit-Log, KI-Protokoll und Personenexport. So bekommt sie
neue Leserechte automatisch und neue Schreibrechte nie.

Die Migration ist datenerhaltend und bricht ab, statt einen halben Zustand
zu hinterlassen.

Nebenbei behoben: `db:test-reset` liess die Nachbarwehr stehen, der
Fehlversuch-Test sperrte das gemeinsame Testkonto, ein Testaufbau war
nicht wiederholbar, und die Wartelogik der Bildschirmfotos konnte
ausloesen, bevor eine Seite ueberhaupt geladen war.

581 Unit-Tests und 266 E2E-Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 13:43:24 +02:00

468 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Aufwandsschätzung
Schätzung je Phase, daneben der tatsächliche Aufwand aus dem
[Devlog](DEVLOG.md). Stand: 4. September 2026.
## Wie hier gerechnet wird
Es stehen bewusst **zwei** Größen nebeneinander, weil sie Verschiedenes
aussagen:
| Größe | Bedeutung |
|---|---|
| **Sitzungszeit** | Wie lange Claude tatsächlich gearbeitet hat. Das ist die Zahl, die im Devlog steht, und die Grundlage für die Planung der nächsten Sitzung. |
| **Vergleichsaufwand** | Was eine einzelne Person mit diesem Stack für dasselbe Ergebnis gebraucht hätte, in Personentagen zu 8 Stunden. Das ist die Zahl, die gegenüber der Wehr etwas bedeutet — sie beantwortet „was wäre das sonst wert gewesen". |
Der Vergleichsaufwand ist eine **Schätzung**, keine Messung. Er unterstellt
jemanden, der den Stack kennt, die Feuerwehr-Fachlichkeit aber erst klären
muss — also Recherche zu DGUV-Fristen, FwDV 3 und FwDV 7 einschließt.
Faktoren, die in die Schätzung eingehen:
- **Fachlichkeit** — wie viel muss geklärt werden, bevor gebaut werden kann?
- **Sicherheitsbezug** — Atemschutz und Prüfnachweise brauchen mehr Tests und
mehr Sorgfalt als eine Adressliste.
- **Fremdsysteme** — Gotenberg, Nominatim, tileserver-gl, Ollama, RFID-Leser.
Jedes ist eine eigene Fehlerquelle.
- **Offline und Echtzeit** — die teuersten Anforderungen im ganzen Projekt.
## Übersicht
| Phase | Inhalt | Vergleichsaufwand | Sitzungszeit geschätzt | Sitzungszeit tatsächlich |
|---|---|---:|---:|---:|
| 0 | Fundament | 12–16 PT | — | **1:00 h** |
| 1 | Stammdaten | 10–14 PT | 2,5–3,5 h | **2:05 h** |
| 2 | Wartung und Prüfung | 18–24 PT | 4,5–6 h | **2:25 h** |
| 3 | Einsätze und Übungen | 12–16 PT | 3–4 h | **2:10 h** |
| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | **3:00 h** |
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | **2:40 h** |
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | **2:50 h** |
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | **3:10 h** |
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | **3:40 h** |
| 9 | Kassenverwaltung | 8–11 PT | 2–3 h | **4:10 h** |
| 10 | Mandantenfähigkeit | 4–6 PT | 1–1,5 h | **4:20 h** |
| | **Summe** | **127–170 PT** | **29–38,5 h** | **32:50 h** |
123–164 Personentage entsprechen etwa **6 bis 8 Monaten Vollzeitarbeit** einer
einzelnen Person. Zum Vergleich: eine kommerzielle Feuerwehrverwaltung kostet
je nach Umfang 500 bis 2.500 Euro im Jahr, dauerhaft, mit den Daten beim
Anbieter.
---
## Phase 0 — Fundament
**Vergleichsaufwand: 12–16 PT** · **Tatsächlich: 1:00 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| Monorepo, Werkzeuge, TypeScript, Tailwind, shadcn | 1,5 |
| Docker Compose, Dockerfile, Traefik, Healthchecks | 2,5 |
| Prisma-Schema der Basismodelle | 2,0 |
| Row Level Security samt Rollen- und Rechtekonzept in Postgres | 2,5 |
| Auth.js mit OIDC, lokalem Login und Terminal-Anmeldung | 3,0 |
| storage, audit, warteschlange, Integrationsschnittstellen | 2,0 |
| Basis-Layout, Anmeldeseite, Übersicht, Dark Mode | 1,5 |
| Seed, Vitest, Playwright, Dokumentation | 1,5 |
Das Teuerste war nicht der sichtbare Teil, sondern die Mandantentrennung. RLS
so aufzusetzen, dass Migrationen, Seed und Anmeldung funktionieren und die
Anwendung trotzdem nichts Fremdes sieht, verlangt zwei getrennte
Datenbankzugänge und ein sauberes Verständnis davon, wann welcher greift. Wer
das nachträglich einbaut, zahlt ein Vielfaches.
## Phase 1 — Stammdaten
**Vergleichsaufwand: 10–14 PT** · **Tatsächlich: 2:05 h Sitzungszeit** (1:15 h Hauptteil, 0:50 h Nachtrag)
| Baustein | PT |
|---|---:|
| Modelle: Standort, Geraeteraum, Geraetetyp, Qualifikation | 1,5 |
| Fahrzeug und Geraet um DIN-Typisierung und Lebensdauer erweitern | 1,5 |
| Tabellen mit Sortierung, Filter, Volltextsuche, Spaltenauswahl | 3,0 |
| QR-Code je Gerät, Etikettenbogen als PDF (65 Etiketten, DIN A4) | 1,5 |
| QR-Scan im Browser über die Kamera | 1,0 |
| CSV-/Excel-Import mit Spaltenzuordnung und Vorschau | 2,5 |
| Gerätehistorie aus dem Audit-Log | 1,0 |
| Massenbearbeitung und Tastaturbedienung | 1,0 |
Der Import ist der Posten, der erfahrungsgemäß aus dem Ruder läuft. Jede Wehr
bringt eine andere Excel-Datei mit, und die Vorschau vor dem Übernehmen ist
kein Komfort, sondern die Bedingung dafür, dass die Wehr dem Ergebnis traut.
**Rückblick.** Die Schätzung hat gut getroffen — mit einer Verschiebung: Der
Import war teurer als angesetzt (deutsche Datumsformate, Semikolon-CSV,
Windows-1252, Excel-Seriennummern, Dubletten in der Datei), die Tabellen
dagegen billiger, weil Sortierung und Blättern serverseitig laufen und keine
Tabellenbibliothek nötig war. Nicht eingeplant war die Zeitzonen-Falle in der
Fristenrechnung; sie hätte in Phase 2 deutlich mehr gekostet, wenn sie erst
dort aufgefallen wäre.
Das zunächst offen gebliebene Formular zum einzelnen Anlegen von Geräten wurde
als Nachtrag ergänzt — geschätzt waren 0,5 PT, tatsächlich 0:50 h Sitzungszeit.
Die Hälfte davon ging für zwei Dinge drauf, die nichts mit dem Formular zu tun
hatten: eine Server/Client-Grenze, die stillschweigend `undefined` liefert, und
Datumsfelder, die sich in Chromium nicht automatisiert füllen lassen.
## Phase 2 — Wartung und Prüfung
**Vergleichsaufwand: 18–24 PT** · **Tatsächlich: 2:25 h Sitzungszeit** — die aufwendigste Phase
| Baustein | PT |
|---|---:|
| Modelle: Vorlage, Checklistenpunkt, Termin, Prüfung, Ergebnis, Mangel, Rückruf | 3,0 |
| Fristen-Dashboard mit Gruppierungen | 2,5 |
| Nächtlicher BullMQ-Job: Termine erzeugen, Status setzen, erinnern | 2,5 |
| Prüfungs-Wizard mit Fotoupload je Checklistenpunkt | 3,5 |
| Unterschrift auf Canvas, Touch und Maus | 1,5 |
| PDF-Protokoll über Gotenberg mit Fotos und Signatur | 3,0 |
| Unveränderlichkeit und Korrekturprotokolle mit Verweis | 1,5 |
| Lebensdauerüberwachung und Aussonderungsvorschlag | 1,5 |
| Rückrufe, externe Wartung, Mängel-Workflow | 2,5 |
Hier entsteht der eigentliche Wert: die Software wird zum Nachweis gegenüber
der Unfallkasse. Das heißt aber auch, dass jeder Fehler teuer ist. Die
Unveränderlichkeit abgeschlossener Protokolle ist der Kern — und der Grund,
warum der Audit-Log schon in Phase 0 append-only ist.
**Rückblick.** Deutlich unter der geschätzten Sitzungszeit, und das aus einem
Grund, der sich benennen lässt: die Fristenrechnung aus Phase 1 war schon da
und musste nur exportiert werden. Wäre sie hier entstanden — samt der
Zeitzonen-Falle, über die Phase 1 gestolpert ist —, sähe die Zahl anders aus.
Teurer als gedacht war das, was zwischen den Bausteinen liegt. Drei Beispiele:
- Die Reihenfolge beim Abschluss einer Prüfung ist nicht beliebig.
Unterschriften und Mängel müssen vor dem Statuswechsel entstehen, denn danach
lässt die Datenbank am Protokoll nichts mehr zu. Derselbe Stolperstein traf
auch den Seed, der die Beispielprüfung zunächst fertig anlegen wollte.
- Die Protokollnummer muss unter einer Sperre vergeben werden. Aufgefallen ist
das erst, weil die E2E-Tests Desktop und Tablet gleichzeitig prüfen ließen —
zwei Prüfer, die im selben Moment abschließen, sind im Gerätehaus kein
konstruierter Fall.
- Das PDF entsteht zwischen zwei Datenbanktransaktionen, nicht in einer. Fotos
aus MinIO holen und Gotenberg rendern lassen dauert länger, als eine offene
Transaktion warten darf.
Billiger als angesetzt war das Unterschriftenfeld: Pointer-Events decken Finger,
Stift und Maus in einem ab, und mehr als Skalierung auf die Pixeldichte und
`touch-action: none` braucht es nicht.
## Phase 3 — Einsätze und Übungen
**Vergleichsaufwand: 12–16 PT** · **Tatsächlich: 2:10 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| Modelle: Einsatz, Einsatzfahrzeug, Einsatzkraft, Material, Lagemeldung, Übung | 2,0 |
| Schnellerfassung mit Nachtragen | 1,5 |
| Geocoding über self-hosted Nominatim mit Rückfallweg | 1,5 |
| Personalauswahl nach Verfügbarkeit und Qualifikation | 2,0 |
| Stärkeberechnung nach FwDV 3 | 1,0 |
| Einsatzbericht als PDF, Jahresexport für die Gemeinde | 2,0 |
| Statistik: Kategorien, Eintreffzeiten, Beteiligung, Auslastung | 2,5 |
| Externes Portal mit signiertem QR-Link, ohne Login, rate-limited | 2,0 |
Das externe Portal für Fremdwehren ist sicherheitsrelevant: ein Zugang ohne
Anmeldung, der zeitlich begrenzt und mengenbegrenzt sein muss. Der Aufwand
steckt weniger in der Funktion als in der Absicherung.
**Rückblick.** Die Schätzung lag richtig darin, wo der Aufwand steckt — nur
nicht in welcher Reihenfolge.
Billiger als gedacht war das Portal. Der Grund liegt in Phase 0 und 2: die
Mandantentrennung, das Hashen von Geheimnissen und die Ablage von Dateien gab
es schon, und der Zugang war am Ende ein Datensatz mit Hash, ein Zeitfenster
und ein Zähler in Redis. Was blieb, war die Frage, was das Portal **nicht**
zeigen darf — und die ist fachlicher Natur, nicht technischer.
Teurer war die Statistik. Nicht das Rechnen, sondern die Entscheidung, welche
Zahl gilt: der Durchschnitt der Eintreffzeiten sagt nichts, weil ein einzelner
Einsatz auf der Autobahn ihn verschiebt. Es zählt der 80-Prozent-Wert, weil die
Gemeinde danach fragt — und das steht in keinem Pflichtenheft, das muss man
wissen.
Zwei Dinge fielen erst beim Tun auf:
- Koordinaten müssen doppelt liegen. Prisma kann den PostGIS-Typ nicht lesen;
ohne die Dezimalgrade daneben wäre jede Anzeige eine Rohabfrage geworden. Ein
Trigger hält beides zusammen.
- Der Nummernkreis war schon zweimal da (Prüfprotokolle, jetzt Einsätze und
Übungen). Beim dritten Mal wurde daraus ein eigener Baustein — die passende
Gelegenheit war genau diese, keine früher.
## Phase 4 — Kiosk-PWA
**Vergleichsaufwand: 16–22 PT** · **Tatsächlich: 3:00 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| Eigenes Kiosk-Layout, 64px-Ziele, hoher Kontrast, kein Hover-Zwang | 2,0 |
| Anmeldung per RFID (WebHID), PIN und QR vom Handy | 2,5 |
| Wizards: Wartung, Fahrtenbuch, Mangel, Inventur, Wäsche, Anwesenheit | 4,0 |
| Hallen-Monitor mit DWD-Wetterwarnung | 2,0 |
| Service Worker, App-Shell und Stammdaten offline | 3,0 |
| IndexedDB-Outbox mit Idempotenzschlüssel und Synchronisation | 4,0 |
| Konfliktbehandlung und Konfliktliste für den Administrator | 2,0 |
| Playwright-Tests für den Offline-Fall | 1,5 |
Offline-Fähigkeit mit schreibenden Aktionen ist die technisch schwierigste
Anforderung des ganzen Projekts. Eine Outbox ist schnell gebaut; dass sie
unter Verbindungsabbruch, Doppelklick und parallelem Zugriff von zwei
Terminals nichts verliert und nichts doppelt schreibt, ist die eigentliche
Arbeit. Die Idempotenzschlüssel sind dafür der Schlüssel, nicht die Synchronisation.
**Rückblick.** Der Satz oben hat sich bestätigt, nur anders als gedacht. Die
Outbox selbst war in einer halben Stunde geschrieben und mit zwölf Unit-Tests
belegt. Zwei Stellen kosteten die Zeit, und keine davon steht in der
Aufstellung:
- **Der Zeitgeber, der nie ablief.** Die Warteschlange hatte einen
Wiederholungstakt von 20 Sekunden — und der wurde bei jedem Rendern neu
aufgezogen, weil das Mutationsobjekt von React Query in den Abhängigkeiten
stand. Alles sah richtig aus, alle Unit-Tests waren grün, und der liegen
gebliebene Vorgang wäre im Gerätehaus trotzdem liegen geblieben. Gefunden hat
das nur der Playwright-Test, der `context.setOffline(true)` setzt und dann
wartet. Die 1,5 PT für „Playwright-Tests für den Offline-Fall" waren der
bestangelegte Posten der Phase.
- **Die Frage, wann ein Vorgang aus der Warteschlange darf.** Nicht „wenn
erfolgreich", sondern „wenn er beim Server angekommen ist" — dazu gehört auch
„bereits verarbeitet" und „in der Konfliktliste". Diese Unterscheidung ist die
ganze Idempotenz; ohne sie kreist ein Vorgang für immer.
Billiger als angesetzt war die Anmeldung per RFID: die Aufstellung nennt WebHID,
gebraucht wurde es nicht. Handelsübliche Leser geben die Kartenkennung als
Tastatureingabe aus. Ein Zuhörer auf dem Fenster, der am Anschlagstempo
erkennt, dass da keine Hand tippt, ersetzt eine ganze Geräteschnittstelle.
Billiger war auch der Service Worker — 80 Zeilen von Hand, ohne Workbox. Die
Regeln sind einfach, solange man sie sich traut aufzuschreiben: Seiten aus dem
Netz mit Ausweichseite, lesende Abfragen aus dem Zwischenspeicher mit
Nachladen, schreibende **überhaupt nicht** anfassen. Eine Bibliothek hätte hier
mehr Fragen aufgeworfen als beantwortet.
Teurer war die Terminaloberfläche selbst. Nicht die Kacheln, sondern die
Zustände dahinter: Wer schon eingecheckt ist, sieht „Gehen" statt „Kommen". Wer
mit einem Fahrzeug unterwegs ist, sieht „Fahrt beenden". Wer keine Rechte für
die Inventur hat, sieht die Kachel nicht. Jede dieser Verzweigungen ist
trivial — sechs davon in einem Schirm, der ohne Nachdenken bedienbar bleiben
soll, sind es nicht.
## Phase 5 — Atemschutz nach FwDV 7
**Vergleichsaufwand: 14–18 PT** · **Tatsächlich: 2:40 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| Modelle: Atemschutzeinsatz, Trupp, Druckmessung, G26, Belastungsübung | 2,0 |
| Truppanlage per RFID, Eingangsdruck | 1,5 |
| Serverseitig autoritative Timer, Socket.IO-Verteilung | 3,5 |
| Rückzugsdruck-Berechnung, Ampel, akustischer Alarm | 2,0 |
| Erinnerung zur Druckabfrage im Intervall | 1,0 |
| Mayday-Knopf mit Protokolleintrag | 1,0 |
| Lückenloses PDF-Protokoll am Einsatz | 1,5 |
| G26.3-Fristen, Belastungsübungen, Tauglichkeitsampel | 2,0 |
| Tests: Reload und Verbindungsabbruch überstehen | 2,0 |
Der Aufwand liegt nicht in der Funktion, sondern im Beweis, dass sie hält. Ein
Timer, der einen Reload überlebt, ist Pflicht — deshalb muss die Zeit
serverseitig geführt werden und der Browser nur anzeigen. Diese Phase bekommt
den höchsten Testanteil im Projekt.
**Rückblick.** Der Satz stimmt, aber die Schlussfolgerung war die falsche. Die
Aufstellung veranschlagt 3,5 PT für „serverseitig autoritative Timer" — als
wäre ein Timer zu bauen. Gebraucht wurde das Gegenteil: **kein** Timer. Wer die
Zeit als Zeitstempel in die Datenbank schreibt und jede Ansicht selbst rechnen
lässt, hat nichts, was einen Reload nicht überlebt. Der teuerste Posten der
Schätzung war der billigste der Umsetzung, weil die richtige Entscheidung ihn
auflöst statt ihn zu lösen.
Auch Socket.IO fiel weg. Es geht nur in eine Richtung — der Server meldet eine
Änderung, der Browser holt den Stand über den Weg, den es ohnehin gibt. Ein
Route Handler mit Server-Sent Events ist siebzig Zeilen und braucht kein
verändertes Deployment.
Teurer war das, was in keiner Zeile der Aufstellung steht: **die Frage, was
passiert, wenn nichts passiert.** Ein abgerissener Ereignisstrom meldet sich
nicht — `EventSource` schweigt einfach. Eine Anzeige, die deshalb weiter
„Verbunden" behauptet, ist gefährlicher als eine, die gar nichts anzeigt: sie
lässt einen alten Stand wie einen aktuellen aussehen. Aufgefallen ist das nur,
weil der E2E-Test das Netz gekappt und danach nachgesehen hat — die 2,0 PT für
„Tests: Reload und Verbindungsabbruch überstehen" waren wieder der
bestangelegte Posten der Phase.
Billiger als gedacht war die Tauglichkeit. Nicht das Rechnen — die
Entscheidung, **nicht** zu sperren. Damit entfielen alle Sonderfälle, die eine
Sperre nach sich zieht: Übersteuern, Begründungspflicht, Freigabe durch wen.
Was bleibt, ist ein Satz neben dem Namen, und der ist im Einsatz mehr wert.
## Phase 6 — Kartenmodule
**Vergleichsaufwand: 14–18 PT** · **Tatsächlich: 2:50 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| MapLibre GL, tileserver-gl, MBTiles fürs Ausrückegebiet | 2,5 |
| Hydranten mit PostGIS, Cluster, Filter, Umkreissuche | 3,0 |
| Import aus GeoJSON und Stadtwerke-CSV | 1,5 |
| Objekte und Feuerwehrpläne nach DIN 14095, Volltextsuche | 3,0 |
| Lagekarte mit taktischen Zeichen nach FwDV 102 als SVG-Set | 3,5 |
| Echtzeit-Sync der Lagekarte zwischen Geräten | 2,0 |
| Wasserförderung: Höhenprofil, Druckverlust, Verstärkerpumpen | 2,5 |
Das SVG-Set für die taktischen Zeichen ist Fleißarbeit, aber unvermeidbar —
fertige Sets sind selten frei verwendbar. Die Wasserförderungsberechnung ist
Physik mit klaren Formeln und deshalb gut testbar.
**Rückblick.** Beide Sätze stimmen. Das Zeichenset war Fleißarbeit — 22 Zeichen
als SVG-Pfade, keine davon schwierig, alle zusammen eine Stunde. Und die
Wasserförderung war tatsächlich der am besten testbare Teil des ganzen
Projekts: 21 Tests, die alle etwas über die Physik aussagen statt über den Code.
Dabei fiel auf, dass die Beiwerte im ersten Wurf um den Faktor zehn danebenlagen
— aufgefallen ist es nur, weil die Tests gegen die **Tabellenwerte der
Ausbildung** prüfen und nicht gegen das, was die Funktion gerade ausrechnet.
Ein Test, der die eigene Implementierung bestätigt, hätte hier fröhlich grün
gemeldet.
Billiger als angesetzt war die Umkreissuche. 3,0 PT für „Hydranten mit PostGIS,
Cluster, Filter, Umkreissuche" — davon entfiel der Löwenanteil auf eine einzige
SQL-Abfrage mit `ST_DWithin` und einen GiST-Index. PostGIS war seit Phase 0 im
Container; genutzt wurde es erst jetzt.
Teurer war die Frage, was passiert, wenn es **keine Karte** gibt. Eine MBTiles-
Datei des Ausrückegebiets ist hunderte Megabyte groß, gehört niemandem außer
der Wehr und kann nicht im Repository liegen. Das heißt: der Normalfall beim
ersten Start ist eine Karte ohne Hintergrund — und sie muss trotzdem etwas
taugen. Aus dieser Anforderung kam der leere Stil mit Hinweis, die Erkennung
eines nicht erreichbaren Servers und die Zusicherung im E2E-Test, dass genau
das ehrlich angezeigt wird. In der Aufstellung steht davon keine Zeile.
## Phase 7 — Benachrichtigungen und Termine
**Vergleichsaufwand: 10–13 PT** · **Tatsächlich: 3:10 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| Kanalabstraktion: E-Mail, Telegram, ntfy, Web-Push | 2,5 |
| Vorlagen mit Platzhaltern, je Ereignis und Rolle | 2,0 |
| Abonnements je Person | 1,0 |
| Kalender mit Serienterminen und Selbstanmeldung | 3,0 |
| ICS-Feed je Person mit Token | 1,5 |
| Digitaler Dienstausweis mit signiertem QR und Prüfseite | 2,0 |
Das Grundgerüst der Kanalabstraktion stand bereits aus Phase 0
(`lib/integrations/`), das hat etwa einen Personentag gespart.
Die einzige Phase bisher, die über der geschätzten Sitzungszeit liegt — und
zwar an einer Stelle, die in der Aufstellung nicht vorkommt. „Kalender mit
Serienterminen" klingt nach einer Datumsrechnung; teuer war die Entscheidung
**gegen** eine Regel und **für** ausgeschriebene Termine, und dann die
Kalenderarithmetik dahinter: `setMonth(+1)` auf dem 31. Januar ergibt den
3. März, der 29. Februar existiert in vier von fünf Jahren nicht, und ein
Übungsabend muss über die ganze Serie auf demselben Wochentag bleiben. Das sind
drei Tests, die man erst schreibt, wenn man den Fall kennt.
Die 1,5 PT für den Feed waren dagegen zu hoch angesetzt, weil sie CalDAV
unterstellen. ICS ist ein Textformat; der ganze Erzeuger sind hundertsechzig
Zeilen samt Maskierung und Zeilenumbruch nach 75 Oktetten.
## Phase 8 — Lokale KI
**Vergleichsaufwand: 9–12 PT** · **Tatsächlich: 3:40 h Sitzungszeit**
| Baustein | PT |
|---|---:|
| `lib/ai/` mit OpenAI-kompatiblem Client, abschaltbar | 1,0 |
| Diktat: Aufnahme im Browser, faster-whisper, Transkript | 2,5 |
| Berichtsassistent mit Übernehmen/Verwerfen | 2,0 |
| Wiki mit pgvector, semantische Suche, RAG mit Quellenangabe | 3,5 |
| Belegerfassung über ein Vision-Modell, immer bestätigen | 2,0 |
Der Client aus Phase 0 (`lib/integrations/llm.ts`) war bereits vorhanden — er
konnte allerdings nur Text und musste um Einbettungen und Bilder erweitert
werden.
Die Kennzeichnungspflicht war mit „kostet wenig Zeit" zu günstig eingeschätzt.
Sie kostet wenig, wenn man sie als Abzeichen an einer Ansicht versteht. Als
Zusicherung — eigener Datensatz je Ausgabe, unveränderlich, genau eine
Entscheidung, Trigger und REVOKE dahinter, zwei Protokollansichten — ist sie
ein guter halber Personentag, der in der Aufstellung gar nicht vorkommt.
Zwei weitere Posten fehlen in der Schätzung:
**Das eigene Postgres-Image.** pgvector und PostGIS gibt es nicht zusammen; es
gibt kein offizielles Image mit beidem. Ein Dockerfile, ein Neubau, ein Neustart
des Dienstes — eine halbe Stunde, aber sie stand nirgends.
**Der Betrieb ohne Modell.** Der größte Einzelposten dieser Phase, und in der
Aufstellung steht davon keine Zeile. Eine Wehr setzt FLORIAN auf, lange bevor
sie über eine Grafikkarte nachdenkt — also muss jede Ansicht ohne Modell
vollständig benutzbar bleiben und ehrlich sagen, was fehlt. Das sind vier
verschiedene Hinweise an vier Stellen, eine zweite Suche, die immer
funktioniert, und vierzehn E2E-Tests, die genau diesen Zustand absichern.
## Phase 9 — Kassenverwaltung
**Vergleichsaufwand: 8–11 PT** — am 03.09.2026 ergänzt
| Baustein | PT |
|---|---:|
| Modelle: Konto, Kategorie, Buchung, Beitragssatz, Beitrag, Rechnung, Abschluss | 2,0 |
| Buchungsmaske mit Kategorievorschlag, Beleg als Datei | 1,5 |
| Beitragslauf, Mahnstufen, Zahlungsabgleich aus CAMT.053 oder MT940 | 2,5 |
| Rechnungen mit Nummernkreis und PDF | 1,0 |
| Kassenabschluss mit Zeitraumsperre und Prüfbericht | 1,5 |
| Auswertungen und Exporte | 1,5 |
Der Zahlungsabgleich ist der unsichere Posten: CAMT.053 ist gut spezifiziert,
MT940 dagegen je nach Bank unterschiedlich ausgelegt. Falls die Hausbank nur
MT940 liefert, kommt ein halber bis ganzer Tag für die Eigenheiten dazu.
Die Unveränderlichkeit festgeschriebener Buchungen kostet hier fast nichts —
das Muster steht seit Phase 0 mit dem Audit-Log-Trigger und lässt sich
übernehmen.
**Tatsächlich: 4:10 h** — die längste Phase bisher. Beide Schätzungen oben
haben sich bestätigt und zugleich verschoben: MT940 kostete weniger als
gedacht, weil beide Formate ohne Fremdbibliothek auskommen. Dafür war die
Unveränderlichkeit nicht das Übernehmen eines Musters, sondern drei
Trigger mit drei genau begründeten Ausnahmen — und die Ausnahmen sind der
Teil, der Zeit gekostet hat. Zwei davon sind erst im Test aufgefallen:
der Prüfbericht, der nach dem Festschreiben entsteht, und die
DSGVO-Löschung einer Wehr, die an ihren eigenen festgeschriebenen Buchungen
scheiterte.
## Phase 10 — Mandantenfähigkeit
**Vergleichsaufwand: 4–6 PT** — aus der Kandidatenliste vorgezogen, am
05.09.2026 umgesetzt.
| Baustein | PT |
|---|---:|
| `Benutzer` und `Mitgliedschaft`, datenerhaltende Umstellung | 1,5 |
| Anmeldung: Konto statt Akte, Wehrwechsel im Sitzungstoken | 1,5 |
| Kreisebene mit reinen Leserechten | 1,0 |
| Wehrwähler und Verwaltung der Installation | 1,5 |
Die Schätzung ging von „technisch bereits möglich, Oberfläche fehlt" aus —
und das war zur Hälfte falsch. Datenmodell und RLS trugen tatsächlich mehrere
Wehren, aber die Anmeldung nicht: Passwort und OIDC-Subject hingen an der
Personenakte, und die gehört genau einer Wehr. Das war kein Oberflächenthema,
sondern ein Umbau am Fundament.
**Tatsächlich: 4:20 h.** Getragen hat eine Entscheidung ganz am Anfang: die
Sitzung behält ihre Form und entsteht nur anders. Ohne sie wären 61 Dateien
mit 414 Fundstellen zu ändern gewesen; so waren es acht. Die Zeit ging
stattdessen in die Absicherung — zwei Trigger, vier CHECKs und elf
Datenbanktests — und das ist die richtige Verteilung für eine Phase, deren
einziger Zweck es ist, dass eine Wehr die andere nicht sieht.
---
## Was hier nicht drin ist
**Betrieb und Einführung.** Datenübernahme aus der Altsoftware, Schulung der
Wehr, Sicherungskonzept, Monitoring: nach Erfahrung noch einmal 15 bis 25
Prozent des Entwicklungsaufwands.