Files
FireAdministration/.env.example
T
Florian WildeandClaude Opus 5 4e2daaafd4 Atemschutzueberwachung nach FwDV 7 ergaenzen
Phase 5: das sicherheitsrelevanteste Modul des Projekts. Ueber jeder Ansicht
steht dauerhaft, dass die analoge Atemschutzueberwachung fuehrend bleibt —
abschaltbar nur zusammen mit der Fundstelle einer Freigabe des Kommandanten.

Es gibt keinen Timer. In der Datenbank stehen Zeitstempel; wie lange ein
Trupp unter Atemschutz ist, rechnet jede Ansicht bei jedem Bild neu aus.
Damit ist die Anforderung "uebersteht Neuladen und Verbindungsabbruch" keine
Funktion, die gelingen muss, sondern eine Eigenschaft, die sich nicht
verlieren laesst. Server und Browser rechnen mit denselben Funktionen.

Der Rueckzugsdruck folgt dem doppelten Anmarschverbrauch: was der Weg hinein
gekostet hat, kostet der Weg zurueck noch einmal. Bis der Trupp das
Einsatzziel meldet, greift die Zwei-Drittel-Regel — sie liegt hoeher und ist
damit die vorsichtigere Annahme.

Das Protokoll ist doppelt gesichert: entzogene Rechte und ein Trigger auf
druckmessung und atemschutz_ereignis. Ein Fehler wird durch einen neuen
Eintrag richtiggestellt, nicht durch Aendern.

Verteilt wird ueber Server-Sent Events statt Socket.IO — es geht nur in eine
Richtung, und so bleibt das Deployment bei "next start".

Behoben: ein abgerissener Ereignisstrom meldete sich nicht. Die Anzeige haette
weiter "Verbunden" behauptet, waehrend der Draht tot war. Jetzt gilt Stille
von acht Sekunden als Abriss. Dazu zwei Altlasten: der Navigationseintrag
"Protokoll" fuehrte ins Leere, und die Startseite stand noch auf Phase 1.

