# 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 | offen | | Phase 2 — Wartung und Prüfung | offen | | 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 | 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: ``` ///- ``` Downloads laufen über presignte Links mit begrenzter Gültigkeit (`S3_URL_TTL`, Vorgabe 15 Minuten). --- ## 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.