Files
FireAdministration/docs/DEVLOG.md
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

6.3 KiB
Raw Permalink Blame History

Devlog

Entwicklungstagebuch mit Zeiterfassung. Jeder Arbeitsabschnitt bekommt einen Eintrag: wann, wie lange, was entstanden ist, und was dabei schiefging.

Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte „tatsächlich" der Aufwandsschätzung.

Zeitübersicht

Phase Sitzungszeit Sitzungen abgeschlossen
Phase 0 — Fundament 1:00 h 1 03.09.2026
Summe 1:00 h 1

2026-09-02 / 03 · Phase 0 — Fundament

23:13 – 00:13 · 1:00 h · Version 0.1.0

Was entstanden ist

46 Quelldateien, rund 6.100 Zeilen. Im Einzelnen:

Bereich Inhalt
Betrieb docker-compose.yml (7 Dienste), Entwicklungs-Override, mehrstufiges Dockerfile, Postgres-Initialisierung, .env.example mit Kommentar zu jeder Variable
Datenmodell Prisma-Schema mit 11 Tabellen, 3 Migrationen
Kern env, db, authz, storage, audit, auth, passwort, warteschlange, 3 Integrationsschnittstellen
Server tRPC v11 mit drei Prozedurarten, Kontext, Wurzel-Router
Oberfläche Layout, Seitenleiste, Anmeldeseite, Übersicht, 5 shadcn-Bausteine, Farbwelt in OKLCH
Daten Seed: 1 Wehr, 9 Rollen, 38 Rechte, 16 Personen, 3 Fahrzeuge, 40 Geräte
Tests 32 Vitest-Tests, 10 Playwright-Tests
Doku README, Anleitung, Roadmap, Aufwandsschätzung, Versionierung, Changelog

Entscheidungen

Zwei Datenbankverbindungen statt einer. Der erste Entwurf hatte einen einzigen Prisma-Client. Der ist beim ersten Anmeldeversuch aufgelaufen: die Anwendung verbindet sich als florian_app, für den Row Level Security greift — und die Anmeldung muss eine E-Mail-Adresse über alle Wehren hinweg suchen, bevor überhaupt feststeht, zu welcher Wehr sie gehört. Ohne gesetzten Mandantenkontext liefert RLS null Zeilen, also fand der Login nie jemanden.

Die Lösung ist eine saubere Trennung: prismaApp für alle Fachdaten (RLS aktiv), prismaSystem für die wenigen Vorgänge, die vor der Mandantenauflösung stattfinden. Der Name ist bewusst unbequem, damit auffällt, wenn ihn jemand im falschen Zusammenhang benutzt.

Kein FORCE ROW LEVEL SECURITY. Der Tabelleneigentümer umgeht RLS. Das ist gewollt: Migrationen und Seed müssen über alle Mandanten hinweg arbeiten können. Mit FORCE wäre der Seed nicht mehr lauffähig gewesen.

Audit-Log: append-only mit genau einer Ausnahme. Der erste Trigger hat UPDATE und DELETE ausnahmslos gesperrt. Das ist beim Aufräumen der Testdaten aufgefallen — eine Organisation ließ sich nicht mehr löschen, weil die Kaskade auf den Audit-Log traf.

Das ist kein Testproblem, sondern ein echtes: nach AUDIT_AUFBEWAHRUNG_TAGE muss der Worker alte Einträge exportieren und dann entfernen, sonst wächst die Tabelle unbegrenzt und das Löschkonzept nach DSGVO lässt sich nicht umsetzen. Die zweite Migration führt deshalb einen ausdrücklichen Schalter ein: SET LOCAL app.audit_bereinigung = 'an' erlaubt DELETE für die laufende Transaktion. UPDATE bleibt unter allen Umständen gesperrt — ein Eintrag darf verschwinden, nachdem er gesichert wurde, aber niemals verfälscht werden.

Rollen aus authentik nur als Vorbelegung. Auf Nachfrage entschieden: der Gruppen-Claim setzt die Rolle beim ersten Login, danach ist die in FLORIAN gepflegte Rolle führend. Umgesetzt über das Feld ausOidcGruppe an PersonRolle und eine Prüfung, die nur bei leerer Rollenliste greift.

Terminal mit zwei Schranken. Erst weist sich das Gerät mit KIOSK_DEVICE_SECRET aus, dann die Person mit PIN oder Karte. Eine erratene sechsstellige PIN nützt ohne das Gerätegeheimnis nichts.

Wo es gehakt hat

Problem Ursache Lösung
Postgres startete nicht de_DE.UTF-8 gibt es im Debian-Basisimage nicht ICU-Locale: --locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8
Init-Skript brach mit Syntaxfehler ab psql ersetzt seine Variablen nicht innerhalb dollar-quotierter Blöcke DO $$-Block durch SELECT format(...) \gexec ersetzt
Anmeldung schlug immer fehl siehe oben: RLS blockierte die E-Mail-Suche zweite Datenbankverbindung
npm install ließ Installationsskripte aus npm 11.17 verlangt eine ausdrückliche Freigabe npm approve-scripts für Prisma, esbuild und drei weitere
Aufräumen der Testdaten scheiterte Audit-Trigger blockierte die Kaskade Aufbewahrungsschalter (siehe oben)
Zwei E2E-Tests rot Next.js hängt einen eigenen Routen-Ansager mit role="alert" in die Seite; CardTitle ist ein div, keine Überschrift Fehlermeldung über data-testid angesprochen, Anmeldetitel zu einer echten h1 gemacht

Abweichung von der Vorgabe

Berechtigung trägt kein organisationId. Die Architekturregel verlangt es für jede Tabelle. Diese Tabelle ist aber kein Mandantendatum, sondern der systemweite Katalog dessen, was die Software überhaupt kann — für alle Wehren identisch. Mandantenbezogen wird es erst über RolleBerechtigung, und die trägt organisationId. Die Tabelle ist für alle lesbar und für die Anwendungsrolle nicht beschreibbar. Die Ausnahme steht im Schema und in der Migration kommentiert.

Phasenzuordnung in der Seitenleiste korrigiert. Beim Schreiben der Roadmap ist aufgefallen, dass ich die Module den falschen Phasen zugeordnet hatte (Atemschutz als Phase 3, Einsätze als Phase 4). Nach Phase-3.md bis Phase-8.md gilt: 3 Einsätze, 4 Kiosk, 5 Atemschutz, 6 Karten, 7 Benachrichtigungen, 8 KI. Seitenleiste, Kommentare in authz.ts und die Übersicht sind entsprechend berichtigt.

Kassenmodul kommt in keiner Phase vor. Die Rolle KASSIER ist gefordert und angelegt, ein Modul dazu gibt es in Phase-0.md bis Phase-8.md nicht. Die Rechte stehen im Katalog, es gibt aber keinen Navigationseintrag. Offene Entscheidung, siehe ROADMAP.md.

Nachweise

  • npm run typecheck — ohne Ausgabe
  • npm run test — 32 Tests grün, davon 8 gegen eine echte Datenbank
  • npm run test:e2e — 10 Tests grün (Desktop und Tablet)
  • npm run build — 6 Routen, 103 kB gemeinsames JavaScript
  • docker compose up -d — Postgres, Redis, MinIO, Gotenberg gesund

Vorlage für neue Einträge

## JJJJ-MM-TT · Phase N — Titel

**HH:MM – HH:MM · X:XX h · Version 0.N.0**

### Was entstanden ist
### Entscheidungen
### Wo es gehakt hat
### Abweichung von der Vorgabe
### Nachweise