68 neue Tests (51 Unit, 17 gegen eine echte Datenbank, 8 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 23:19:05 +02:00

299 lines
15 KiB
Bash

# =============================================================================
# FLORIAN — Feuerwehr-Verwaltungsplattform
# Kopiere diese Datei nach ".env" und passe die Werte an:
# cp .env.example .env
# Alles, was hier steht, wird von docker-compose.yml und von lib/env.ts gelesen.
# lib/env.ts validiert beim Start mit Zod — fehlt eine Pflichtvariable, startet
# die App gar nicht erst, statt später halb zu funktionieren.
# =============================================================================
# -----------------------------------------------------------------------------
# Allgemein
# -----------------------------------------------------------------------------
# development | production | test — steuert Logging, Fehlerseiten und Caching.
NODE_ENV=development
# Öffentliche Basis-URL der Anwendung, ohne abschließenden Schrägstrich.
# Muss exakt der Adresse entsprechen, unter der Nutzer die App aufrufen —
# Auth.js baut daraus die OIDC-Redirect-URI.
APP_URL=http://localhost:3100
# Zeitzone für alle serverseitigen Fristen- und Terminberechnungen.
# Nicht ändern, außer die Wehr liegt außerhalb Deutschlands.
TZ=Europe/Berlin
# Host-Ports fuer die Entwicklung. Bewusst nicht die Standardports,
# damit FLORIAN neben anderen lokalen Stacks laufen kann.
APP_PORT=3100
GOTENBERG_PORT=3101
MAILPIT_UI_PORT=8125
MAILPIT_SMTP_PORT=1125
ADMINER_PORT=8081
# -----------------------------------------------------------------------------
# Traefik / Domains (nur für den Produktionsbetrieb relevant)
# In der Entwicklung greift docker-compose.override.yml und veröffentlicht die
# Dienste direkt auf localhost — Traefik wird dort nicht gebraucht.
# -----------------------------------------------------------------------------
# Hostname der Anwendung hinter Traefik, z. B. florian.beispielwehr.de
APP_DOMAIN=florian.localhost
# Hostname der MinIO-S3-API (Datei-Uploads, presignte URLs).
MINIO_DOMAIN=s3.florian.localhost
# Hostname der MinIO-Weboberfläche (Administration der Buckets).
MINIO_CONSOLE_DOMAIN=minio.florian.localhost
# Hostname des Traefik-Dashboards. Leer lassen, um es nicht zu veröffentlichen.
TRAEFIK_DASHBOARD_DOMAIN=traefik.florian.localhost
# E-Mail-Adresse für die ACME-Zertifikate (Let us Encrypt).
ACME_EMAIL=kommandant@beispielwehr.de
# Traefik-Entrypoint für HTTPS. "websecure" ist der Standard aus dieser Datei.
TRAEFIK_ENTRYPOINT=websecure
# -----------------------------------------------------------------------------
# PostgreSQL 17 + PostGIS
# -----------------------------------------------------------------------------
POSTGRES_USER=florian
# Vor dem ersten Start ersetzen: openssl rand -base64 24
POSTGRES_PASSWORD=bitte_aendern_postgres
POSTGRES_DB=florian
# Port, unter dem Postgres auf dem Host erreichbar ist (nur Entwicklung).
POSTGRES_PORT=5442
# Verbindungszeichenfolge der Anwendung. Läuft über den Datenbankbenutzer
# "florian_app", für den Row Level Security AKTIV ist — dieser Zugang sieht nur
# Daten der Organisation, die per SET LOCAL app.current_organisation gesetzt
# wurde (siehe src/lib/db.ts).
DATABASE_URL=postgresql://florian_app:bitte_aendern_app@postgres:5432/florian?schema=public
# Passwort des Anwendungsbenutzers. Muss zu DATABASE_URL passen und wird beim
# ersten Start von docker/postgres/10-init-app-role.sh gesetzt.
POSTGRES_APP_PASSWORD=bitte_aendern_app
# Verbindung für Migrationen und Seed. Nutzt den Eigentümer-Zugang, der RLS
# umgehen darf. Niemals im Anwendungscode verwenden.
DIRECT_DATABASE_URL=postgresql://florian:bitte_aendern_postgres@postgres:5432/florian?schema=public
# -----------------------------------------------------------------------------
# Redis (Warteschlangen für BullMQ, Rate-Limiting)
# -----------------------------------------------------------------------------
# Vor dem ersten Start ersetzen: openssl rand -base64 24
REDIS_PASSWORD=bitte_aendern_redis
REDIS_URL=redis://:bitte_aendern_redis@redis:6379
# Port auf dem Host (nur Entwicklung).
REDIS_PORT=6389
# Anzahl paralleler Jobs je Worker-Prozess.
WORKER_CONCURRENCY=5
# -----------------------------------------------------------------------------
# MinIO (S3-kompatibler Objektspeicher — ALLE Dateien liegen hier, nie im FS)
# -----------------------------------------------------------------------------
MINIO_ROOT_USER=florian
# Mindestens 8 Zeichen, vor dem ersten Start ersetzen.
MINIO_ROOT_PASSWORD=bitte_aendern_minio
# Endpunkt, den App und Worker intern ansprechen — im Compose-Netz der
# Dienstname, außerhalb der externe Hostname.
S3_ENDPOINT=minio
S3_PORT=9000
# true, sobald MinIO hinter Traefik mit TLS läuft.
S3_USE_SSL=false
S3_ACCESS_KEY=florian
S3_SECRET_KEY=bitte_aendern_minio
S3_REGION=eu-central-1
# Bucket für alle Nutzdateien (Prüfprotokolle, Fotos, Rechnungen, Exporte).
S3_BUCKET=florian
# Öffentlich erreichbare Basis-URL für presignte Download-Links. In der
# Entwicklung http://localhost:9010, in Produktion https://$MINIO_DOMAIN
S3_PUBLIC_URL=http://localhost:9010
# Gültigkeitsdauer presignter Links in Sekunden (Standard: 15 Minuten).
S3_URL_TTL=900
# Ports auf dem Host (nur Entwicklung): API und Weboberfläche.
MINIO_API_PORT=9010
MINIO_CONSOLE_PORT=9011
# -----------------------------------------------------------------------------
# Gotenberg (PDF-Erzeugung für Prüfprotokolle, Berichte, Rechnungen)
# -----------------------------------------------------------------------------
GOTENBERG_URL=http://gotenberg:3000
# Zeitlimit je PDF-Auftrag in Sekunden.
GOTENBERG_TIMEOUT=30
# -----------------------------------------------------------------------------
# Auth.js — Sitzungsverwaltung
# -----------------------------------------------------------------------------
# Signiert die Sitzungs-Cookies. Zwingend erforderlich, pro Installation neu:
# openssl rand -base64 32
AUTH_SECRET=bitte_aendern_mindestens_32_zeichen_lang
# Muss hinter einem Reverse Proxy (Traefik) auf true stehen, damit Auth.js die
# X-Forwarded-Header auswertet und korrekte Redirect-URIs erzeugt.
AUTH_TRUST_HOST=true
# Lebensdauer einer Sitzung in Sekunden (Standard: 8 Stunden).
AUTH_SESSION_MAX_AGE=28800
# -----------------------------------------------------------------------------
# OIDC gegen authentik
# Die authentik-Instanz wird SEPARAT betrieben und ist nicht Teil dieses Stacks.
# In authentik anzulegen: Provider "OAuth2/OpenID" (Client-Typ: confidential),
# Redirect-URI exakt $APP_URL/api/auth/callback/authentik
# Scopes: openid profile email — plus einen Gruppen-Scope, wenn Rollen aus
# authentik-Gruppen vorbelegt werden sollen (siehe AUTH_OIDC_GROUP_CLAIM).
# -----------------------------------------------------------------------------
# false blendet den OIDC-Knopf aus; dann bleibt nur der lokale Login. Sinnvoll
# für den Erststart, bevor authentik konfiguriert ist.
AUTH_OIDC_ENABLED=false
# Issuer-URL des Providers, OHNE /.well-known/openid-configuration.
# Bei authentik: https://<authentik-host>/application/o/<application-slug>/
AUTH_OIDC_ISSUER=https://auth.beispiel.de/application/o/florian/
AUTH_OIDC_CLIENT_ID=
AUTH_OIDC_CLIENT_SECRET=
# Beschriftung des Anmeldeknopfes in der Oberfläche.
AUTH_OIDC_NAME=Anmeldung über authentik
# Claim, aus dem die Gruppenzugehörigkeit gelesen wird. In authentik über ein
# Scope-Mapping bereitzustellen. Leer lassen, um das Mapping abzuschalten —
# dann werden Rollen ausschließlich in FLORIAN gepflegt.
AUTH_OIDC_GROUP_CLAIM=groups
# Zuordnung authentik-Gruppe -> FLORIAN-Rolle, kommagetrennt als "gruppe:ROLLE".
# Wirkt nur als VORBELEGUNG beim ersten Login; danach ist die in FLORIAN
# gepflegte Rolle führend und wird nicht mehr überschrieben.
AUTH_OIDC_GROUP_MAPPING=feuerwehr-kommandant:KOMMANDANT,feuerwehr-geraetewart:GERAETEWART,feuerwehr-atemschutz:ATEMSCHUTZGERAETEWART,feuerwehr-kassier:KASSIER,feuerwehr-schriftfuehrer:SCHRIFTFUEHRER,feuerwehr-ausbilder:AUSBILDER,feuerwehr-mitglied:MITGLIED
# true legt einen in authentik bekannten Nutzer beim ersten Login automatisch
# als Person an. false verlangt, dass die Person vorher in FLORIAN existiert
# und über die E-Mail-Adresse verknüpft wird.
AUTH_OIDC_AUTO_PROVISION=true
# Organisation (Slug), der automatisch angelegte Personen zugeordnet werden.
AUTH_OIDC_STANDARD_ORGANISATION=ff-musterhausen
# -----------------------------------------------------------------------------
# Lokaler E-Mail/Passwort-Login (Rückfalloption für Erststart und Notbetrieb)
# -----------------------------------------------------------------------------
# Nach der Einrichtung von authentik auf false setzen, damit nur noch OIDC geht.
AUTH_LOCAL_ENABLED=true
# Zugangsdaten des ersten Administrators, angelegt vom Seed-Skript.
# Nach dem ersten Login das Passwort in der Oberfläche ändern.
SEED_ADMIN_EMAIL=admin@beispielwehr.de
SEED_ADMIN_PASSWORT=Feuerwehr2026
# bcrypt-Kostenfaktor. 12 ist ein guter Kompromiss aus Sicherheit und Tempo.
AUTH_BCRYPT_ROUNDS=12
# -----------------------------------------------------------------------------
# Terminal-/Kiosk-Anmeldung im Gerätehaus (PIN und RFID)
# -----------------------------------------------------------------------------
# Schaltet den Kiosk-Modus samt Terminal-Login frei.
KIOSK_ENABLED=true
# Gemeinsames Geheimnis, mit dem sich ein Terminal-Gerät gegenüber dem Server
# ausweist, bevor überhaupt eine PIN oder ein Token akzeptiert wird:
# openssl rand -hex 32
KIOSK_DEVICE_SECRET=bitte_aendern_kiosk_geheimnis
# Länge der Personen-PIN. 4 ist bequem, 6 ist sicherer.
KIOSK_PIN_LAENGE=6
# Nach wie vielen Fehlversuchen eine PIN gesperrt wird.
KIOSK_MAX_FEHLVERSUCHE=5
# Sperrdauer nach zu vielen Fehlversuchen, in Sekunden.
KIOSK_SPERRDAUER=300
# Automatische Abmeldung am Terminal nach Untätigkeit, in Sekunden.
KIOSK_IDLE_TIMEOUT=120
# -----------------------------------------------------------------------------
# Atemschutz nach FwDV 7 (Phase 5)
# -----------------------------------------------------------------------------
# Der Sicherheitshinweis ueber jeder Ansicht der Atemschutzueberwachung:
# "Ergaenzendes System — die analoge Atemschutzueberwachung bleibt fuehrend."
# Er gehoert dorthin. Auf false setzen darf ihn nur, wer eine schriftliche
# Freigabe des Kommandanten hat — und deren Fundstelle gehoert in
# ATEMSCHUTZ_FREIGABE, sonst bleibt der Hinweis stehen.
ATEMSCHUTZ_HINWEIS=true
# Aktenzeichen oder Fundstelle der Freigabe, z. B. "Kommandantenbeschluss vom 12.03.2026".
ATEMSCHUTZ_FREIGABE=
# Abstand der Druckabfrage in Sekunden. FwDV 7 nennt keinen festen Wert;
# zehn Minuten sind die uebliche Praxis.
ATEMSCHUTZ_ABFRAGE_INTERVALL=600
# Ansprechdruck der Warneinrichtung in bar. Je nach Baujahr 50 bis 60.
ATEMSCHUTZ_WARNDRUCK=55
# Sicherheitszuschlag auf den errechneten Rueckzugsdruck, in bar.
ATEMSCHUTZ_ZUSCHLAG=10
# -----------------------------------------------------------------------------
# Wetterwarnungen auf dem Hallenmonitor (optional, DWD)
# Die einzige Verbindung nach draussen, die FLORIAN von sich aus aufbaut —
# deshalb standardmaessig aus. Ohne WETTER_ENABLED=true wird nichts abgerufen.
# -----------------------------------------------------------------------------
WETTER_ENABLED=false
# Warnzellen-Nummer der eigenen Gemeinde aus der DWD-Warnzellenliste:
# https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv
DWD_WARNCELL=
DWD_WARNUNGEN_URL=https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json
# Zeitlimit fuer den Abruf, in Sekunden. Der Monitor darf davon nicht haengen.
WETTER_TIMEOUT=5
# Wie lange eine Antwort gilt, bevor neu gefragt wird, in Sekunden.
WETTER_ZWISCHENSPEICHER=300
# -----------------------------------------------------------------------------
# E-Mail-Versand (optional, hinter lib/integrations/mail.ts abstrahiert)
# Ohne MAIL_ENABLED=true wird nur ins Log geschrieben — praktisch für Tests.
# -----------------------------------------------------------------------------
MAIL_ENABLED=false
MAIL_HOST=smtp.beispiel.de
MAIL_PORT=587
MAIL_SECURE=false
MAIL_USER=
MAIL_PASSWORD=
MAIL_FROM=FLORIAN <florian@beispielwehr.de>
# -----------------------------------------------------------------------------
# Telegram-Benachrichtigungen (optional, abschaltbar)
# -----------------------------------------------------------------------------
TELEGRAM_ENABLED=false
TELEGRAM_BOT_TOKEN=
TELEGRAM_CHAT_ID=
# -----------------------------------------------------------------------------
# LLM-Anbindung (optional; nur selbstgehostete Endpunkte, kein Cloud-Zwang)
# -----------------------------------------------------------------------------
LLM_ENABLED=false
# OpenAI-kompatibler Endpunkt, z. B. eine lokale Ollama- oder vLLM-Instanz.
LLM_BASE_URL=http://ollama:11434/v1
LLM_API_KEY=
LLM_MODELL=qwen2.5:14b
# -----------------------------------------------------------------------------
# Einsätze und Übungen (Phase 3)
# -----------------------------------------------------------------------------
# Adressen zu Koordinaten auflösen. Vorgesehen ist ein SELBST betriebener
# Nominatim — Einsatzadressen gehören nicht an einen öffentlichen Dienst, und
# dessen Nutzungsbedingungen erlauben es ohnehin nicht.
GEOCODING_ENABLED=false
# NOMINATIM_URL=http://nominatim:8080
GEOCODING_TIMEOUT=5
# Auf welche Länder die Suche eingegrenzt wird (ISO-Codes, kommagetrennt).
GEOCODING_LAENDER=de,at,ch
# Einsatzportal für Fremdwehren: ein signierter Link ohne Anmeldung, über den
# eine Nachbarwehr den Stand sieht und Lagemeldungen absetzen kann.
PORTAL_ENABLED=true
# Wie lange ein neu erzeugter Link gilt.
PORTAL_GUELTIGKEIT_STUNDEN=24
# Höchstzahl Zugriffe je Link und Stunde.
PORTAL_MAX_ZUGRIFFE=120
# -----------------------------------------------------------------------------
# Wartung und Prüfung (Phase 2)
# -----------------------------------------------------------------------------
# Wann der nächtliche Lauf Prüftermine plant. Cron-Syntax, Zeitzone
# Europe/Berlin. Der Worker trägt den Zeitplan beim Start je Wehr selbst ein.
WARTUNG_NACHTLAUF=15 3 * * *
# Wann die Erinnerungen verschickt werden. Bewusst nach der Planung: sonst
# erinnert der Lauf an Termine, die es noch gar nicht gibt.
WARTUNG_ERINNERUNG_LAUF=45 3 * * *
# Wie weit im Voraus Termine angelegt werden. Ohne Horizont stünden Termine für
# die nächsten zehn Jahre im Dashboard.
WARTUNG_PLANUNGSHORIZONT_TAGE=180
# -----------------------------------------------------------------------------
# Datenschutz und Aufbewahrung (DSGVO)
# -----------------------------------------------------------------------------
# Aufbewahrungsfrist für Einsatz- und Prüfdokumentation in Jahren.
AUFBEWAHRUNG_JAHRE=10
# Aufbewahrungsfrist für den Audit-Log in Tagen. Der Log ist append-only;
# Älteres wird vom Worker exportiert, bevor es entfernt wird.
AUDIT_AUFBEWAHRUNG_TAGE=3650
# Frist in Tagen, nach der ein ausgetretenes Mitglied anonymisiert wird.
PERSON_ANONYMISIERUNG_TAGE=365
# -----------------------------------------------------------------------------
# Telemetrie bleibt aus. Nicht ändern.
# -----------------------------------------------------------------------------
NEXT_TELEMETRY_DISABLED=1
CHECKPOINT_DISABLE=1