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>
345 lines
14 KiB
Markdown
345 lines
14 KiB
Markdown
# FLORIAN — Feuerwehr-Verwaltungsplattform
|
|
|
|
Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren. Läuft im
|
|
Gerätehaus, nicht in der Cloud. Keine Telemetrie, keine externen CDNs.
|
|
|
|
## Schnellstart
|
|
|
|
```bash
|
|
git clone https://git.wilde-server.de/florian.wilde/FireAdministration.git && cd FireAdministration
|
|
cp .env.example .env # Passwörter und AUTH_SECRET ersetzen
|
|
npm ci # Abhängigkeiten
|
|
docker compose up -d # Postgres, Redis, MinIO, Gotenberg
|
|
npm run db:migrate && npm run db:seed # Schema und Beispielwehr
|
|
npm run dev # http://localhost:3100
|
|
```
|
|
|
|
Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
|
(Vorgabe: `admin@beispielwehr.de` / `Feuerwehr2026`). Passwort danach ändern.
|
|
|
|
---
|
|
|
|
## Was drin ist
|
|
|
|
| Bereich | Stand |
|
|
|---|---|
|
|
| Phase 0 — Fundament | fertig |
|
|
| Phase 1 — Stammdaten | fertig |
|
|
| Phase 2 — Wartung und Prüfung | fertig |
|
|
| Phase 3 — Einsätze und Übungen | offen |
|
|
| Phase 4 — Kiosk-PWA | offen |
|
|
| Phase 5 — Atemschutz (FwDV 7) | offen |
|
|
| Phase 6 — Kartenmodule | offen |
|
|
| Phase 7 — Benachrichtigungen und Termine | offen |
|
|
| Phase 8 — Lokale KI | offen |
|
|
| Phase 9 — Kassenverwaltung | offen |
|
|
|
|
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
|
|
[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md).
|
|
|
|
## Weiterführende Dokumentation
|
|
|
|
| Datei | Inhalt |
|
|
|---|---|
|
|
| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Bedienung mit Bildschirmfotos |
|
|
| [docs/ROADMAP.md](docs/ROADMAP.md) | Phasenplan und was jeweils entsteht |
|
|
| [docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md) | Aufwand je Phase, Schätzung gegen Ist |
|
|
| [docs/DEVLOG.md](docs/DEVLOG.md) | Entwicklungstagebuch mit Zeiterfassung |
|
|
| [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md) | Versions- und Branch-Regeln |
|
|
| [CHANGELOG.md](CHANGELOG.md) | Was sich wann geändert hat |
|
|
|
|
---
|
|
|
|
## Phase 0 — Fundament
|
|
|
|
### Aufbau
|
|
|
|
```
|
|
FireAdministration/
|
|
├─ docker/ Dockerfile, Entrypoint, Postgres-Initialisierung
|
|
├─ prisma/ Schema, Migrationen, Seed
|
|
├─ scripts/ Hilfsskripte (Bildschirmfotos)
|
|
├─ src/
|
|
│ ├─ app/ Next.js App Router
|
|
│ │ ├─ (app)/ alles, was Anmeldung verlangt
|
|
│ │ ├─ anmelden/ Anmeldeseite
|
|
│ │ └─ api/ Auth.js, tRPC, Health
|
|
│ ├─ components/ shadcn/ui-Bausteine und Layout
|
|
│ ├─ lib/ authz, db, storage, audit, env, auth, warteschlange
|
|
│ ├─ server/ tRPC-Router und Kontext
|
|
│ └─ worker/ BullMQ-Hintergrundprozess
|
|
├─ tests/ Vitest (unit) und Playwright (e2e)
|
|
└─ docs/ Anleitung, Roadmap, Devlog, Aufwände
|
|
```
|
|
|
|
### Mandantentrennung
|
|
|
|
Jede Tabelle trägt `organisationId`, und Postgres setzt die Trennung mit Row
|
|
Level Security durch. Es gibt zwei Datenbankverbindungen:
|
|
|
|
- **`florian_app`** (`DATABASE_URL`) — RLS aktiv, `NOBYPASSRLS`. Über diese
|
|
Verbindung arbeitet die Anwendung. Ohne gesetztes
|
|
`app.current_organisation` liefert jede Abfrage **null Zeilen**.
|
|
- **Eigentümer** (`DIRECT_DATABASE_URL`) — umgeht RLS. Nur für Migrationen,
|
|
Seed, Anmeldung (die E-Mail-Suche muss über alle Wehren gehen) und
|
|
Wartungsjobs.
|
|
|
|
Fachdaten laufen ausschließlich über `fuerOrganisation()` aus
|
|
[`src/lib/db.ts`](src/lib/db.ts):
|
|
|
|
```ts
|
|
const personen = await fuerOrganisation(orgId, (db) =>
|
|
db.person.findMany({ where: { aktiv: true } }),
|
|
);
|
|
```
|
|
|
|
Ein vergessener Mandantenfilter kostet damit im schlimmsten Fall ein leeres
|
|
Ergebnis — nicht die Daten einer fremden Wehr.
|
|
`tests/unit/mandantentrennung.test.ts` prüft das gegen eine echte Datenbank.
|
|
|
|
### Audit-Log
|
|
|
|
Jede schreibende Operation erzeugt einen Eintrag mit Akteur, Zeitpunkt,
|
|
Entität und Vorher/Nachher als JSONB. Die Tabelle ist append-only:
|
|
|
|
- `UPDATE` ist per Trigger **immer** gesperrt.
|
|
- `DELETE` nur im gekennzeichneten Aufbewahrungslauf
|
|
(`SET LOCAL app.audit_bereinigung = 'an'`), den ausschließlich der Worker
|
|
ausführt — nachdem er exportiert hat. Ohne diese Ausnahme ließe sich das
|
|
Löschkonzept nach DSGVO nicht umsetzen.
|
|
|
|
Geheimnisse (Passwort-Hashes, PINs, Token) werden vor dem Schreiben entfernt.
|
|
|
|
### Berechtigungen
|
|
|
|
Alles in [`src/lib/authz.ts`](src/lib/authz.ts) — keine Rollenprüfung in
|
|
Komponenten. 9 Rollen, 38 Einzelrechte. Der Seed spiegelt den Katalog in die
|
|
Datenbank, damit eine Wehr Rechte nachträglich zuschneiden kann, ohne dass
|
|
jemand Code ändert.
|
|
|
|
```ts
|
|
export const geraetSpeichern = mitRecht("geraet.bearbeiten").mutation(/* … */);
|
|
```
|
|
|
|
### Anmeldung
|
|
|
|
Drei Wege, eine Sitzung:
|
|
|
|
1. **OIDC gegen authentik** — der Regelweg. authentik wird **separat**
|
|
betrieben und ist nicht Teil dieses Stacks. Im Provider einzutragen:
|
|
Redirect-URI `${APP_URL}/api/auth/callback/authentik`, Scopes
|
|
`openid profile email`.
|
|
2. **E-Mail und Passwort** — Rückfalloption für den Erststart und für den Fall,
|
|
dass authentik nicht erreichbar ist. Über `AUTH_LOCAL_ENABLED` abschaltbar.
|
|
3. **Terminal (PIN oder RFID)** — für das Kiosk-Gerät im Gerätehaus. Zwei
|
|
Schranken: erst weist sich das *Gerät* mit `KIOSK_DEVICE_SECRET` aus, dann
|
|
die *Person* mit PIN oder Karte. Die vollständige Oberfläche dazu kommt in
|
|
Phase 4.
|
|
|
|
Rollen aus authentik-Gruppen wirken als **Vorbelegung beim ersten Login**
|
|
(`AUTH_OIDC_GROUP_MAPPING`). Sobald jemand die Rolle in FLORIAN ändert, ist
|
|
FLORIAN führend und OIDC überschreibt nicht mehr.
|
|
|
|
### Dateien
|
|
|
|
Alles liegt in MinIO, nie im Dateisystem und nie in der Datenbank. Zugriff
|
|
ausschließlich über [`src/lib/storage.ts`](src/lib/storage.ts). Objektschlüssel:
|
|
|
|
```
|
|
<wehr-slug>/<entitaet>/<entitaetId>/<uuid>-<dateiname>
|
|
```
|
|
|
|
Downloads laufen über presignte Links mit begrenzter Gültigkeit
|
|
(`S3_URL_TTL`, Vorgabe 15 Minuten).
|
|
|
|
---
|
|
|
|
## Phase 2 — Wartung und Prüfung
|
|
|
|
Das Modul, wegen dem eine Wehr so ein Programm überhaupt einführt: Prüffristen
|
|
im Blick behalten und im Streitfall nachweisen können, dass geprüft wurde.
|
|
|
|
### Der Weg durch das Modul
|
|
|
|
```
|
|
Wartungsvorlage was an einem Gerätetyp in welchem Takt geprüft wird
|
|
└─ Checklistenpunkt die einzelne Frage: ja/nein, Messwert, Text, Foto
|
|
↓ nächtlicher Lauf
|
|
Wartungstermin ein fälliger Termin an einem konkreten Gerät
|
|
↓ Prüfwizard
|
|
Pruefung durchgeführte Prüfung, unterschrieben, festgeschrieben
|
|
├─ Pruefergebnis je Checklistenpunkt eine Antwort
|
|
├─ Signatur Unterschrift aus dem Zeichenfeld, PNG in MinIO
|
|
└─ Mangel was aufgefallen ist und behoben werden muss
|
|
```
|
|
|
|
Ohne Vorlage gibt es keine Termine, ohne Termine kein Fristen-Dashboard. Die
|
|
erste Seite, die eine Wehr in Phase 2 ausfüllt, ist deshalb
|
|
`/pruefungen/vorlagen`.
|
|
|
|
### Das Protokoll ist unveränderbar
|
|
|
|
Ein abgeschlossenes Prüfprotokoll ist der Nachweis gegenüber der Unfallkasse.
|
|
Wäre es nachträglich änderbar, wäre es als Nachweis wertlos. Die Sperre sitzt
|
|
deshalb in der Datenbank (Trigger `pruefung_unveraenderlich`, Migration
|
|
`20260903000300_wartung_rls`) und nicht nur im Anwendungscode — ein vergessener
|
|
Zustandscheck in einem Router würde sonst reichen, um sie auszuhebeln.
|
|
|
|
Nach dem Abschluss sind genau drei Änderungen zulässig, und alle drei ergänzen
|
|
das Protokoll, statt es zu verfälschen:
|
|
|
|
| Änderung | Warum |
|
|
|---|---|
|
|
| `ABGESCHLOSSEN` → `ERSETZT` | eine Korrekturprüfung ist nachgefolgt |
|
|
| `protokollDateiId`, solange leer | das PDF entsteht im Hintergrund und wird nachgetragen |
|
|
| `geaendertAm` | setzt Prisma bei jedem Schreibvorgang mit |
|
|
|
|
Dasselbe gilt für die Bestandteile: an einem abgeschlossenen Protokoll lassen
|
|
sich weder Einzelergebnisse ändern noch Unterschriften nachtragen. **Korrekturen
|
|
erzeugen eine neue Prüfung** mit Verweis auf die alte; beide bleiben lesbar.
|
|
|
|
Gelöscht wird ein Protokoll nie — außer im ausdrücklich gekennzeichneten
|
|
Aufbewahrungslauf, demselben Schalter wie beim Audit-Log
|
|
(`SET LOCAL app.audit_bereinigung = 'an'`).
|
|
|
|
### Der nächtliche Lauf
|
|
|
|
Der Worker trägt sich beim Start je Wehr zwei wiederkehrende Aufträge ein:
|
|
|
|
| Auftrag | Vorgabe | Was er tut |
|
|
|---|---|---|
|
|
| `fristen.pruefen` | 03:15 Uhr | Termine aus den Vorlagen planen, Frist und Status an jedem Gerät nachziehen |
|
|
| `erinnerung.senden` | 03:45 Uhr | Vorwarnungen zu offenen Terminen verschicken |
|
|
|
|
Beide sind wiederholbar: doppelte Termine verhindert die Eindeutigkeit über
|
|
Gerät, Vorlage und Fälligkeitstag, doppelte Mails ein Erinnerungseintrag je
|
|
Termin und Stufe. Erinnert wird bei T-30 (bzw. der Vorwarnzeit der Vorlage),
|
|
T-7, T-0 und einmal bei Überfälligkeit — als **eine** Sammelnachricht je
|
|
Empfänger, nicht als eine Mail je Gerät.
|
|
|
|
„Überfällig" ist bewusst kein gespeicherter Zustand, sondern wird aus dem
|
|
Fälligkeitsdatum und dem heutigen Tag gerechnet. Gespeicherte Zustände laufen
|
|
der Uhr hinterher; berechnete nicht.
|
|
|
|
### Prüfwizard
|
|
|
|
Zugeschnitten auf das Tablet im Gerätehaus: ein Prüfpunkt je Bildschirm, große
|
|
Schaltflächen, Foto direkt aus der Kamera, Unterschrift auf einem Zeichenfeld,
|
|
das Finger, Stift und Maus gleichermaßen versteht.
|
|
|
|
Jede Antwort geht sofort zum Server. Bricht das WLAN weg, ist höchstens die
|
|
letzte Antwort verloren, nicht die ganze Prüfung — beim erneuten Öffnen wird der
|
|
Entwurf fortgesetzt.
|
|
|
|
Die Bewertung rechnet der Server, nicht der Browser
|
|
([`src/lib/wartung/bewertung.ts`](src/lib/wartung/bewertung.ts)):
|
|
|
|
- ein Messwert außerhalb des zulässigen Bereichs gilt als beanstandet,
|
|
- ein beanstandeter **Pflichtpunkt** bedeutet „nicht bestanden" und sperrt das
|
|
Gerät,
|
|
- ein beanstandeter Kann-Punkt bedeutet „bestanden mit Mängeln".
|
|
|
|
Das Ergebnis ist ein Vorschlag; die Entscheidung trifft der Prüfer, denn er
|
|
trägt die Verantwortung.
|
|
|
|
### Prüfprotokoll als PDF
|
|
|
|
Erzeugt der Worker über Gotenberg
|
|
([`src/lib/wartung/protokoll.ts`](src/lib/wartung/protokoll.ts)) und legt es in
|
|
MinIO ab. Kopf mit Wehr, Gerät und Prüfer-Kennung, die Checkliste mit Soll, Ist
|
|
und Befund, eingebettete Fotos und Unterschriften, Fußzeile mit Prüfvorschrift,
|
|
Prüfdatum und Seitenzahl.
|
|
|
|
Alles ist eingebettet — keine externen Schriften, keine nachgeladenen Bilder.
|
|
Ein Protokoll muss in zehn Jahren noch genauso aussehen wie heute.
|
|
|
|
Die laufende Protokollnummer (`JJJJ-NNNN`) wird je Wehr und Jahr unter einer
|
|
Transaktionssperre vergeben; zwei gleichzeitig abschließende Prüfer bekommen
|
|
verschiedene Nummern.
|
|
|
|
### Mängel und Rückrufe
|
|
|
|
Der Mängelweg ist immer derselbe: melden → zugewiesen → in Arbeit → behoben.
|
|
**Melden darf jedes Mitglied** — der Mangel fällt dem auf, der das Gerät
|
|
benutzt, nicht dem Gerätewart am Schreibtisch. Ein kritischer Mangel sperrt das
|
|
Gerät sofort; beim Beheben wird die Sperre wieder aufgehoben, aber nur, wenn
|
|
kein anderer offener Mangel dasselbe Gerät sperrt.
|
|
|
|
Bei einem Rückruf sucht FLORIAN anhand von Bauart, Modell, Seriennummernbereich
|
|
und Herstellungszeitraum nach betroffenen Geräten. Das Ergebnis ist ein
|
|
**Vorschlag**, keine automatische Sperre: eine Chargenangabe des Herstellers
|
|
passt selten eins zu eins auf die Seriennummern im eigenen Bestand.
|
|
|
|
### Zusätzliche Umgebungsvariablen
|
|
|
|
| Variable | Vorgabe | Bedeutung |
|
|
|---|---|---|
|
|
| `WARTUNG_NACHTLAUF` | `15 3 * * *` | wann Termine geplant werden (cron, Europe/Berlin) |
|
|
| `WARTUNG_ERINNERUNG_LAUF` | `45 3 * * *` | wann Erinnerungen verschickt werden |
|
|
| `WARTUNG_PLANUNGSHORIZONT_TAGE` | `180` | wie weit im Voraus Termine angelegt werden |
|
|
|
|
|
|
---
|
|
|
|
## Befehle
|
|
|
|
```bash
|
|
npm run dev # Entwicklungsserver auf Port 3100
|
|
npm run build # Produktionsbuild
|
|
npm run worker:dev # Hintergrundprozess mit Neuladen
|
|
npm run test # Vitest
|
|
npm run test:e2e # Playwright
|
|
npm run typecheck # TypeScript ohne Ausgabe
|
|
npm run db:migrate # ausstehende Migrationen anwenden
|
|
npm run db:seed # Beispielwehr einspielen
|
|
npm run db:studio # Prisma Studio
|
|
npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen
|
|
```
|
|
|
|
## Dienste und Ports (Entwicklung)
|
|
|
|
Bewusst nicht die Standardports, damit FLORIAN neben anderen lokalen Stacks
|
|
laufen kann. Alle Werte stehen in der `.env`.
|
|
|
|
| Dienst | Adresse |
|
|
|---|---|
|
|
| Anwendung | http://localhost:3100 |
|
|
| PostgreSQL 17 + PostGIS | `localhost:5442` |
|
|
| Redis | `localhost:6389` |
|
|
| MinIO API | http://localhost:9010 |
|
|
| MinIO Weboberfläche | http://localhost:9011 |
|
|
| Gotenberg | http://localhost:3101 |
|
|
| Mailpit (nur Entwicklung) | http://localhost:8125 |
|
|
| Adminer (nur Entwicklung) | http://localhost:8081 |
|
|
|
|
## Produktionsbetrieb
|
|
|
|
```bash
|
|
docker compose -f docker-compose.yml up -d
|
|
```
|
|
|
|
Die Angabe von `-f` ist wichtig: ohne sie legt Docker Compose automatisch
|
|
`docker-compose.override.yml` darüber, und das ist die Entwicklungsvariante
|
|
ohne Traefik. In Produktion terminiert Traefik TLS und verteilt auf
|
|
`APP_DOMAIN` und `MINIO_DOMAIN`.
|
|
|
|
Der App-Container wendet beim Start ausstehende Migrationen an; der Worker
|
|
tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren.
|
|
|
|
## Datenschutz
|
|
|
|
- Keine Telemetrie, keine externen CDNs, keine Google Fonts. Es werden nur
|
|
Schriften verwendet, die auf dem Gerät vorhanden sind.
|
|
- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`,
|
|
`AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`).
|
|
- Externe Dienste (Mail, Telegram, LLM) sitzen hinter Schnittstellen in
|
|
`src/lib/integrations/` und sind einzeln abschaltbar. Ohne Freischaltung
|
|
wird nur ins Log geschrieben.
|
|
|
|
## Sicherheitshinweis
|
|
|
|
Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Solange die
|
|
Wehr das System nicht ausdrücklich freigegeben hat, bleibt die **analoge
|
|
Überwachung führend**. Jede entsprechende Ansicht zeigt diesen Hinweis
|
|
dauerhaft an.
|