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>
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
# 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](AUFWANDSSCHAETZUNG.md).
|
||||
|
||||
## 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](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
|
||||
|
||||
```markdown
|
||||
## 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
|
||||
```
|
||||
Reference in New Issue
Block a user