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>
6.3 KiB
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 Ausgabenpm run test— 32 Tests grün, davon 8 gegen eine echte Datenbanknpm run test:e2e— 10 Tests grün (Desktop und Tablet)npm run build— 6 Routen, 103 kB gemeinsames JavaScriptdocker 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