Commit Graph
13 Commits
Author SHA1 Message Date
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
florian.wilde 2b53556175 Phase 9: Kassenverwaltung (#14) 2026-09-05 02:30:51 +00:00
Florian WildeandClaude Opus 5 7196c7110c Aufraeumarbeiten: ESLint, DOM-Verschachtelung, eigene Testwehr
ESLint hatte keine Konfiguration — das Skript "npm run lint" pruefte nichts.
Jetzt Flat Config ohne Formatierungsregeln, dafuer mit den Regeln, die die
Fehler dieses Projekts gemeldet haetten. Er hat gleich zwei gefunden, die
niemand gesucht hatte.

Das Badge rendert ein span statt eines div. Damit ist die Meldung
"div cannot be a descendant of p" weg, die seit Phase 3 auf der Einsatzseite
stand — sie ist der klassische Ausloeser fuer einen Hydration-Fehler.

Die E2E-Tests laufen gegen eine eigene Wehr. Nach fuenf Phasen standen in der
Beispielwehr 120 Atemschutz-Ueberwachungen statt einer und 115 Geraete statt
45; bei jedem Lauf aenderten sich alle Bildschirmfotos. Der Bestand ist einmal
zurueckgesetzt, npm run db:test-reset baut die Testwehr neu auf.

Behoben nebenbei:
- Die Fehlerflaeche der Karte konnte nie etwas anzeigen.
- Zwei useMemo der Lagekarte rechneten bei jedem Rendern neu.
- tests/e2e war vom Typecheck ausgenommen und liess einen doppelt
  deklarierten Bezeichner durch.

Closes #10
Closes #11
Closes #12

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 13:24:48 +02:00
Florian WildeandClaude Opus 5 f7ccd9a908 Phase 8: Lokale KI
Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper,
Berichtsassistent und Belegerfassung — alles gegen selbst betriebene
Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was
Menschen getan haben.

Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede
Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich
grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und
Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal.

Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion.
Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von
vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen.

Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und
faster-whisper stehen im Compose-Profil "ki".

73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 11:06:03 +02:00
Florian WildeandClaude Opus 5 0b8874da26 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>
2026-09-04 08:39:38 +02:00
Florian WildeandClaude Opus 5 1c1e141666 Kartenmodule ergaenzen
Phase 6: alles, was einen Ort hat — Loeschwasser, Objekte mit Feuerwehrplan,
die Lagekarte eines Einsatzes und die geplante Foerderstrecke.

Keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre Hydranten
liegen, ist niemandes Geschaeft; der Kartenserver laeuft selbst
(tileserver-gl im Compose-Profil "karte"). Ohne ihn zeigt die Karte einen
leeren Grund und sagt das auch — Punkte, Entfernungen und Umkreissuche
funktionieren trotzdem. Dasselbe gilt fuer einen eingetragenen, aber nicht
erreichbaren Server.

Die Umkreissuche laeuft ueber PostGIS: ST_DWithin auf der Geografie mit
GiST-Index rechnet in Metern und nutzt den Index. Gefragt wird waehrend der
Fahrt, und bei tausend Hydranten waere alles andere ein vollstaendiger
Tabellendurchlauf.

Der Import aus GeoJSON und Stadtwerke-CSV ist nachsichtig, aber ehrlich: er
erkennt "DN 100", deutsche Kommazahlen, Datumsangaben und vertauschte Achsen
— raet aber nichts. Fehlt die Position, wird die Zeile uebersprungen; ohne
Statusangabe steht "ungeprueft", nicht "einsatzbereit".

22 taktische Zeichen nach FwDV 102 selbst gezeichnet, die Lagekarte gleicht
ueber denselben Ereignisstrom ab wie die Atemschutzueberwachung.

Die Wasserfoerderung rechnet Reibung, Hoehe und Pumpenzahl; das Ergebnis wird
nie gespeichert, sondern bei jeder Anzeige neu gerechnet. Geht eine Strecke
nicht auf, sagt die Ausgabe das.

Behoben: die Reibungsbeiwerte lagen im ersten Wurf um den Faktor zehn daneben.
Aufgefallen ist es nur, weil die Tests gegen die Tabellenwerte der Ausbildung
pruefen und nicht gegen die eigene Implementierung.

Dazu ein Abruf fuer abgelegte Dateien (/api/dateien/[id]) — bis hierher gab es
nur den Upload.

59 neue Tests (48 Unit, 11 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 01:41:24 +02:00
Florian WildeandClaude Opus 5 4e2daaafd4 Atemschutzueberwachung nach FwDV 7 ergaenzen
Phase 5: das sicherheitsrelevanteste Modul des Projekts. Ueber jeder Ansicht
steht dauerhaft, dass die analoge Atemschutzueberwachung fuehrend bleibt —
abschaltbar nur zusammen mit der Fundstelle einer Freigabe des Kommandanten.

Es gibt keinen Timer. In der Datenbank stehen Zeitstempel; wie lange ein
Trupp unter Atemschutz ist, rechnet jede Ansicht bei jedem Bild neu aus.
Damit ist die Anforderung "uebersteht Neuladen und Verbindungsabbruch" keine
Funktion, die gelingen muss, sondern eine Eigenschaft, die sich nicht
verlieren laesst. Server und Browser rechnen mit denselben Funktionen.

Der Rueckzugsdruck folgt dem doppelten Anmarschverbrauch: was der Weg hinein
gekostet hat, kostet der Weg zurueck noch einmal. Bis der Trupp das
Einsatzziel meldet, greift die Zwei-Drittel-Regel — sie liegt hoeher und ist
damit die vorsichtigere Annahme.

Das Protokoll ist doppelt gesichert: entzogene Rechte und ein Trigger auf
druckmessung und atemschutz_ereignis. Ein Fehler wird durch einen neuen
Eintrag richtiggestellt, nicht durch Aendern.

Verteilt wird ueber Server-Sent Events statt Socket.IO — es geht nur in eine
Richtung, und so bleibt das Deployment bei "next start".

Behoben: ein abgerissener Ereignisstrom meldete sich nicht. Die Anzeige haette
weiter "Verbunden" behauptet, waehrend der Draht tot war. Jetzt gilt Stille
von acht Sekunden als Abriss. Dazu zwei Altlasten: der Navigationseintrag
"Protokoll" fuehrte ins Leere, und die Startseite stand noch auf Phase 1.

68 neue Tests (51 Unit, 17 gegen eine echte Datenbank, 8 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 23:19:05 +02:00
Florian WildeandClaude Opus 5 791523027f Terminal im Geraetehaus mit Offline-Betrieb ergaenzen
Phase 4: eine eigene Oberflaeche unter /kiosk fuer das Wandtablet in der
Fahrzeughalle — mit Handschuhen bedienbar und arbeitsfaehig, wenn das Netz
weg ist.

Datenmodell: Fahrt, Anwesenheit, Waescheabgabe, Inventurgang,
Inventurposten, KioskVorgang und SyncKonflikt. Die Kennung eines Vorgangs
erzeugt der Browser beim Einreihen und ist damit der Idempotenzschluessel;
kommt derselbe Vorgang nach abgebrochener Uebertragung erneut an, erkennt
ihn der Server wieder. Die Datenbank setzt vier Regeln selbst durch und
verweigert UPDATE und DELETE auf kiosk_vorgang.

Offline: von Hand geschriebener Service Worker, Warteschlange in der
IndexedDB (Dexie), Fotos als Blob daneben, sichtbarer Zustand als Leiste
ueber allem. Was sich beim Nachtragen nicht anwenden laesst, landet als
Konflikt unter /terminal statt verworfen zu werden.

Dazu: Anmeldung per Ausweis, PIN oder signierter QR-Marke vom Handy,
Hallenmonitor mit optionalen DWD-Warnungen, Installation als App.

Behoben: die Warteschlange zog ihren Wiederholungstakt bei jedem Rendern
neu auf und haette einen liegen gebliebenen Vorgang nie ein zweites Mal
geschickt. Gefunden vom Offline-E2E-Test.

31 neue Tests (20 Unit, 11 gegen eine echte Datenbank, 9 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 18:35:48 +02:00
Florian WildeandClaude Opus 5 91954c402b Phase 3: Einsatz- und Uebungserfassung
Schnellerfassung im Alarmfall, sechs Zeitstempel mit Ausrueckzeit, Anfahrt
und Hilfsfrist, Staerke nach FwDV 3, Personalvorschlag mit
Qualifikationsstand, Einsatzbericht als PDF, Jahresexport fuer die Gemeinde,
Statistik und Uebungen mit Anwesenheitsliste.

Dazu das Einsatzportal fuer Fremdwehren: ein signierter Link ohne Anmeldung,
zeitlich und mengenbegrenzt, der Stichwort, Ort, Zeiten und Lagemeldungen
zeigt und Meldungen mit Foto annimmt. Es sind die ersten oeffentlich
erreichbaren Endpunkte in FLORIAN.

Acht neue Tabellen, alle mit organisationId und Row Level Security. Ein
freigegebener Einsatzbericht ist unveraenderlich; ein Datenbanktrigger setzt
das durch. Koordinaten liegen doppelt — als Dezimalgrad und als
PostGIS-Punkt, zusammengehalten von einem Trigger.

57 neue Tests (32 Unit, 14 gegen eine echte Datenbank, 11 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 13:50:58 +02:00
Florian WildeandClaude Opus 5 e503aafc18 Phase 2: Wartungs- und Pruefmanagement
Wartungsvorlagen mit Checklisten, Prueftermine aus einem naechtlichen Lauf,
Prueffristen-Dashboard, Pruefwizard mit Foto und Unterschrift, unveraenderliche
Protokolle als PDF, Maengel-Workflow, Rueckrufe und Lebensdauerueberwachung.

Elf neue Tabellen, alle mit organisationId und Row Level Security. Die
Unveraenderbarkeit abgeschlossener Protokolle setzt ein Datenbanktrigger
durch, nicht der Anwendungscode: eine Korrektur erzeugt eine neue Pruefung
mit Verweis auf die alte, beide bleiben lesbar.

79 neue Tests (47 Unit, 10 gegen eine echte Datenbank, 22 E2E).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 12:07:11 +02:00
Florian WildeandClaude Opus 5 26b62a3477 Phase 1: Stammdatenverwaltung
Ab hier fuehrt die Wehr ihren Bestand in FLORIAN.

Datenmodell:
- Standort, Geraeteraum, Geraetetyp, Qualifikationstyp und Qualifikation
- Fahrzeug um Fahrgestellnummer, DIN-Typ, Erstzulassung, Sitzplaetze,
  Tankinhalt und UVV-Frist erweitert
- Geraet um Geraetetyp, Lebensdauerkategorie, errechnetes Lebensdauerende,
  Anschaffungspreis und die Lagerung erweitert
- Ein Geraet steht entweder an einem Standort oder ist auf einem Fahrzeug
  verlastet. Eine Pruefbedingung setzt das durch, ein Trigger stellt zusaetzlich
  sicher, dass ein angegebener Geraeteraum zum angegebenen Fahrzeug gehoert.
- RLS auf allen neuen Tabellen, Trigram-Indizes fuer die Volltextsuche

Fachlogik:
- lib/stammdaten/fristen.ts rechnet Prueftermine, Lebensdauer und den
  abgeleiteten Geraetestatus. Der Geraetetyp gibt vor, das Geraet darf
  abweichen.
- lib/stammdaten/qr.ts und qr-parser.ts erzeugen und lesen die Codes
- lib/stammdaten/etiketten.ts baut den A4-Bogen mit 65 Etiketten
- lib/stammdaten/import.ts liest CSV und Excel, erkennt deutsche Datums- und
  Zahlenformate, Windows-1252 und Dubletten innerhalb der Datei
- lib/pdf.ts spricht Gotenberg an

Oberflaeche:
- Geraeteliste mit Volltextsuche, Statusfilter, serverseitiger Sortierung,
  Blaettern, Spaltenauswahl und Massenbearbeitung
- Geraetedetail mit QR-Code, Pruefvorgabe aus dem Geraetetyp und Verlauf
- Fahrzeugliste und -detail mit Beladung nach Beladeordnung
- Mitgliederliste nach Abteilung, Mitgliedsdetail mit Qualifikationen
- Qualifikationen mit Ablaufwarnung je Vorwarnzeit des Typs
- Import-Assistent mit Zuordnung und Vorschau vor dem Uebernehmen
- QR-Scanner ueber die Kamera, mit Eingabefeld als Rueckfallweg

Behoben:
- Fristenrechnung lief ueber startOfDay aus date-fns und damit in der Zeitzone
  des Servers. Ein DATE aus der Datenbank kommt als UTC-Mitternacht und wurde
  dadurch auf den Vortag geschoben — Fristen waeren einen Tag zu frueh faellig
  geworden. Jetzt eigene Kalenderarithmetik auf UTC-Bestandteilen.
- ctx.sitzung blieb in tRPC-Prozeduren nullbar, weil t.middleware() die
  Verengung der vorigen Middleware verwirft. Middleware jetzt direkt an .use().
- CardTitle war ein div und damit keine Ueberschrift. Wer mit einer
  Sprachausgabe arbeitet, springt von Ueberschrift zu Ueberschrift; Karten
  tauchten in dieser Navigation nicht auf.

Daten und Tests:
- Seed um 3 Standorte, 20 Geraeteraeume, 14 Geraetetypen mit echten
  Pruefvorgaben, 49 Geraete und 107 Qualifikationen erweitert
- 104 Vitest-Tests (vorher 32), 44 Playwright-Tests (vorher 10)
- Anleitung um sieben bebilderte Abschnitte erweitert, Changelog, Devlog,
  Aufwandsschaetzung und Roadmap fortgeschrieben

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:31:08 +02:00
Florian WildeandClaude Opus 5 4b3021bb50 Kassenverwaltung als Phase 9 ergaenzen, Festlegungen dokumentieren
Die Rolle KASSIER war seit Phase 0 angelegt, ein Kassenmodul fehlte in der
Planung. Phase-9.md beschreibt es jetzt: Konten, Buchungen mit Beleg,
Beitragslauf mit Mahnstufen, Rechnungen, Kassenabschluss und Auswertungen.
Geschaetzt 8-11 PT, Version 0.10.0.

Zwei Festlegungen aus der Spezifikation, weil sie den Rest bestimmen: eine
festgeschriebene Buchung ist unveraenderbar und wird nur ueber eine
Gegenbuchung korrigiert; der Kassenabschluss friert seinen Zeitraum ein.

Ausserdem als entschieden festgehalten:
- Berechtigung bleibt ohne organisationId. Die Tabelle ist der systemweite
  Katalog dessen, was die Software kann; mandantenbezogen wird es erst ueber
  RolleBerechtigung.
- Die Loeschausnahme im Audit-Log bleibt. UPDATE ist dauerhaft gesperrt,
  DELETE nur im gekennzeichneten Aufbewahrungslauf des Workers.

Roadmap, Aufwandsschaetzung, Versionierung, README und die Seitenleiste sind
entsprechend ergaenzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 07:11:37 +02:00
Florian WildeandClaude Opus 5 845942f36c Phase 0: Fundament der Feuerwehr-Verwaltungsplattform
Legt den kompletten Unterbau an, auf dem die Fachmodule aufsetzen.

Betrieb:
- docker-compose.yml mit Traefik, App, Worker, PostgreSQL 17 + PostGIS,
  Redis, MinIO und Gotenberg; Entwicklungs-Override ohne Traefik, dafuer
  mit Mailpit und Adminer
- Mehrstufiges Dockerfile: eigener Benutzer, tini als PID 1, Healthcheck
- .env.example mit einem Kommentar zu jeder Variable

Mandantentrennung:
- Row Level Security auf allen mandantenpflichtigen Tabellen
- Zwei getrennte Datenbankverbindungen: florian_app mit aktiver RLS fuer
  die Anwendung, Eigentuemer-Zugang nur fuer Migration, Seed und Anmeldung
- fuerOrganisation() als einziger Weg zu Fachdaten

Nachvollziehbarkeit:
- AuditLog append-only per Trigger; UPDATE ist dauerhaft gesperrt, DELETE
  nur im gekennzeichneten Aufbewahrungslauf
- Geheimnisse werden vor dem Schreiben entfernt

Anmeldung:
- OIDC gegen ein extern betriebenes authentik, Rollen aus Gruppen nur als
  Vorbelegung beim ersten Login
- Lokaler E-Mail/Passwort-Login als Rueckfallweg
- Terminal per PIN und RFID mit zwei Schranken (Geraet, dann Person)

Weiteres:
- lib/authz.ts mit 9 Rollen und 38 Einzelrechten
- lib/storage.ts (MinIO), lib/audit.ts, lib/warteschlange.ts (BullMQ)
- Integrationen fuer Mail, Telegram und LLM hinter abschaltbaren Interfaces
- tRPC v11, Basis-Layout mit Seitenleiste und dunkler Darstellung
- Seed mit einer Beispielwehr: 16 Personen, 3 Fahrzeuge, 40 Geraete
- 32 Vitest-Tests (davon 8 gegen eine echte Datenbank), 10 Playwright-Tests
- README, Anleitung mit Screenshots, Roadmap, Aufwandsschaetzung, Devlog,
  Versionierungsregeln und Changelog

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 00:13:53 +02:00