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:
Florian Wilde
2026-09-03 00:13:53 +02:00
co-authored by Claude Opus 5
commit 845942f36c
84 changed files with 21433 additions and 0 deletions
+258
View File
@@ -0,0 +1,258 @@
# Anleitung
Bedienung von FLORIAN, Stand Phase 0 (Version 0.1.0). Die Bilder zeigen die
Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt.
> Was hier noch fehlt — Mitglieder, Geräte, Prüfungen — kommt in den
> folgenden Phasen dazu. Der Stand ist bewusst schmal: Phase 0 baut das
> Fundament, nicht die Fachmodule. Siehe [ROADMAP.md](ROADMAP.md).
---
## 1. Anmelden
Aufgerufen wird FLORIAN im Browser unter der Adresse, die der Administrator
eingerichtet hat — in der Entwicklung `http://localhost:3100`.
![Anmeldeseite](screenshots/01-anmeldung-light.png)
Die Anmeldeseite zeigt genau die Wege, die in dieser Installation eingerichtet
sind:
| Weg | Wann |
|---|---|
| **Anmeldung über authentik** | Der Regelweg, sobald authentik eingerichtet ist. Ein Klick, keine Passworteingabe in FLORIAN. |
| **E-Mail und Passwort** | Beim Erststart, und wenn authentik gerade nicht erreichbar ist. |
| **Terminal im Gerätehaus** | Am Wandtablet, mit PIN oder Ausweiskarte. Eigene Oberfläche, kommt in Phase 4. |
Beim allerersten Start gibt es genau ein Konto: das aus `SEED_ADMIN_EMAIL` und
`SEED_ADMIN_PASSWORT` in der `.env`. Vorgabe:
```
admin@beispielwehr.de
Feuerwehr2026
```
**Dieses Passwort gehört nach dem ersten Login geändert.**
### Wenn die Anmeldung nicht klappt
Nach mehreren Fehlversuchen wird das Konto vorübergehend gesperrt — voreingestellt
nach fünf Versuchen für fünf Minuten (`KIOSK_MAX_FEHLVERSUCHE`,
`KIOSK_SPERRDAUER`). Die Meldung nennt nicht, ob die E-Mail-Adresse oder das
Passwort falsch war; das ist Absicht und verhindert, dass sich vorhandene
Konten erraten lassen.
---
## 2. Die Übersicht
Nach der Anmeldung steht die Übersicht der eigenen Wehr.
![Übersicht](screenshots/02-uebersicht-light.png)
Von oben nach unten:
**Kopfzeile.** Name der Wehr und darunter, wer angemeldet ist und mit welchen
Rollen. Wer mehrere Rollen trägt — der Gerätewart ist oft auch Ausbilder —
sieht alle.
**Sicherheitshinweis.** Der gelbe Kasten bleibt stehen, solange die Wehr die
Anwendung nicht ausdrücklich freigegeben hat. Er ist keine Formalität:
Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant, und bis zur
Freigabe bleibt die analoge Überwachung führend.
**Kennzahlen.** Vier Kacheln mit aktiven Mitgliedern, Fahrzeugen, Geräten und
fälligen Prüfungen. Sie zählen nur die eigene Wehr — was eine andere Wehr auf
derselben Installation hat, ist nicht sichtbar und nicht abfragbar.
**Stand des Aufbaus.** Welche Phasen fertig sind und was als Nächstes kommt.
### Die Seitenleiste
Links stehen alle Module. Was noch nicht existiert, ist ausgegraut und trägt
rechts sein Phasenkürzel — `P2` heißt „kommt in Phase 2".
Was jemand überhaupt sieht, hängt an seinen Rechten. Ein einfaches Mitglied
sieht die Mitgliederliste, aber nicht das Protokoll. Wichtig zum Verständnis:
das Ausblenden ist nur Bequemlichkeit. Der eigentliche Schutz sitzt auf dem
Server — wer eine Adresse direkt aufruft, für die ihm das Recht fehlt, bekommt
trotzdem nichts.
---
## 3. Helle und dunkle Darstellung
Oben rechts wird zwischen hell und dunkel umgeschaltet. Voreingestellt ist die
Einstellung des Betriebssystems.
![Übersicht in dunkler Darstellung](screenshots/02-uebersicht-dark.png)
Die dunkle Darstellung ist bewusst kein invertiertes Schwarz, sondern ein
warmes Grau. Wer abends vom Fahrzeug hereinkommt und ans Terminal tritt, soll
nicht geblendet werden.
---
## 4. Rollen und was sie dürfen
Neun Rollen, jede mit einem festen Satz Rechte. Die Zuordnung steht zentral in
`src/lib/authz.ts` und wird beim Seed in die Datenbank gespiegelt — eine Wehr
kann sie also nachträglich zuschneiden, ohne dass jemand Code ändert.
| Rolle | Darf im Kern |
|---|---|
| **Administrator** | alles, auch über Wehren hinweg |
| **Kommandant** | alles innerhalb der eigenen Wehr, einschließlich Rollen vergeben und Protokoll einsehen |
| **Gerätewart** | Fahrzeuge und Geräte anlegen, ändern, aussondern; Prüfungen planen und durchführen |
| **Atemschutzgerätewart** | zusätzlich Atemschutzgeräte, Träger und die Überwachung nach FwDV 7 |
| **Kassier** | Buchungen und Kassenabschluss (Modul noch nicht geplant, siehe Roadmap) |
| **Schriftführer** | Mitglieder pflegen, Einsätze und Berichte erfassen |
| **Ausbilder** | Übungen planen, Teilnahme und Qualifikationen erfassen |
| **Mitglied** | eigene Daten, Listen lesen |
| **Terminal** | nur anmelden und erfassen — nie verwalten |
Zwei Dinge sind bewusst so gebaut:
**Der Gerätewart kommt nicht an die Kasse, der Kassier nicht an die Geräte.**
Trennung nach Verantwortung, nicht nach Bequemlichkeit.
**Das Terminal darf fast nichts.** Wer am Wandtablet arbeitet, meldet sich
zusätzlich mit PIN oder Karte an und handelt dann unter seiner eigenen Person.
Das Terminal selbst ist nur die Tür.
---
## 5. Anmeldung über authentik einrichten
authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks.
### In authentik
1. Neuen Provider anlegen, Typ **OAuth2/OpenID Provider**
2. Client-Typ: **Confidential**
3. Redirect-URI genau so eintragen:
```
https://<ihre-florian-adresse>/api/auth/callback/authentik
```
4. Scopes: `openid`, `profile`, `email`
5. Für die Rollenvorbelegung zusätzlich ein Scope-Mapping, das die Gruppen des
Benutzers als Claim `groups` ausliefert
6. Eine Application anlegen und mit dem Provider verbinden. Der Slug der
Application ist der letzte Teil der Issuer-URL.
### In der `.env`
```bash
AUTH_OIDC_ENABLED=true
AUTH_OIDC_ISSUER=https://auth.beispiel.de/application/o/florian/
AUTH_OIDC_CLIENT_ID=<aus authentik>
AUTH_OIDC_CLIENT_SECRET=<aus authentik>
AUTH_OIDC_GROUP_CLAIM=groups
AUTH_OIDC_GROUP_MAPPING=feuerwehr-kommandant:KOMMANDANT,feuerwehr-geraetewart:GERAETEWART
AUTH_OIDC_AUTO_PROVISION=true
AUTH_OIDC_STANDARD_ORGANISATION=ff-musterhausen
```
Danach die Anwendung neu starten. Auf der Anmeldeseite erscheint der Knopf für
authentik.
### Wie die Rollenvorbelegung wirkt
Die Zuordnung greift **nur beim allerersten Login** einer Person. Danach ist
die in FLORIAN gepflegte Rolle führend, und authentik überschreibt sie nicht
mehr. Wer also einem Mitglied in FLORIAN die Rolle „Gerätewart" gibt, behält
sie — auch wenn die authentik-Gruppe etwas anderes sagt.
Steht `AUTH_OIDC_AUTO_PROVISION=false`, muss die Person vorher in FLORIAN
angelegt sein. Verknüpft wird über die E-Mail-Adresse.
### Wenn authentik ausfällt
Solange `AUTH_LOCAL_ENABLED=true` steht, bleibt der Passwort-Login als
Rückfallweg erreichbar. Für den Wirkbetrieb ist es sinnvoll, mindestens ein
Konto mit Passwort zu behalten — sonst sperrt ein Ausfall von authentik die
ganze Wehr aus.
---
## 6. Terminal-Anmeldung im Gerätehaus
Die Oberfläche dazu kommt in Phase 4; der Anmeldeweg steht bereits.
Am Terminal wird zweifach geprüft:
1. **Das Gerät** weist sich mit `KIOSK_DEVICE_SECRET` aus. Ohne diesen Wert
nimmt der Server keine PIN und keine Karte an.
2. **Die Person** meldet sich mit einer sechsstelligen PIN (zusammen mit ihrer
Mitgliedsnummer) oder mit einer RFID-Karte an.
Beides wird nur als Hash gespeichert — weder die PIN noch die Kartenkennung
liegen im Klartext in der Datenbank.
PINs, die sich zu leicht raten lassen, weist FLORIAN ab: lauter gleiche
Ziffern und fortlaufende Folgen wie `123456`.
Aus dem Seed lässt sich das ausprobieren: Mitgliedsnummer `0001` (Andreas
Huber), PIN `471203`.
---
## 7. Was protokolliert wird
Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person,
betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich.
Auch jede Anmeldung — erfolgreich wie gescheitert.
Zwei Eigenschaften sind wichtig:
**Der Log lässt sich nicht ändern.** Nicht über die Oberfläche, nicht über die
Datenbank, auch nicht vom Administrator. Die Datenbank selbst verhindert es.
**Er lässt sich nur im Aufbewahrungslauf kürzen.** Nach der eingestellten
Frist (`AUDIT_AUFBEWAHRUNG_TAGE`, Vorgabe 10 Jahre) sichert der
Hintergrundprozess die alten Einträge und entfernt sie danach. Das ist der
einzige Weg, auf dem etwas aus dem Log verschwindet.
**Passwörter, PINs und Kartenkennungen stehen nie im Log** — sie werden vor
dem Schreiben herausgenommen.
Einsehbar ist der Log mit dem Recht `system.auditlog`, das Kommandant und
Administrator haben. Die Ansicht dazu kommt in Phase 1.
---
## 8. Wenn etwas nicht geht
| Beobachtung | Wahrscheinliche Ursache | Was hilft |
|---|---|---|
| Die Anwendung startet nicht und meldet fehlende Variablen | `.env` unvollständig | Die Meldung nennt jede fehlende Variable. Erklärungen dazu stehen in `.env.example`. |
| Anmeldung schlägt immer fehl, obwohl das Passwort stimmt | Konto nach Fehlversuchen gesperrt | Fünf Minuten warten, oder `gesperrtBis` in der Datenbank zurücksetzen |
| Listen sind leer, obwohl Daten da sind | Kein Mandantenkontext gesetzt | Das ist der Schutzmechanismus. Der Zugriff muss über `fuerOrganisation()` laufen. |
| Dateien lassen sich nicht hochladen | MinIO nicht erreichbar oder Bucket fehlt | `http://localhost:9011` öffnen; `docker compose logs minio-init` |
| Der Knopf für authentik fehlt | Issuer, Client-ID oder Secret nicht gesetzt | Alle drei sind nötig, sonst wird der Weg ausgeblendet |
| `docker compose up` startet Traefik mit | Override-Datei wurde nicht mitgeladen | In Produktion `-f docker-compose.yml` angeben |
Zustand aller Randsysteme auf einen Blick:
```
http://localhost:3100/api/health
```
Antwortet mit `200` und `"status": "ok"`, wenn Datenbank, Objektspeicher und
Warteschlange erreichbar sind — sonst mit `503` und der Angabe, welcher Dienst
fehlt.
---
## Bilder erneuern
```bash
docker compose up -d
npm run dev
npm run screenshots
```
Die Bilder entstehen in `docs/screenshots/`, jeweils in heller und dunkler
Darstellung, und werden mit eingecheckt — die Anleitung soll auch dann lesbar
sein, wenn gerade kein Stack läuft.