Die Rolle KASSIER war seit Phase 0 angelegt, ein Kassenmodul fehlte in der Planung. Phase-9.md beschreibt es jetzt: Konten, Buchungen mit Beleg, Beitragslauf mit Mahnstufen, Rechnungen, Kassenabschluss und Auswertungen. Geschaetzt 8-11 PT, Version 0.10.0. Zwei Festlegungen aus der Spezifikation, weil sie den Rest bestimmen: eine festgeschriebene Buchung ist unveraenderbar und wird nur ueber eine Gegenbuchung korrigiert; der Kassenabschluss friert seinen Zeitraum ein. Ausserdem als entschieden festgehalten: - Berechtigung bleibt ohne organisationId. Die Tabelle ist der systemweite Katalog dessen, was die Software kann; mandantenbezogen wird es erst ueber RolleBerechtigung. - Die Loeschausnahme im Audit-Log bleibt. UPDATE ist dauerhaft gesperrt, DELETE nur im gekennzeichneten Aufbewahrungslauf des Workers. Roadmap, Aufwandsschaetzung, Versionierung, README und die Seitenleiste sind entsprechend ergaenzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
218 lines
8.0 KiB
Markdown
218 lines
8.0 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 | 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 |
|
|
| 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).
|
|
|
|
---
|
|
|
|
## 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.
|