Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b20abcb23 | ||
|
|
14cc4de9b8 | ||
|
|
91742a0dd7 | ||
|
|
2b53556175 | ||
|
|
d70acad4cd | ||
|
|
7196c7110c | ||
|
|
8081131eb7 | ||
|
|
f7ccd9a908 | ||
|
|
12243f177d | ||
|
|
0b8874da26 | ||
|
|
c779266450 | ||
|
|
4cda79434f | ||
|
|
1c1e141666 | ||
|
|
e1480c0c3e | ||
|
|
4e2daaafd4 | ||
|
|
b2b31ed00a | ||
|
|
e500ad6c0e | ||
|
|
791523027f | ||
|
|
d8d692098d | ||
|
|
91954c402b | ||
|
|
bc62a9c1e8 | ||
|
|
e503aafc18 | ||
|
|
7f866ac35c | ||
|
|
1744403620 | ||
|
|
47b8e36a38 | ||
|
|
26b62a3477 |
@@ -164,6 +164,11 @@ AUTH_LOCAL_ENABLED=true
|
|||||||
# Nach dem ersten Login das Passwort in der Oberfläche ändern.
|
# Nach dem ersten Login das Passwort in der Oberfläche ändern.
|
||||||
SEED_ADMIN_EMAIL=admin@beispielwehr.de
|
SEED_ADMIN_EMAIL=admin@beispielwehr.de
|
||||||
SEED_ADMIN_PASSWORT=Feuerwehr2026
|
SEED_ADMIN_PASSWORT=Feuerwehr2026
|
||||||
|
# Das Konto der Kreisebene aus dem Beispielbestand (Phase 10): Mitglied in der
|
||||||
|
# eigenen Wehr, Aufsicht in der Nachbarwehr. Vorgabe leitet sich aus der
|
||||||
|
# Wehrkennung ab, also kbi@ff-musterhausen.de.
|
||||||
|
# SEED_KBI_EMAIL=
|
||||||
|
# SEED_KBI_PASSWORT=Feuerwehr2026
|
||||||
# bcrypt-Kostenfaktor. 12 ist ein guter Kompromiss aus Sicherheit und Tempo.
|
# bcrypt-Kostenfaktor. 12 ist ein guter Kompromiss aus Sicherheit und Tempo.
|
||||||
AUTH_BCRYPT_ROUNDS=12
|
AUTH_BCRYPT_ROUNDS=12
|
||||||
|
|
||||||
@@ -185,6 +190,57 @@ KIOSK_SPERRDAUER=300
|
|||||||
# Automatische Abmeldung am Terminal nach Untätigkeit, in Sekunden.
|
# Automatische Abmeldung am Terminal nach Untätigkeit, in Sekunden.
|
||||||
KIOSK_IDLE_TIMEOUT=120
|
KIOSK_IDLE_TIMEOUT=120
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Kartenmodule (Phase 6)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# FLORIAN laedt keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre
|
||||||
|
# Hydranten liegen, ist niemandes Geschaeft — der Kartenserver laeuft selbst:
|
||||||
|
#
|
||||||
|
# docker compose --profile karte up -d
|
||||||
|
#
|
||||||
|
# Dafuer wird eine MBTiles-Datei des Ausrueckegebiets gebraucht (siehe README).
|
||||||
|
# Ohne KARTE_STIL_URL zeigt die Karte einen leeren Hintergrund mit Gitternetz;
|
||||||
|
# Hydranten, Objekte und Lagekarte funktionieren trotzdem.
|
||||||
|
KARTE_STIL_URL=http://localhost:8085/styles/basic/style.json
|
||||||
|
# Kartenmittelpunkt beim Oeffnen (Musterhausen).
|
||||||
|
KARTE_BREITENGRAD=48.5
|
||||||
|
KARTE_LAENGENGRAD=12.15
|
||||||
|
KARTE_ZOOM=13
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 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)
|
# E-Mail-Versand (optional, hinter lib/integrations/mail.ts abstrahiert)
|
||||||
# Ohne MAIL_ENABLED=true wird nur ins Log geschrieben — praktisch für Tests.
|
# Ohne MAIL_ENABLED=true wird nur ins Log geschrieben — praktisch für Tests.
|
||||||
@@ -204,14 +260,155 @@ TELEGRAM_ENABLED=false
|
|||||||
TELEGRAM_BOT_TOKEN=
|
TELEGRAM_BOT_TOKEN=
|
||||||
TELEGRAM_CHAT_ID=
|
TELEGRAM_CHAT_ID=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Push über ntfy (Phase 7, optional)
|
||||||
|
#
|
||||||
|
# ntfy ist ein winziger Push-Dienst, den man selbst betreibt: ein Container,
|
||||||
|
# eine App im Store, fertig. Man abonniert ein "Thema" (einen frei gewählten
|
||||||
|
# Namen) und bekommt Meldungen darauf — ohne Konto, ohne Telefonnummer.
|
||||||
|
#
|
||||||
|
# Läuft der Server im eigenen Netz, verlässt keine Nachricht das Gerätehaus.
|
||||||
|
# Deshalb gilt ntfy in FLORIAN als INTERNER Kanal und bekommt die
|
||||||
|
# ausführlichen Texte. Wer stattdessen das öffentliche ntfy.sh einträgt, macht
|
||||||
|
# ihn zu einem externen Dienst wie jeden anderen.
|
||||||
|
#
|
||||||
|
# ACHTUNG: Ein Thema ist nur so geheim wie sein Name. Wer ihn kennt, liest mit.
|
||||||
|
# FLORIAN schlägt deshalb Namen mit Zufallsanteil vor.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
NTFY_ENABLED=false
|
||||||
|
# NTFY_URL=http://ntfy:80
|
||||||
|
# Nur nötig, wenn der eigene ntfy Zugriffsschutz eingeschaltet hat.
|
||||||
|
NTFY_TOKEN=
|
||||||
|
NTFY_TIMEOUT=10
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Web-Push in den Browser (Phase 7, optional)
|
||||||
|
#
|
||||||
|
# Der Weg, der ohne App auskommt. Die Meldung läuft über den Push-Dienst des
|
||||||
|
# Browserherstellers (Mozilla, Google, Apple) — verschlüsselt, aber sie
|
||||||
|
# verlässt das Netz der Wehr. Deshalb ist Web-Push ein EXTERNER Kanal und
|
||||||
|
# bekommt nur Kurzfassungen: "3 Prüfungen fällig", nicht welche.
|
||||||
|
#
|
||||||
|
# Schlüsselpaar erzeugen mit: npm run vapid
|
||||||
|
#
|
||||||
|
# ACHTUNG: Beide Schlüssel gehören ins Backup. Ein neuer öffentlicher Schlüssel
|
||||||
|
# macht alle bestehenden Push-Abos wertlos — jeder müsste Push neu erlauben,
|
||||||
|
# und niemand merkt es, die Meldungen bleiben einfach aus.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
WEBPUSH_ENABLED=false
|
||||||
|
WEBPUSH_PUBLIC_KEY=
|
||||||
|
WEBPUSH_PRIVATE_KEY=
|
||||||
|
# Kontaktweg für den Push-Dienst, "mailto:..." oder eine Adresse.
|
||||||
|
WEBPUSH_KONTAKT=
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# WhatsApp: bewusst NICHT.
|
||||||
|
#
|
||||||
|
# Es gibt keine WHATSAPP_-Variablen, und das ist eine Entscheidung, keine Lücke.
|
||||||
|
# Die Begründung steht im README unter "Warum kein WhatsApp" — kurz: die
|
||||||
|
# Business-Plattform kostet je Konversation, verlangt einen Vertrag mit Meta
|
||||||
|
# als Auftragsverarbeiter, und die App gleicht das Telefonbuch ab. Für eine
|
||||||
|
# Wehr, deren Daten im Gerätehaus bleiben sollen, ist das der falsche Weg.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Benachrichtigungen und Termine (Phase 7)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Wann der Worker die Erinnerungen anlegt und verschickt (cron, Europe/Berlin).
|
||||||
|
# Bewusst am frühen Morgen und nicht nachts: eine Push-Meldung um drei Uhr
|
||||||
|
# weckt Leute, die am nächsten Tag arbeiten müssen.
|
||||||
|
BENACHRICHTIGUNG_LAUF=0 7 * * *
|
||||||
|
# Wie viele Tage vor einem Termin erinnert wird (kommagetrennt).
|
||||||
|
# "7,1": einmal in der Woche davor, damit man den Abend freihalten kann, und
|
||||||
|
# einmal am Vortag, weil man es bis dahin wieder vergessen hat.
|
||||||
|
TERMIN_ERINNERUNG_TAGE=7,1
|
||||||
|
# Wie oft eine fehlgeschlagene Zustellung wiederholt wird, bevor sie als
|
||||||
|
# fehlgeschlagen stehen bleibt. Stehen bleiben ist Absicht: eine still
|
||||||
|
# verschluckte Benachrichtigung ist schlimmer als eine sichtbar gescheiterte.
|
||||||
|
BENACHRICHTIGUNG_VERSUCHE=3
|
||||||
|
# Wie lange ZUGESTELLTE Benachrichtigungen aufgehoben werden.
|
||||||
|
# Fehlgeschlagene bleiben — sie zeigen einen kaputten Kanal an.
|
||||||
|
BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE=90
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# LLM-Anbindung (optional; nur selbstgehostete Endpunkte, kein Cloud-Zwang)
|
# LLM-Anbindung (optional; nur selbstgehostete Endpunkte, kein Cloud-Zwang)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
# ES GIBT KEINEN RUECKFALL AUF EINEN ANBIETER. Laeuft der eigene Endpunkt
|
||||||
|
# nicht, fehlen die KI-Funktionen — sie werden nicht durch einen fremden
|
||||||
|
# Dienst ersetzt. Ein Einsatzbericht spricht ueber Menschen in ihrer
|
||||||
|
# schlechtesten Stunde; solche Texte gehoeren nirgendwo anders hin.
|
||||||
|
#
|
||||||
|
# Container starten: docker compose --profile ki up -d
|
||||||
|
# Modelle holen: docker compose exec ollama ollama pull <modell>
|
||||||
LLM_ENABLED=false
|
LLM_ENABLED=false
|
||||||
# OpenAI-kompatibler Endpunkt, z. B. eine lokale Ollama- oder vLLM-Instanz.
|
# OpenAI-kompatibler Endpunkt, z. B. eine lokale Ollama- oder vLLM-Instanz.
|
||||||
LLM_BASE_URL=http://ollama:11434/v1
|
LLM_BASE_URL=http://ollama:11434/v1
|
||||||
LLM_API_KEY=
|
LLM_API_KEY=
|
||||||
LLM_MODELL=qwen2.5:14b
|
LLM_MODELL=qwen2.5:14b
|
||||||
|
# Zeitlimit fuer eine Antwort. Ein Bericht darf dauern, aber nicht ewig.
|
||||||
|
LLM_TIMEOUT=120
|
||||||
|
|
||||||
|
# Modell fuer Einbettungen (semantische Suche im Wiki).
|
||||||
|
#
|
||||||
|
# ACHTUNG: Die Spalte wikiabschnitt.einbettung ist auf 768 Dimensionen
|
||||||
|
# festgelegt — das liefert nomic-embed-text. Ein Modell mit anderer Laenge
|
||||||
|
# braucht eine Migration; wie, steht im README unter Phase 8.
|
||||||
|
LLM_EINBETTUNG_MODELL=nomic-embed-text
|
||||||
|
LLM_EINBETTUNG_DIMENSIONEN=768
|
||||||
|
|
||||||
|
# Modell, das Bilder lesen kann — fuer die Belegerfassung. Ohne dieses Modell
|
||||||
|
# werden Belege trotzdem abgelegt, die Angaben nur von Hand eingetragen.
|
||||||
|
# LLM_VISION_MODELL=llama3.2-vision
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Spracherkennung fuers Diktat (Phase 8, optional)
|
||||||
|
#
|
||||||
|
# Selbst betriebener faster-whisper im Compose-Profil "ki". Groesse ueber
|
||||||
|
# WHISPER_MODELL: "tiny" laeuft auf jedem Buerorechner, "base" ist der
|
||||||
|
# brauchbare Kompromiss, "small" erkennt Fachbegriffe merklich besser und
|
||||||
|
# will dafuer Zeit oder eine Grafikkarte.
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
WHISPER_ENABLED=false
|
||||||
|
WHISPER_URL=http://whisper:8000
|
||||||
|
WHISPER_MODELL=base
|
||||||
|
WHISPER_SPRACHE=de
|
||||||
|
# Zeitlimit fuer eine Aufnahme. Zehn Minuten Sprache brauchen Zeit.
|
||||||
|
WHISPER_TIMEOUT=300
|
||||||
|
# Hoechstlaenge einer Aufnahme in Sekunden.
|
||||||
|
DIKTAT_MAX_SEKUNDEN=600
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# 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)
|
# Datenschutz und Aufbewahrung (DSGVO)
|
||||||
@@ -229,3 +426,19 @@ PERSON_ANONYMISIERUNG_TAGE=365
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
NEXT_TELEMETRY_DISABLED=1
|
NEXT_TELEMETRY_DISABLED=1
|
||||||
CHECKPOINT_DISABLE=1
|
CHECKPOINT_DISABLE=1
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Testwehr fuer die E2E-Tests
|
||||||
|
#
|
||||||
|
# Die Playwright-Tests laufen gegen eine EIGENE Wehr, nicht gegen die
|
||||||
|
# Beispielwehr aus der Anleitung. Sonst wachsen deren Listen bei jedem Lauf,
|
||||||
|
# und die Bildschirmfotos aendern sich, ohne dass sich die Oberflaeche
|
||||||
|
# geaendert haette.
|
||||||
|
#
|
||||||
|
# Vor dem Lauf einmal: npm run db:test-reset
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
E2E_SLUG=ff-teststelle
|
||||||
|
E2E_NAME=Freiwillige Feuerwehr Teststelle
|
||||||
|
E2E_KURZNAME=FF Teststelle
|
||||||
|
E2E_ADMIN_EMAIL=e2e@teststelle.de
|
||||||
|
E2E_ADMIN_PASSWORT=Feuerwehr2026
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# Die Pruefung bei jedem Push.
|
||||||
|
#
|
||||||
|
# Bis Phase 10 gab es 581 Unit- und 266 E2E-Tests — und nichts, was sie
|
||||||
|
# ausfuehrte ausser einem Menschen, der daran dachte. In der Sitzung, in der
|
||||||
|
# die Kassenverwaltung entstand, haben die Tests vier echte Fehler gefunden,
|
||||||
|
# zwei davon in Code, der eine Stunde alt war. Genau das soll hier von allein
|
||||||
|
# passieren.
|
||||||
|
#
|
||||||
|
# Zwei Auftraege, bewusst getrennt:
|
||||||
|
#
|
||||||
|
# pruefung typecheck, lint und die Unit-Tests. Braucht eine Datenbank,
|
||||||
|
# weil ein Teil der Tests die Trigger und RLS gegen echtes
|
||||||
|
# Postgres prueft — mit einer Attrappe waeren sie wertlos.
|
||||||
|
#
|
||||||
|
# oberflaeche die E2E-Tests. Dauern eine Dreiviertelstunde und brauchen den
|
||||||
|
# halben Stack, deshalb laufen sie nur auf main und in Pull
|
||||||
|
# Requests, nicht bei jedem Zwischenstand auf einem Zweig.
|
||||||
|
# =============================================================================
|
||||||
|
name: Pruefung
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Dieselben Werte wie in .env.example. Es sind Testdaten in einem
|
||||||
|
# Wegwerf-Container; echte Geheimnisse haben in einer Pipeline nichts zu
|
||||||
|
# suchen und stehen im Betrieb in der .env.
|
||||||
|
POSTGRES_USER: florian
|
||||||
|
POSTGRES_PASSWORD: pruefung
|
||||||
|
POSTGRES_DB: florian
|
||||||
|
POSTGRES_APP_PASSWORD: pruefung_app
|
||||||
|
DATABASE_URL: postgresql://florian_app:pruefung_app@localhost:5432/florian?schema=public
|
||||||
|
DIRECT_DATABASE_URL: postgresql://florian:pruefung@localhost:5432/florian?schema=public
|
||||||
|
AUTH_SECRET: nur-fuer-die-pruefung-nicht-fuer-den-betrieb
|
||||||
|
AUTH_TRUST_HOST: "true"
|
||||||
|
APP_URL: http://localhost:3100
|
||||||
|
S3_ENDPOINT: localhost
|
||||||
|
S3_PORT: "9000"
|
||||||
|
S3_USE_SSL: "false"
|
||||||
|
S3_ACCESS_KEY: florian
|
||||||
|
S3_SECRET_KEY: pruefung_minio
|
||||||
|
S3_BUCKET: florian
|
||||||
|
REDIS_URL: redis://localhost:6379
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pruefung:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
# Das eigene Postgres-Image statt eines fertigen: FLORIAN braucht
|
||||||
|
# PostGIS **und** pgvector, und kein oeffentliches Image bringt beides
|
||||||
|
# mit. Ein Dienst-Container mit postgres:17 wuerde die Migrationen
|
||||||
|
# durchlassen und erst bei der ersten Umkreissuche scheitern.
|
||||||
|
- name: Datenbank bauen und starten
|
||||||
|
run: |
|
||||||
|
docker build -t florian-postgres:pruefung ./docker/postgres
|
||||||
|
docker run -d --name postgres -p 5432:5432 \
|
||||||
|
-e POSTGRES_USER=$POSTGRES_USER \
|
||||||
|
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||||
|
-e POSTGRES_DB=$POSTGRES_DB \
|
||||||
|
-e POSTGRES_APP_PASSWORD=$POSTGRES_APP_PASSWORD \
|
||||||
|
-e POSTGRES_INITDB_ARGS="--locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8 --encoding=UTF8" \
|
||||||
|
-v "$(pwd)/docker/postgres:/docker-entrypoint-initdb.d:ro" \
|
||||||
|
florian-postgres:pruefung
|
||||||
|
|
||||||
|
- name: Redis und MinIO starten
|
||||||
|
run: |
|
||||||
|
docker run -d --name redis -p 6379:6379 redis:7-alpine
|
||||||
|
docker run -d --name minio -p 9000:9000 \
|
||||||
|
-e MINIO_ROOT_USER=$S3_ACCESS_KEY \
|
||||||
|
-e MINIO_ROOT_PASSWORD=$S3_SECRET_KEY \
|
||||||
|
minio/minio:latest server /data
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
|
||||||
|
- name: Auf die Datenbank warten
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
docker exec postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" && exit 0
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
echo "Postgres kam nicht hoch:"
|
||||||
|
docker logs postgres
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- run: npx prisma generate
|
||||||
|
- run: npm run typecheck
|
||||||
|
- run: npm run lint
|
||||||
|
|
||||||
|
- name: Migrationen einspielen
|
||||||
|
run: npm run db:migrate
|
||||||
|
|
||||||
|
# Der Seed gehoert dazu: er ist selbst ein Test. Faellt eine Migration
|
||||||
|
# oder ein Standardwert um, merkt man es hier und nicht erst, wenn eine
|
||||||
|
# Wehr ihn zum ersten Mal ausfuehrt.
|
||||||
|
- name: Beispielbestand anlegen
|
||||||
|
run: npm run db:seed
|
||||||
|
|
||||||
|
- run: npm run test
|
||||||
|
|
||||||
|
- name: Aufraeumen
|
||||||
|
if: always()
|
||||||
|
run: docker rm -f postgres redis minio || true
|
||||||
|
|
||||||
|
oberflaeche:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Nur wo es sich lohnt: ein voller Lauf dauert etwa 45 Minuten.
|
||||||
|
if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Dienste starten
|
||||||
|
run: |
|
||||||
|
docker build -t florian-postgres:pruefung ./docker/postgres
|
||||||
|
docker run -d --name postgres -p 5432:5432 \
|
||||||
|
-e POSTGRES_USER=$POSTGRES_USER \
|
||||||
|
-e POSTGRES_PASSWORD=$POSTGRES_PASSWORD \
|
||||||
|
-e POSTGRES_DB=$POSTGRES_DB \
|
||||||
|
-e POSTGRES_APP_PASSWORD=$POSTGRES_APP_PASSWORD \
|
||||||
|
-e POSTGRES_INITDB_ARGS="--locale-provider=icu --icu-locale=de-DE --locale=C.UTF-8 --encoding=UTF8" \
|
||||||
|
-v "$(pwd)/docker/postgres:/docker-entrypoint-initdb.d:ro" \
|
||||||
|
florian-postgres:pruefung
|
||||||
|
docker run -d --name redis -p 6379:6379 redis:7-alpine
|
||||||
|
docker run -d --name minio -p 9000:9000 \
|
||||||
|
-e MINIO_ROOT_USER=$S3_ACCESS_KEY \
|
||||||
|
-e MINIO_ROOT_PASSWORD=$S3_SECRET_KEY \
|
||||||
|
minio/minio:latest server /data
|
||||||
|
docker run -d --name gotenberg -p 3001:3000 gotenberg/gotenberg:8
|
||||||
|
|
||||||
|
- run: npm ci
|
||||||
|
- run: npx playwright install --with-deps chromium
|
||||||
|
|
||||||
|
- name: Auf die Datenbank warten
|
||||||
|
run: |
|
||||||
|
for i in $(seq 1 60); do
|
||||||
|
docker exec postgres pg_isready -U "$POSTGRES_USER" -d "$POSTGRES_DB" && exit 0
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
docker logs postgres
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- run: npx prisma generate
|
||||||
|
- run: npm run db:migrate
|
||||||
|
|
||||||
|
# Die E2E-Tests laufen seit Phase 8 gegen eine eigene Wehr, nicht gegen
|
||||||
|
# den Beispielbestand. Das Skript legt beide an.
|
||||||
|
- name: Testwehr anlegen
|
||||||
|
run: npm run db:test-reset
|
||||||
|
|
||||||
|
- run: npm run build
|
||||||
|
- run: npx playwright test
|
||||||
|
env:
|
||||||
|
GOTENBERG_URL: http://localhost:3001
|
||||||
|
|
||||||
|
- name: Fehlerbilder sichern
|
||||||
|
if: failure()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: playwright-bericht
|
||||||
|
path: |
|
||||||
|
playwright-report/
|
||||||
|
test-results/
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
|
- name: Aufraeumen
|
||||||
|
if: always()
|
||||||
|
run: docker rm -f postgres redis minio gotenberg || true
|
||||||
@@ -40,3 +40,6 @@ debug.log
|
|||||||
# Prisma
|
# Prisma
|
||||||
prisma/*.db
|
prisma/*.db
|
||||||
prisma/*.db-journal
|
prisma/*.db-journal
|
||||||
|
|
||||||
|
# Sicherungen bleiben lokal — sie enthalten den gesamten Bestand.
|
||||||
|
sicherungen/
|
||||||
|
|||||||
@@ -47,6 +47,20 @@ x-app-env: &app-env
|
|||||||
KIOSK_MAX_FEHLVERSUCHE: ${KIOSK_MAX_FEHLVERSUCHE:-5}
|
KIOSK_MAX_FEHLVERSUCHE: ${KIOSK_MAX_FEHLVERSUCHE:-5}
|
||||||
KIOSK_SPERRDAUER: ${KIOSK_SPERRDAUER:-300}
|
KIOSK_SPERRDAUER: ${KIOSK_SPERRDAUER:-300}
|
||||||
KIOSK_IDLE_TIMEOUT: ${KIOSK_IDLE_TIMEOUT:-120}
|
KIOSK_IDLE_TIMEOUT: ${KIOSK_IDLE_TIMEOUT:-120}
|
||||||
|
KARTE_STIL_URL: ${KARTE_STIL_URL:-}
|
||||||
|
KARTE_BREITENGRAD: ${KARTE_BREITENGRAD:-48.5}
|
||||||
|
KARTE_LAENGENGRAD: ${KARTE_LAENGENGRAD:-12.15}
|
||||||
|
KARTE_ZOOM: ${KARTE_ZOOM:-13}
|
||||||
|
ATEMSCHUTZ_HINWEIS: ${ATEMSCHUTZ_HINWEIS:-true}
|
||||||
|
ATEMSCHUTZ_FREIGABE: ${ATEMSCHUTZ_FREIGABE:-}
|
||||||
|
ATEMSCHUTZ_ABFRAGE_INTERVALL: ${ATEMSCHUTZ_ABFRAGE_INTERVALL:-600}
|
||||||
|
ATEMSCHUTZ_WARNDRUCK: ${ATEMSCHUTZ_WARNDRUCK:-55}
|
||||||
|
ATEMSCHUTZ_ZUSCHLAG: ${ATEMSCHUTZ_ZUSCHLAG:-10}
|
||||||
|
WETTER_ENABLED: ${WETTER_ENABLED:-false}
|
||||||
|
DWD_WARNCELL: ${DWD_WARNCELL:-}
|
||||||
|
DWD_WARNUNGEN_URL: ${DWD_WARNUNGEN_URL:-https://www.dwd.de/DWD/warnungen/warnapp/json/warnings.json}
|
||||||
|
WETTER_TIMEOUT: ${WETTER_TIMEOUT:-5}
|
||||||
|
WETTER_ZWISCHENSPEICHER: ${WETTER_ZWISCHENSPEICHER:-300}
|
||||||
MAIL_ENABLED: ${MAIL_ENABLED:-false}
|
MAIL_ENABLED: ${MAIL_ENABLED:-false}
|
||||||
MAIL_HOST: ${MAIL_HOST:-}
|
MAIL_HOST: ${MAIL_HOST:-}
|
||||||
MAIL_PORT: ${MAIL_PORT:-587}
|
MAIL_PORT: ${MAIL_PORT:-587}
|
||||||
@@ -57,10 +71,42 @@ x-app-env: &app-env
|
|||||||
TELEGRAM_ENABLED: ${TELEGRAM_ENABLED:-false}
|
TELEGRAM_ENABLED: ${TELEGRAM_ENABLED:-false}
|
||||||
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
TELEGRAM_BOT_TOKEN: ${TELEGRAM_BOT_TOKEN:-}
|
||||||
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
TELEGRAM_CHAT_ID: ${TELEGRAM_CHAT_ID:-}
|
||||||
|
NTFY_ENABLED: ${NTFY_ENABLED:-false}
|
||||||
|
NTFY_URL: ${NTFY_URL:-}
|
||||||
|
NTFY_TOKEN: ${NTFY_TOKEN:-}
|
||||||
|
NTFY_TIMEOUT: ${NTFY_TIMEOUT:-10}
|
||||||
|
WEBPUSH_ENABLED: ${WEBPUSH_ENABLED:-false}
|
||||||
|
WEBPUSH_PUBLIC_KEY: ${WEBPUSH_PUBLIC_KEY:-}
|
||||||
|
WEBPUSH_PRIVATE_KEY: ${WEBPUSH_PRIVATE_KEY:-}
|
||||||
|
WEBPUSH_KONTAKT: ${WEBPUSH_KONTAKT:-}
|
||||||
|
BENACHRICHTIGUNG_LAUF: ${BENACHRICHTIGUNG_LAUF:-0 7 * * *}
|
||||||
|
TERMIN_ERINNERUNG_TAGE: ${TERMIN_ERINNERUNG_TAGE:-7,1}
|
||||||
|
BENACHRICHTIGUNG_VERSUCHE: ${BENACHRICHTIGUNG_VERSUCHE:-3}
|
||||||
|
BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE: ${BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE:-90}
|
||||||
LLM_ENABLED: ${LLM_ENABLED:-false}
|
LLM_ENABLED: ${LLM_ENABLED:-false}
|
||||||
LLM_BASE_URL: ${LLM_BASE_URL:-}
|
LLM_BASE_URL: ${LLM_BASE_URL:-}
|
||||||
LLM_API_KEY: ${LLM_API_KEY:-}
|
LLM_API_KEY: ${LLM_API_KEY:-}
|
||||||
LLM_MODELL: ${LLM_MODELL:-}
|
LLM_MODELL: ${LLM_MODELL:-}
|
||||||
|
LLM_TIMEOUT: ${LLM_TIMEOUT:-120}
|
||||||
|
LLM_EINBETTUNG_MODELL: ${LLM_EINBETTUNG_MODELL:-nomic-embed-text}
|
||||||
|
LLM_EINBETTUNG_DIMENSIONEN: ${LLM_EINBETTUNG_DIMENSIONEN:-768}
|
||||||
|
LLM_VISION_MODELL: ${LLM_VISION_MODELL:-}
|
||||||
|
WHISPER_ENABLED: ${WHISPER_ENABLED:-false}
|
||||||
|
WHISPER_URL: ${WHISPER_URL:-}
|
||||||
|
WHISPER_MODELL: ${WHISPER_MODELL:-base}
|
||||||
|
WHISPER_SPRACHE: ${WHISPER_SPRACHE:-de}
|
||||||
|
WHISPER_TIMEOUT: ${WHISPER_TIMEOUT:-300}
|
||||||
|
DIKTAT_MAX_SEKUNDEN: ${DIKTAT_MAX_SEKUNDEN:-600}
|
||||||
|
GEOCODING_ENABLED: ${GEOCODING_ENABLED:-false}
|
||||||
|
NOMINATIM_URL: ${NOMINATIM_URL:-}
|
||||||
|
GEOCODING_TIMEOUT: ${GEOCODING_TIMEOUT:-5}
|
||||||
|
GEOCODING_LAENDER: ${GEOCODING_LAENDER:-de,at,ch}
|
||||||
|
PORTAL_ENABLED: ${PORTAL_ENABLED:-true}
|
||||||
|
PORTAL_GUELTIGKEIT_STUNDEN: ${PORTAL_GUELTIGKEIT_STUNDEN:-24}
|
||||||
|
PORTAL_MAX_ZUGRIFFE: ${PORTAL_MAX_ZUGRIFFE:-120}
|
||||||
|
WARTUNG_NACHTLAUF: ${WARTUNG_NACHTLAUF:-15 3 * * *}
|
||||||
|
WARTUNG_ERINNERUNG_LAUF: ${WARTUNG_ERINNERUNG_LAUF:-45 3 * * *}
|
||||||
|
WARTUNG_PLANUNGSHORIZONT_TAGE: ${WARTUNG_PLANUNGSHORIZONT_TAGE:-180}
|
||||||
AUFBEWAHRUNG_JAHRE: ${AUFBEWAHRUNG_JAHRE:-10}
|
AUFBEWAHRUNG_JAHRE: ${AUFBEWAHRUNG_JAHRE:-10}
|
||||||
AUDIT_AUFBEWAHRUNG_TAGE: ${AUDIT_AUFBEWAHRUNG_TAGE:-3650}
|
AUDIT_AUFBEWAHRUNG_TAGE: ${AUDIT_AUFBEWAHRUNG_TAGE:-3650}
|
||||||
PERSON_ANONYMISIERUNG_TAGE: ${PERSON_ANONYMISIERUNG_TAGE:-365}
|
PERSON_ANONYMISIERUNG_TAGE: ${PERSON_ANONYMISIERUNG_TAGE:-365}
|
||||||
@@ -108,10 +154,18 @@ services:
|
|||||||
logging: *logging
|
logging: *logging
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# PostgreSQL 17 mit PostGIS. Einziger Ort für relationale Daten.
|
# PostgreSQL 17 mit PostGIS und pgvector. Einziger Ort für relationale Daten.
|
||||||
|
#
|
||||||
|
# Eigenes Image, weil FLORIAN beides braucht und es kein offizielles gibt,
|
||||||
|
# das beides mitbringt — siehe docker/postgres/Dockerfile. Beim ersten
|
||||||
|
# `docker compose up` wird es gebaut; bei einer bestehenden Installation
|
||||||
|
# genügt `docker compose build postgres` und ein Neustart des Dienstes. Das
|
||||||
|
# Datenverzeichnis bleibt unberührt, die Hauptversion ändert sich nicht.
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
postgres:
|
postgres:
|
||||||
image: postgis/postgis:17-3.5
|
build:
|
||||||
|
context: ./docker/postgres
|
||||||
|
image: florian-postgres:${IMAGE_TAG:-17-3.5-pgvector}
|
||||||
container_name: florian-postgres
|
container_name: florian-postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
@@ -252,6 +306,152 @@ services:
|
|||||||
- intern
|
- intern
|
||||||
logging: *logging
|
logging: *logging
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Kartenserver (Phase 6).
|
||||||
|
#
|
||||||
|
# Nur mit Profil: "docker compose --profile karte up -d". FLORIAN läuft ohne
|
||||||
|
# ihn — die Karte zeigt dann einen leeren Hintergrund, Hydranten und
|
||||||
|
# Lagekarte funktionieren trotzdem.
|
||||||
|
#
|
||||||
|
# Gebraucht wird eine MBTiles-Datei des Ausrückegebiets unter
|
||||||
|
# docker/karten/. Woher man sie bekommt, steht in der README; sie liegt
|
||||||
|
# bewusst nicht im Repository — das sind je nach Zuschnitt hunderte Megabyte
|
||||||
|
# bis Gigabyte, und jede Wehr braucht einen anderen Ausschnitt.
|
||||||
|
#
|
||||||
|
# Der Dienst hängt im internen Netz und im Edge-Netz: der Browser des
|
||||||
|
# Anwenders holt die Kacheln direkt, nicht über die Anwendung.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
tileserver:
|
||||||
|
image: maptiler/tileserver-gl:v4.11.1
|
||||||
|
container_name: florian-tileserver
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: [karte]
|
||||||
|
command: ["--port", "8080", "--public_url", "${KARTE_OEFFENTLICH:-http://localhost:8085/}"]
|
||||||
|
volumes:
|
||||||
|
- ./docker/karten:/data:ro
|
||||||
|
ports:
|
||||||
|
- "${KARTE_PORT:-8085}:8080"
|
||||||
|
networks:
|
||||||
|
- edge
|
||||||
|
- intern
|
||||||
|
logging: *logging
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Ollama — Sprachmodelle im eigenen Haus (Phase 8).
|
||||||
|
#
|
||||||
|
# Nur mit Profil: docker compose --profile ki up -d
|
||||||
|
#
|
||||||
|
# Es gibt bewusst KEINEN Rückfall auf einen Anbieter. Läuft dieser Container
|
||||||
|
# nicht, fehlen die KI-Funktionen — sie werden nicht durch einen fremden
|
||||||
|
# Dienst ersetzt. Ein Einsatzbericht spricht über Menschen in ihrer
|
||||||
|
# schlechtesten Stunde; solche Texte gehören nirgendwo anders hin.
|
||||||
|
#
|
||||||
|
# Nach dem ersten Start müssen die Modelle geholt werden:
|
||||||
|
#
|
||||||
|
# docker compose exec ollama ollama pull qwen2.5:14b # Text
|
||||||
|
# docker compose exec ollama ollama pull nomic-embed-text # Einbettungen
|
||||||
|
# docker compose exec ollama ollama pull llama3.2-vision # Belege (optional)
|
||||||
|
#
|
||||||
|
# Auf einem Rechner ohne Grafikkarte läuft das Einbettungsmodell noch flott,
|
||||||
|
# das Textmodell zäh und das Vision-Modell praktisch nicht. Deshalb sind alle
|
||||||
|
# drei einzeln konfigurierbar: eine Wehr kann das Wiki durchsuchbar machen,
|
||||||
|
# ohne Berichte schreiben zu lassen.
|
||||||
|
#
|
||||||
|
# Für eine Nvidia-Karte zusätzlich das Compose-Fragment aus dem README
|
||||||
|
# einbinden (deploy.resources.reservations.devices) — es steht nicht fest
|
||||||
|
# hier drin, weil es ohne Karte den Start verhindert.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ollama:
|
||||||
|
image: ollama/ollama:0.5.7
|
||||||
|
container_name: florian-ollama
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: [ki]
|
||||||
|
environment:
|
||||||
|
# Modelle bleiben geladen, statt nach fünf Minuten aus dem Speicher zu
|
||||||
|
# fallen. Wer zweimal am Tag einen Bericht schreibt, wartet sonst jedes
|
||||||
|
# Mal auf das Nachladen.
|
||||||
|
OLLAMA_KEEP_ALIVE: ${OLLAMA_KEEP_ALIVE:-30m}
|
||||||
|
OLLAMA_HOST: "0.0.0.0:11434"
|
||||||
|
volumes:
|
||||||
|
- ollama-data:/root/.ollama
|
||||||
|
ports:
|
||||||
|
- "${OLLAMA_PORT:-11435}:11434"
|
||||||
|
networks:
|
||||||
|
- intern
|
||||||
|
logging: *logging
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# faster-whisper — Spracherkennung fürs Diktat (Phase 8).
|
||||||
|
#
|
||||||
|
# Nur mit Profil: docker compose --profile ki up -d
|
||||||
|
#
|
||||||
|
# OpenAI-kompatible Schnittstelle, aber vollständig lokal. Das Modell wird
|
||||||
|
# beim ersten Aufruf geholt und dann behalten.
|
||||||
|
#
|
||||||
|
# WHISPER_MODELL steuert die Größe: "tiny" läuft auf jedem Bürorechner,
|
||||||
|
# "base" ist der brauchbare Kompromiss, "small" erkennt Fachbegriffe
|
||||||
|
# merklich besser und will dafür Zeit oder eine Grafikkarte.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
whisper:
|
||||||
|
image: fedirz/faster-whisper-server:latest-cpu
|
||||||
|
container_name: florian-whisper
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: [ki]
|
||||||
|
environment:
|
||||||
|
WHISPER__MODEL: ${WHISPER_MODELL:-base}
|
||||||
|
WHISPER__INFERENCE_DEVICE: ${WHISPER_GERAET:-cpu}
|
||||||
|
# Nach zehn Minuten ohne Anfrage den Speicher freigeben. Eine Wehr
|
||||||
|
# diktiert nicht stündlich.
|
||||||
|
WHISPER__TTL: ${WHISPER_TTL:-600}
|
||||||
|
ENABLE_UI: "false"
|
||||||
|
volumes:
|
||||||
|
- whisper-data:/root/.cache/huggingface
|
||||||
|
ports:
|
||||||
|
- "${WHISPER_PORT:-8087}:8000"
|
||||||
|
networks:
|
||||||
|
- intern
|
||||||
|
logging: *logging
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ntfy — Push aufs Handy, ohne dass etwas das Haus verlässt (Phase 7).
|
||||||
|
#
|
||||||
|
# Nur mit Profil: docker compose --profile push up -d
|
||||||
|
#
|
||||||
|
# Warum das der interessanteste Push-Weg für eine Wehr ist: kein Konto, keine
|
||||||
|
# Telefonnummer, kein Telefonbuchabgleich. Man abonniert in der App ein
|
||||||
|
# Thema — einen frei gewählten Namen — und bekommt Meldungen darauf.
|
||||||
|
#
|
||||||
|
# Der Container ist winzig und speichert standardmäßig nichts dauerhaft:
|
||||||
|
# eine zugestellte Meldung ist erledigt. Wer sie nachlesen können will,
|
||||||
|
# schaltet den Cache ein (siehe ntfy-Dokumentation) — dann liegen die Texte
|
||||||
|
# allerdings auch dort und nicht nur in FLORIAN.
|
||||||
|
#
|
||||||
|
# ACHTUNG: Ein Thema ist nur so geheim wie sein Name. Wer ihn kennt, liest
|
||||||
|
# mit. Deshalb schlägt FLORIAN Namen mit Zufallsanteil vor, und deshalb
|
||||||
|
# steht NTFY_TOKEN bereit, falls der eigene ntfy Zugriffsschutz bekommt.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ntfy:
|
||||||
|
image: binwiederhier/ntfy:v2.11.0
|
||||||
|
container_name: florian-ntfy
|
||||||
|
restart: unless-stopped
|
||||||
|
profiles: [push]
|
||||||
|
command: ["serve"]
|
||||||
|
environment:
|
||||||
|
NTFY_BASE_URL: ${NTFY_OEFFENTLICH:-http://localhost:8086}
|
||||||
|
NTFY_LISTEN_HTTP: ":80"
|
||||||
|
# Keine Anmeldung nötig, um zu lesen und zu schreiben — geschützt ist
|
||||||
|
# das Ding durch die Unerratbarkeit der Themennamen und dadurch, dass es
|
||||||
|
# im eigenen Netz steht. Wer es ins Internet stellt, setzt
|
||||||
|
# NTFY_AUTH_DEFAULT_ACCESS auf "deny-all" und vergibt Token.
|
||||||
|
NTFY_AUTH_DEFAULT_ACCESS: ${NTFY_ZUGRIFF:-read-write}
|
||||||
|
NTFY_BEHIND_PROXY: "true"
|
||||||
|
ports:
|
||||||
|
- "${NTFY_PORT:-8086}:80"
|
||||||
|
networks:
|
||||||
|
- edge
|
||||||
|
- intern
|
||||||
|
logging: *logging
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Die Anwendung selbst (Next.js, Standalone-Build).
|
# Die Anwendung selbst (Next.js, Standalone-Build).
|
||||||
# Führt vor dem Start die ausstehenden Prisma-Migrationen aus.
|
# Führt vor dem Start die ausstehenden Prisma-Migrationen aus.
|
||||||
@@ -298,7 +498,11 @@ services:
|
|||||||
image: florian-app:${IMAGE_TAG:-latest}
|
image: florian-app:${IMAGE_TAG:-latest}
|
||||||
container_name: florian-worker
|
container_name: florian-worker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: ["node", "--enable-source-maps", "dist/worker/index.js"]
|
# --conditions=react-server: lib/storage.ts und lib/pdf.ts sichern sich mit
|
||||||
|
# dem Paket "server-only" ab. Unter node wirft dessen Standardexport;
|
||||||
|
# mit dieser Auflösungsbedingung greift der leere react-server-Export,
|
||||||
|
# und der Worker darf dieselben Bausteine benutzen wie die Anwendung.
|
||||||
|
command: ["node", "--enable-source-maps", "--conditions=react-server", "dist/worker/index.js"]
|
||||||
environment:
|
environment:
|
||||||
<<: *app-env
|
<<: *app-env
|
||||||
ROLLE: worker
|
ROLLE: worker
|
||||||
@@ -324,6 +528,10 @@ volumes:
|
|||||||
redis-data:
|
redis-data:
|
||||||
minio-data:
|
minio-data:
|
||||||
traefik-acme:
|
traefik-acme:
|
||||||
|
# Phase 8: die Modelle. Sie sind mehrere Gigabyte groß und sollen einen
|
||||||
|
# Neustart des Containers überleben — sonst werden sie jedes Mal neu geholt.
|
||||||
|
ollama-data:
|
||||||
|
whisper-data:
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
# Nur was von außen erreichbar sein muss, hängt hier drin.
|
# Nur was von außen erreichbar sein muss, hängt hier drin.
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Kartenkacheln
|
||||||
|
|
||||||
|
Hier gehört die MBTiles-Datei des Ausrückegebiets hinein. Sie liegt **nicht**
|
||||||
|
im Repository: je nach Zuschnitt sind das hunderte Megabyte bis Gigabyte, und
|
||||||
|
jede Wehr braucht einen anderen Ausschnitt.
|
||||||
|
|
||||||
|
## Woher
|
||||||
|
|
||||||
|
Zwei Wege, beide ohne laufende Kosten und ohne Anmeldung:
|
||||||
|
|
||||||
|
1. **Fertiger Auszug.** Bei [maptiler.com/data](https://data.maptiler.com/downloads/planet/)
|
||||||
|
oder aus einem OpenMapTiles-Auszug den eigenen Landkreis herunterladen.
|
||||||
|
2. **Selbst schneiden.** Aus einem Geofabrik-Auszug (`.osm.pbf`) mit
|
||||||
|
[planetiler](https://github.com/onthegomap/planetiler) den eigenen
|
||||||
|
Ausschnitt erzeugen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -Xmx4g -jar planetiler.jar --download --area=oberbayern \
|
||||||
|
--output=/pfad/zu/docker/karten/ausrueckegebiet.mbtiles
|
||||||
|
```
|
||||||
|
|
||||||
|
Dazu gehört ein Stil. tileserver-gl bringt `basic` mit; ein eigener Stil kommt
|
||||||
|
als `styles/<name>/style.json` daneben.
|
||||||
|
|
||||||
|
## Starten
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --profile karte up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach in der `.env`:
|
||||||
|
|
||||||
|
```
|
||||||
|
KARTE_STIL_URL=http://localhost:8085/styles/basic/style.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ohne Kacheln
|
||||||
|
|
||||||
|
FLORIAN läuft auch ohne. Die Karte zeigt dann einen leeren Hintergrund mit
|
||||||
|
Gitternetz — Hydranten, Objekte und die Lagekarte liegen trotzdem an der
|
||||||
|
richtigen Stelle, nur ohne Straßen darunter. Für den Anfang reicht das, um
|
||||||
|
Daten zu erfassen; für den Einsatz nicht.
|
||||||
|
|
||||||
|
**Keine Kacheln aus dem Netz.** Wo eine Wehr ausrückt, wo ihre Hydranten
|
||||||
|
liegen und welche Objekte sie im Blick hat, ist niemandes Geschäft. Deshalb
|
||||||
|
gibt es in FLORIAN keinen Rückfall auf einen öffentlichen Kartendienst.
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Läuft EINMALIG beim allerersten Start des Postgres-Containers (leeres
|
# Läuft EINMALIG beim allerersten Start des Postgres-Containers (leeres
|
||||||
# Datenverzeichnis). Richtet ein:
|
# Datenverzeichnis). Richtet ein:
|
||||||
# - die benötigten Erweiterungen (PostGIS, pgcrypto, citext, unaccent, pg_trgm)
|
# - die benötigten Erweiterungen (PostGIS, pgvector, pgcrypto, citext,
|
||||||
|
# unaccent, pg_trgm)
|
||||||
# - den Anwendungsbenutzer "florian_app" OHNE BYPASSRLS
|
# - den Anwendungsbenutzer "florian_app" OHNE BYPASSRLS
|
||||||
# - Vorgabeprivilegien, damit künftige Tabellen automatisch nutzbar sind
|
# - Vorgabeprivilegien, damit künftige Tabellen automatisch nutzbar sind
|
||||||
#
|
#
|
||||||
@@ -28,6 +29,7 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-'
|
|||||||
CREATE EXTENSION IF NOT EXISTS citext; -- E-Mail ohne Gross/Klein
|
CREATE EXTENSION IF NOT EXISTS citext; -- E-Mail ohne Gross/Klein
|
||||||
CREATE EXTENSION IF NOT EXISTS unaccent; -- Suche ueber Umlaute hinweg
|
CREATE EXTENSION IF NOT EXISTS unaccent; -- Suche ueber Umlaute hinweg
|
||||||
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- unscharfe Namenssuche
|
CREATE EXTENSION IF NOT EXISTS pg_trgm; -- unscharfe Namenssuche
|
||||||
|
CREATE EXTENSION IF NOT EXISTS vector; -- semantische Suche im Wiki
|
||||||
EOSQL
|
EOSQL
|
||||||
|
|
||||||
# Anwendungsrolle anlegen. Bewusst OHNE DO-Block: psql ersetzt seine Variablen
|
# Anwendungsrolle anlegen. Bewusst OHNE DO-Block: psql ersetzt seine Variablen
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# PostgreSQL 17 mit PostGIS **und** pgvector.
|
||||||
|
#
|
||||||
|
# Warum ein eigenes Image: FLORIAN braucht beides. PostGIS seit Phase 6 für
|
||||||
|
# Hydranten und Umkreissuche, pgvector seit Phase 8 für die semantische Suche
|
||||||
|
# im Wiki. Es gibt kein offizielles Image, das beides mitbringt —
|
||||||
|
# postgis/postgis kennt kein vector, pgvector/pgvector kein PostGIS.
|
||||||
|
#
|
||||||
|
# Die Alternative wäre gewesen, die Ähnlichkeitssuche in der Anwendung zu
|
||||||
|
# rechnen: alle Abschnitte laden, Kosinusabstand in TypeScript. Das geht bei
|
||||||
|
# zweihundert Wiki-Seiten und wird bei zweitausend zäh — und es verlegt eine
|
||||||
|
# Aufgabe in die Anwendung, für die die Datenbank einen Index hat.
|
||||||
|
#
|
||||||
|
# Das Basisimage ist Debian mit eingerichtetem PGDG-Repository; die Erweiterung
|
||||||
|
# kommt als fertiges Paket daher, nicht als Übersetzungslauf.
|
||||||
|
# =============================================================================
|
||||||
|
FROM postgis/postgis:17-3.5
|
||||||
|
|
||||||
|
# Version festhalten: ein stillschweigender Sprung auf eine neue pgvector-
|
||||||
|
# Fassung würde beim nächsten Neubau des Images passieren, nicht dann, wenn
|
||||||
|
# jemand darüber entscheidet.
|
||||||
|
ARG PGVECTOR_PAKET=postgresql-17-pgvector
|
||||||
|
|
||||||
|
RUN set -eux; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y --no-install-recommends "${PGVECTOR_PAKET}"; \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Die Erweiterung selbst wird nicht hier angelegt, sondern in der Migration
|
||||||
|
# (CREATE EXTENSION IF NOT EXISTS vector). Grund: eine bestehende Installation
|
||||||
|
# bekommt zwar das neue Image, aber nicht noch einmal den Init-Lauf — das
|
||||||
|
# Datenverzeichnis ist ja nicht leer. Über die Migration greift es in beiden
|
||||||
|
# Fällen.
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# Aufwandsschätzung
|
# Aufwandsschätzung
|
||||||
|
|
||||||
Schätzung je Phase, daneben der tatsächliche Aufwand aus dem
|
Schätzung je Phase, daneben der tatsächliche Aufwand aus dem
|
||||||
[Devlog](DEVLOG.md). Stand: 3. September 2026.
|
[Devlog](DEVLOG.md). Stand: 4. September 2026.
|
||||||
|
|
||||||
## Wie hier gerechnet wird
|
## Wie hier gerechnet wird
|
||||||
|
|
||||||
@@ -31,16 +31,18 @@ Faktoren, die in die Schätzung eingehen:
|
|||||||
| Phase | Inhalt | Vergleichsaufwand | Sitzungszeit geschätzt | Sitzungszeit tatsächlich |
|
| Phase | Inhalt | Vergleichsaufwand | Sitzungszeit geschätzt | Sitzungszeit tatsächlich |
|
||||||
|---|---|---:|---:|---:|
|
|---|---|---:|---:|---:|
|
||||||
| 0 | Fundament | 12–16 PT | — | **1:00 h** |
|
| 0 | Fundament | 12–16 PT | — | **1:00 h** |
|
||||||
| 1 | Stammdaten | 10–14 PT | 2,5–3,5 h | offen |
|
| 1 | Stammdaten | 10–14 PT | 2,5–3,5 h | **2:05 h** |
|
||||||
| 2 | Wartung und Prüfung | 18–24 PT | 4,5–6 h | offen |
|
| 2 | Wartung und Prüfung | 18–24 PT | 4,5–6 h | **2:25 h** |
|
||||||
| 3 | Einsätze und Übungen | 12–16 PT | 3–4 h | offen |
|
| 3 | Einsätze und Übungen | 12–16 PT | 3–4 h | **2:10 h** |
|
||||||
| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | offen |
|
| 4 | Kiosk-PWA mit Offline | 16–22 PT | 4–5,5 h | **3:00 h** |
|
||||||
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | offen |
|
| 5 | Atemschutz (FwDV 7) | 14–18 PT | 3,5–4,5 h | **2:40 h** |
|
||||||
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | offen |
|
| 6 | Kartenmodule | 14–18 PT | 3,5–4,5 h | **2:50 h** |
|
||||||
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | offen |
|
| 7 | Benachrichtigungen und Termine | 10–13 PT | 2,5–3,5 h | **3:10 h** |
|
||||||
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | offen |
|
| 8 | Lokale KI | 9–12 PT | 2,5–3 h | **3:40 h** |
|
||||||
| 9 | Kassenverwaltung | 8–11 PT | 2–3 h | offen |
|
| 9 | Kassenverwaltung | 8–11 PT | 2–3 h | **4:10 h** |
|
||||||
| | **Summe** | **123–164 PT** | **28–37 h** | |
|
| 10 | Mandantenfähigkeit | 4–6 PT | 1–1,5 h | **4:20 h** |
|
||||||
|
| 11 | Freigabereife | 5–7 PT | 1,5–2 h | **2:40 h** |
|
||||||
|
| | **Summe** | **132–177 PT** | **30,5–40,5 h** | **35:30 h** |
|
||||||
|
|
||||||
123–164 Personentage entsprechen etwa **6 bis 8 Monaten Vollzeitarbeit** einer
|
123–164 Personentage entsprechen etwa **6 bis 8 Monaten Vollzeitarbeit** einer
|
||||||
einzelnen Person. Zum Vergleich: eine kommerzielle Feuerwehrverwaltung kostet
|
einzelnen Person. Zum Vergleich: eine kommerzielle Feuerwehrverwaltung kostet
|
||||||
@@ -72,7 +74,7 @@ das nachträglich einbaut, zahlt ein Vielfaches.
|
|||||||
|
|
||||||
## Phase 1 — Stammdaten
|
## Phase 1 — Stammdaten
|
||||||
|
|
||||||
**Vergleichsaufwand: 10–14 PT**
|
**Vergleichsaufwand: 10–14 PT** · **Tatsächlich: 2:05 h Sitzungszeit** (1:15 h Hauptteil, 0:50 h Nachtrag)
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -89,9 +91,23 @@ Der Import ist der Posten, der erfahrungsgemäß aus dem Ruder läuft. Jede Wehr
|
|||||||
bringt eine andere Excel-Datei mit, und die Vorschau vor dem Übernehmen ist
|
bringt eine andere Excel-Datei mit, und die Vorschau vor dem Übernehmen ist
|
||||||
kein Komfort, sondern die Bedingung dafür, dass die Wehr dem Ergebnis traut.
|
kein Komfort, sondern die Bedingung dafür, dass die Wehr dem Ergebnis traut.
|
||||||
|
|
||||||
|
**Rückblick.** Die Schätzung hat gut getroffen — mit einer Verschiebung: Der
|
||||||
|
Import war teurer als angesetzt (deutsche Datumsformate, Semikolon-CSV,
|
||||||
|
Windows-1252, Excel-Seriennummern, Dubletten in der Datei), die Tabellen
|
||||||
|
dagegen billiger, weil Sortierung und Blättern serverseitig laufen und keine
|
||||||
|
Tabellenbibliothek nötig war. Nicht eingeplant war die Zeitzonen-Falle in der
|
||||||
|
Fristenrechnung; sie hätte in Phase 2 deutlich mehr gekostet, wenn sie erst
|
||||||
|
dort aufgefallen wäre.
|
||||||
|
|
||||||
|
Das zunächst offen gebliebene Formular zum einzelnen Anlegen von Geräten wurde
|
||||||
|
als Nachtrag ergänzt — geschätzt waren 0,5 PT, tatsächlich 0:50 h Sitzungszeit.
|
||||||
|
Die Hälfte davon ging für zwei Dinge drauf, die nichts mit dem Formular zu tun
|
||||||
|
hatten: eine Server/Client-Grenze, die stillschweigend `undefined` liefert, und
|
||||||
|
Datumsfelder, die sich in Chromium nicht automatisiert füllen lassen.
|
||||||
|
|
||||||
## Phase 2 — Wartung und Prüfung
|
## Phase 2 — Wartung und Prüfung
|
||||||
|
|
||||||
**Vergleichsaufwand: 18–24 PT** — die aufwendigste Phase
|
**Vergleichsaufwand: 18–24 PT** · **Tatsächlich: 2:25 h Sitzungszeit** — die aufwendigste Phase
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -110,9 +126,32 @@ der Unfallkasse. Das heißt aber auch, dass jeder Fehler teuer ist. Die
|
|||||||
Unveränderlichkeit abgeschlossener Protokolle ist der Kern — und der Grund,
|
Unveränderlichkeit abgeschlossener Protokolle ist der Kern — und der Grund,
|
||||||
warum der Audit-Log schon in Phase 0 append-only ist.
|
warum der Audit-Log schon in Phase 0 append-only ist.
|
||||||
|
|
||||||
|
**Rückblick.** Deutlich unter der geschätzten Sitzungszeit, und das aus einem
|
||||||
|
Grund, der sich benennen lässt: die Fristenrechnung aus Phase 1 war schon da
|
||||||
|
und musste nur exportiert werden. Wäre sie hier entstanden — samt der
|
||||||
|
Zeitzonen-Falle, über die Phase 1 gestolpert ist —, sähe die Zahl anders aus.
|
||||||
|
|
||||||
|
Teurer als gedacht war das, was zwischen den Bausteinen liegt. Drei Beispiele:
|
||||||
|
|
||||||
|
- Die Reihenfolge beim Abschluss einer Prüfung ist nicht beliebig.
|
||||||
|
Unterschriften und Mängel müssen vor dem Statuswechsel entstehen, denn danach
|
||||||
|
lässt die Datenbank am Protokoll nichts mehr zu. Derselbe Stolperstein traf
|
||||||
|
auch den Seed, der die Beispielprüfung zunächst fertig anlegen wollte.
|
||||||
|
- Die Protokollnummer muss unter einer Sperre vergeben werden. Aufgefallen ist
|
||||||
|
das erst, weil die E2E-Tests Desktop und Tablet gleichzeitig prüfen ließen —
|
||||||
|
zwei Prüfer, die im selben Moment abschließen, sind im Gerätehaus kein
|
||||||
|
konstruierter Fall.
|
||||||
|
- Das PDF entsteht zwischen zwei Datenbanktransaktionen, nicht in einer. Fotos
|
||||||
|
aus MinIO holen und Gotenberg rendern lassen dauert länger, als eine offene
|
||||||
|
Transaktion warten darf.
|
||||||
|
|
||||||
|
Billiger als angesetzt war das Unterschriftenfeld: Pointer-Events decken Finger,
|
||||||
|
Stift und Maus in einem ab, und mehr als Skalierung auf die Pixeldichte und
|
||||||
|
`touch-action: none` braucht es nicht.
|
||||||
|
|
||||||
## Phase 3 — Einsätze und Übungen
|
## Phase 3 — Einsätze und Übungen
|
||||||
|
|
||||||
**Vergleichsaufwand: 12–16 PT**
|
**Vergleichsaufwand: 12–16 PT** · **Tatsächlich: 2:10 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -129,9 +168,33 @@ Das externe Portal für Fremdwehren ist sicherheitsrelevant: ein Zugang ohne
|
|||||||
Anmeldung, der zeitlich begrenzt und mengenbegrenzt sein muss. Der Aufwand
|
Anmeldung, der zeitlich begrenzt und mengenbegrenzt sein muss. Der Aufwand
|
||||||
steckt weniger in der Funktion als in der Absicherung.
|
steckt weniger in der Funktion als in der Absicherung.
|
||||||
|
|
||||||
|
**Rückblick.** Die Schätzung lag richtig darin, wo der Aufwand steckt — nur
|
||||||
|
nicht in welcher Reihenfolge.
|
||||||
|
|
||||||
|
Billiger als gedacht war das Portal. Der Grund liegt in Phase 0 und 2: die
|
||||||
|
Mandantentrennung, das Hashen von Geheimnissen und die Ablage von Dateien gab
|
||||||
|
es schon, und der Zugang war am Ende ein Datensatz mit Hash, ein Zeitfenster
|
||||||
|
und ein Zähler in Redis. Was blieb, war die Frage, was das Portal **nicht**
|
||||||
|
zeigen darf — und die ist fachlicher Natur, nicht technischer.
|
||||||
|
|
||||||
|
Teurer war die Statistik. Nicht das Rechnen, sondern die Entscheidung, welche
|
||||||
|
Zahl gilt: der Durchschnitt der Eintreffzeiten sagt nichts, weil ein einzelner
|
||||||
|
Einsatz auf der Autobahn ihn verschiebt. Es zählt der 80-Prozent-Wert, weil die
|
||||||
|
Gemeinde danach fragt — und das steht in keinem Pflichtenheft, das muss man
|
||||||
|
wissen.
|
||||||
|
|
||||||
|
Zwei Dinge fielen erst beim Tun auf:
|
||||||
|
|
||||||
|
- Koordinaten müssen doppelt liegen. Prisma kann den PostGIS-Typ nicht lesen;
|
||||||
|
ohne die Dezimalgrade daneben wäre jede Anzeige eine Rohabfrage geworden. Ein
|
||||||
|
Trigger hält beides zusammen.
|
||||||
|
- Der Nummernkreis war schon zweimal da (Prüfprotokolle, jetzt Einsätze und
|
||||||
|
Übungen). Beim dritten Mal wurde daraus ein eigener Baustein — die passende
|
||||||
|
Gelegenheit war genau diese, keine früher.
|
||||||
|
|
||||||
## Phase 4 — Kiosk-PWA
|
## Phase 4 — Kiosk-PWA
|
||||||
|
|
||||||
**Vergleichsaufwand: 16–22 PT**
|
**Vergleichsaufwand: 16–22 PT** · **Tatsächlich: 3:00 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -150,9 +213,45 @@ unter Verbindungsabbruch, Doppelklick und parallelem Zugriff von zwei
|
|||||||
Terminals nichts verliert und nichts doppelt schreibt, ist die eigentliche
|
Terminals nichts verliert und nichts doppelt schreibt, ist die eigentliche
|
||||||
Arbeit. Die Idempotenzschlüssel sind dafür der Schlüssel, nicht die Synchronisation.
|
Arbeit. Die Idempotenzschlüssel sind dafür der Schlüssel, nicht die Synchronisation.
|
||||||
|
|
||||||
|
**Rückblick.** Der Satz oben hat sich bestätigt, nur anders als gedacht. Die
|
||||||
|
Outbox selbst war in einer halben Stunde geschrieben und mit zwölf Unit-Tests
|
||||||
|
belegt. Zwei Stellen kosteten die Zeit, und keine davon steht in der
|
||||||
|
Aufstellung:
|
||||||
|
|
||||||
|
- **Der Zeitgeber, der nie ablief.** Die Warteschlange hatte einen
|
||||||
|
Wiederholungstakt von 20 Sekunden — und der wurde bei jedem Rendern neu
|
||||||
|
aufgezogen, weil das Mutationsobjekt von React Query in den Abhängigkeiten
|
||||||
|
stand. Alles sah richtig aus, alle Unit-Tests waren grün, und der liegen
|
||||||
|
gebliebene Vorgang wäre im Gerätehaus trotzdem liegen geblieben. Gefunden hat
|
||||||
|
das nur der Playwright-Test, der `context.setOffline(true)` setzt und dann
|
||||||
|
wartet. Die 1,5 PT für „Playwright-Tests für den Offline-Fall" waren der
|
||||||
|
bestangelegte Posten der Phase.
|
||||||
|
- **Die Frage, wann ein Vorgang aus der Warteschlange darf.** Nicht „wenn
|
||||||
|
erfolgreich", sondern „wenn er beim Server angekommen ist" — dazu gehört auch
|
||||||
|
„bereits verarbeitet" und „in der Konfliktliste". Diese Unterscheidung ist die
|
||||||
|
ganze Idempotenz; ohne sie kreist ein Vorgang für immer.
|
||||||
|
|
||||||
|
Billiger als angesetzt war die Anmeldung per RFID: die Aufstellung nennt WebHID,
|
||||||
|
gebraucht wurde es nicht. Handelsübliche Leser geben die Kartenkennung als
|
||||||
|
Tastatureingabe aus. Ein Zuhörer auf dem Fenster, der am Anschlagstempo
|
||||||
|
erkennt, dass da keine Hand tippt, ersetzt eine ganze Geräteschnittstelle.
|
||||||
|
|
||||||
|
Billiger war auch der Service Worker — 80 Zeilen von Hand, ohne Workbox. Die
|
||||||
|
Regeln sind einfach, solange man sie sich traut aufzuschreiben: Seiten aus dem
|
||||||
|
Netz mit Ausweichseite, lesende Abfragen aus dem Zwischenspeicher mit
|
||||||
|
Nachladen, schreibende **überhaupt nicht** anfassen. Eine Bibliothek hätte hier
|
||||||
|
mehr Fragen aufgeworfen als beantwortet.
|
||||||
|
|
||||||
|
Teurer war die Terminaloberfläche selbst. Nicht die Kacheln, sondern die
|
||||||
|
Zustände dahinter: Wer schon eingecheckt ist, sieht „Gehen" statt „Kommen". Wer
|
||||||
|
mit einem Fahrzeug unterwegs ist, sieht „Fahrt beenden". Wer keine Rechte für
|
||||||
|
die Inventur hat, sieht die Kachel nicht. Jede dieser Verzweigungen ist
|
||||||
|
trivial — sechs davon in einem Schirm, der ohne Nachdenken bedienbar bleiben
|
||||||
|
soll, sind es nicht.
|
||||||
|
|
||||||
## Phase 5 — Atemschutz nach FwDV 7
|
## Phase 5 — Atemschutz nach FwDV 7
|
||||||
|
|
||||||
**Vergleichsaufwand: 14–18 PT**
|
**Vergleichsaufwand: 14–18 PT** · **Tatsächlich: 2:40 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -171,9 +270,36 @@ Timer, der einen Reload überlebt, ist Pflicht — deshalb muss die Zeit
|
|||||||
serverseitig geführt werden und der Browser nur anzeigen. Diese Phase bekommt
|
serverseitig geführt werden und der Browser nur anzeigen. Diese Phase bekommt
|
||||||
den höchsten Testanteil im Projekt.
|
den höchsten Testanteil im Projekt.
|
||||||
|
|
||||||
|
**Rückblick.** Der Satz stimmt, aber die Schlussfolgerung war die falsche. Die
|
||||||
|
Aufstellung veranschlagt 3,5 PT für „serverseitig autoritative Timer" — als
|
||||||
|
wäre ein Timer zu bauen. Gebraucht wurde das Gegenteil: **kein** Timer. Wer die
|
||||||
|
Zeit als Zeitstempel in die Datenbank schreibt und jede Ansicht selbst rechnen
|
||||||
|
lässt, hat nichts, was einen Reload nicht überlebt. Der teuerste Posten der
|
||||||
|
Schätzung war der billigste der Umsetzung, weil die richtige Entscheidung ihn
|
||||||
|
auflöst statt ihn zu lösen.
|
||||||
|
|
||||||
|
Auch Socket.IO fiel weg. Es geht nur in eine Richtung — der Server meldet eine
|
||||||
|
Änderung, der Browser holt den Stand über den Weg, den es ohnehin gibt. Ein
|
||||||
|
Route Handler mit Server-Sent Events ist siebzig Zeilen und braucht kein
|
||||||
|
verändertes Deployment.
|
||||||
|
|
||||||
|
Teurer war das, was in keiner Zeile der Aufstellung steht: **die Frage, was
|
||||||
|
passiert, wenn nichts passiert.** Ein abgerissener Ereignisstrom meldet sich
|
||||||
|
nicht — `EventSource` schweigt einfach. Eine Anzeige, die deshalb weiter
|
||||||
|
„Verbunden" behauptet, ist gefährlicher als eine, die gar nichts anzeigt: sie
|
||||||
|
lässt einen alten Stand wie einen aktuellen aussehen. Aufgefallen ist das nur,
|
||||||
|
weil der E2E-Test das Netz gekappt und danach nachgesehen hat — die 2,0 PT für
|
||||||
|
„Tests: Reload und Verbindungsabbruch überstehen" waren wieder der
|
||||||
|
bestangelegte Posten der Phase.
|
||||||
|
|
||||||
|
Billiger als gedacht war die Tauglichkeit. Nicht das Rechnen — die
|
||||||
|
Entscheidung, **nicht** zu sperren. Damit entfielen alle Sonderfälle, die eine
|
||||||
|
Sperre nach sich zieht: Übersteuern, Begründungspflicht, Freigabe durch wen.
|
||||||
|
Was bleibt, ist ein Satz neben dem Namen, und der ist im Einsatz mehr wert.
|
||||||
|
|
||||||
## Phase 6 — Kartenmodule
|
## Phase 6 — Kartenmodule
|
||||||
|
|
||||||
**Vergleichsaufwand: 14–18 PT**
|
**Vergleichsaufwand: 14–18 PT** · **Tatsächlich: 2:50 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -189,9 +315,33 @@ Das SVG-Set für die taktischen Zeichen ist Fleißarbeit, aber unvermeidbar —
|
|||||||
fertige Sets sind selten frei verwendbar. Die Wasserförderungsberechnung ist
|
fertige Sets sind selten frei verwendbar. Die Wasserförderungsberechnung ist
|
||||||
Physik mit klaren Formeln und deshalb gut testbar.
|
Physik mit klaren Formeln und deshalb gut testbar.
|
||||||
|
|
||||||
|
**Rückblick.** Beide Sätze stimmen. Das Zeichenset war Fleißarbeit — 22 Zeichen
|
||||||
|
als SVG-Pfade, keine davon schwierig, alle zusammen eine Stunde. Und die
|
||||||
|
Wasserförderung war tatsächlich der am besten testbare Teil des ganzen
|
||||||
|
Projekts: 21 Tests, die alle etwas über die Physik aussagen statt über den Code.
|
||||||
|
|
||||||
|
Dabei fiel auf, dass die Beiwerte im ersten Wurf um den Faktor zehn danebenlagen
|
||||||
|
— aufgefallen ist es nur, weil die Tests gegen die **Tabellenwerte der
|
||||||
|
Ausbildung** prüfen und nicht gegen das, was die Funktion gerade ausrechnet.
|
||||||
|
Ein Test, der die eigene Implementierung bestätigt, hätte hier fröhlich grün
|
||||||
|
gemeldet.
|
||||||
|
|
||||||
|
Billiger als angesetzt war die Umkreissuche. 3,0 PT für „Hydranten mit PostGIS,
|
||||||
|
Cluster, Filter, Umkreissuche" — davon entfiel der Löwenanteil auf eine einzige
|
||||||
|
SQL-Abfrage mit `ST_DWithin` und einen GiST-Index. PostGIS war seit Phase 0 im
|
||||||
|
Container; genutzt wurde es erst jetzt.
|
||||||
|
|
||||||
|
Teurer war die Frage, was passiert, wenn es **keine Karte** gibt. Eine MBTiles-
|
||||||
|
Datei des Ausrückegebiets ist hunderte Megabyte groß, gehört niemandem außer
|
||||||
|
der Wehr und kann nicht im Repository liegen. Das heißt: der Normalfall beim
|
||||||
|
ersten Start ist eine Karte ohne Hintergrund — und sie muss trotzdem etwas
|
||||||
|
taugen. Aus dieser Anforderung kam der leere Stil mit Hinweis, die Erkennung
|
||||||
|
eines nicht erreichbaren Servers und die Zusicherung im E2E-Test, dass genau
|
||||||
|
das ehrlich angezeigt wird. In der Aufstellung steht davon keine Zeile.
|
||||||
|
|
||||||
## Phase 7 — Benachrichtigungen und Termine
|
## Phase 7 — Benachrichtigungen und Termine
|
||||||
|
|
||||||
**Vergleichsaufwand: 10–13 PT**
|
**Vergleichsaufwand: 10–13 PT** · **Tatsächlich: 3:10 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -199,15 +349,28 @@ Physik mit klaren Formeln und deshalb gut testbar.
|
|||||||
| Vorlagen mit Platzhaltern, je Ereignis und Rolle | 2,0 |
|
| Vorlagen mit Platzhaltern, je Ereignis und Rolle | 2,0 |
|
||||||
| Abonnements je Person | 1,0 |
|
| Abonnements je Person | 1,0 |
|
||||||
| Kalender mit Serienterminen und Selbstanmeldung | 3,0 |
|
| Kalender mit Serienterminen und Selbstanmeldung | 3,0 |
|
||||||
| CalDAV-/ICS-Feed je Person mit Token | 1,5 |
|
| ICS-Feed je Person mit Token | 1,5 |
|
||||||
| Digitaler Dienstausweis mit signiertem QR und Prüfseite | 2,0 |
|
| Digitaler Dienstausweis mit signiertem QR und Prüfseite | 2,0 |
|
||||||
|
|
||||||
Das Grundgerüst der Kanalabstraktion steht bereits aus Phase 0
|
Das Grundgerüst der Kanalabstraktion stand bereits aus Phase 0
|
||||||
(`lib/integrations/`), das spart hier etwa einen Personentag.
|
(`lib/integrations/`), das hat etwa einen Personentag gespart.
|
||||||
|
|
||||||
|
Die einzige Phase bisher, die über der geschätzten Sitzungszeit liegt — und
|
||||||
|
zwar an einer Stelle, die in der Aufstellung nicht vorkommt. „Kalender mit
|
||||||
|
Serienterminen" klingt nach einer Datumsrechnung; teuer war die Entscheidung
|
||||||
|
**gegen** eine Regel und **für** ausgeschriebene Termine, und dann die
|
||||||
|
Kalenderarithmetik dahinter: `setMonth(+1)` auf dem 31. Januar ergibt den
|
||||||
|
3. März, der 29. Februar existiert in vier von fünf Jahren nicht, und ein
|
||||||
|
Übungsabend muss über die ganze Serie auf demselben Wochentag bleiben. Das sind
|
||||||
|
drei Tests, die man erst schreibt, wenn man den Fall kennt.
|
||||||
|
|
||||||
|
Die 1,5 PT für den Feed waren dagegen zu hoch angesetzt, weil sie CalDAV
|
||||||
|
unterstellen. ICS ist ein Textformat; der ganze Erzeuger sind hundertsechzig
|
||||||
|
Zeilen samt Maskierung und Zeilenumbruch nach 75 Oktetten.
|
||||||
|
|
||||||
## Phase 8 — Lokale KI
|
## Phase 8 — Lokale KI
|
||||||
|
|
||||||
**Vergleichsaufwand: 9–12 PT**
|
**Vergleichsaufwand: 9–12 PT** · **Tatsächlich: 3:40 h Sitzungszeit**
|
||||||
|
|
||||||
| Baustein | PT |
|
| Baustein | PT |
|
||||||
|---|---:|
|
|---|---:|
|
||||||
@@ -217,10 +380,28 @@ Das Grundgerüst der Kanalabstraktion steht bereits aus Phase 0
|
|||||||
| Wiki mit pgvector, semantische Suche, RAG mit Quellenangabe | 3,5 |
|
| Wiki mit pgvector, semantische Suche, RAG mit Quellenangabe | 3,5 |
|
||||||
| Belegerfassung über ein Vision-Modell, immer bestätigen | 2,0 |
|
| Belegerfassung über ein Vision-Modell, immer bestätigen | 2,0 |
|
||||||
|
|
||||||
Der Client aus Phase 0 (`lib/integrations/llm.ts`) ist bereits vorhanden. Die
|
Der Client aus Phase 0 (`lib/integrations/llm.ts`) war bereits vorhanden — er
|
||||||
Kennzeichnungspflicht — jede KI-Ausgabe ist als solche erkennbar und muss
|
konnte allerdings nur Text und musste um Einbettungen und Bilder erweitert
|
||||||
bestätigt werden, bevor sie in ein Dokument mit Nachweischarakter gelangt —
|
werden.
|
||||||
kostet wenig Zeit, ist aber nicht verhandelbar.
|
|
||||||
|
Die Kennzeichnungspflicht war mit „kostet wenig Zeit" zu günstig eingeschätzt.
|
||||||
|
Sie kostet wenig, wenn man sie als Abzeichen an einer Ansicht versteht. Als
|
||||||
|
Zusicherung — eigener Datensatz je Ausgabe, unveränderlich, genau eine
|
||||||
|
Entscheidung, Trigger und REVOKE dahinter, zwei Protokollansichten — ist sie
|
||||||
|
ein guter halber Personentag, der in der Aufstellung gar nicht vorkommt.
|
||||||
|
|
||||||
|
Zwei weitere Posten fehlen in der Schätzung:
|
||||||
|
|
||||||
|
**Das eigene Postgres-Image.** pgvector und PostGIS gibt es nicht zusammen; es
|
||||||
|
gibt kein offizielles Image mit beidem. Ein Dockerfile, ein Neubau, ein Neustart
|
||||||
|
des Dienstes — eine halbe Stunde, aber sie stand nirgends.
|
||||||
|
|
||||||
|
**Der Betrieb ohne Modell.** Der größte Einzelposten dieser Phase, und in der
|
||||||
|
Aufstellung steht davon keine Zeile. Eine Wehr setzt FLORIAN auf, lange bevor
|
||||||
|
sie über eine Grafikkarte nachdenkt — also muss jede Ansicht ohne Modell
|
||||||
|
vollständig benutzbar bleiben und ehrlich sagen, was fehlt. Das sind vier
|
||||||
|
verschiedene Hinweise an vier Stellen, eine zweite Suche, die immer
|
||||||
|
funktioniert, und vierzehn E2E-Tests, die genau diesen Zustand absichern.
|
||||||
|
|
||||||
## Phase 9 — Kassenverwaltung
|
## Phase 9 — Kassenverwaltung
|
||||||
|
|
||||||
@@ -243,6 +424,41 @@ Die Unveränderlichkeit festgeschriebener Buchungen kostet hier fast nichts —
|
|||||||
das Muster steht seit Phase 0 mit dem Audit-Log-Trigger und lässt sich
|
das Muster steht seit Phase 0 mit dem Audit-Log-Trigger und lässt sich
|
||||||
übernehmen.
|
übernehmen.
|
||||||
|
|
||||||
|
**Tatsächlich: 4:10 h** — die längste Phase bisher. Beide Schätzungen oben
|
||||||
|
haben sich bestätigt und zugleich verschoben: MT940 kostete weniger als
|
||||||
|
gedacht, weil beide Formate ohne Fremdbibliothek auskommen. Dafür war die
|
||||||
|
Unveränderlichkeit nicht das Übernehmen eines Musters, sondern drei
|
||||||
|
Trigger mit drei genau begründeten Ausnahmen — und die Ausnahmen sind der
|
||||||
|
Teil, der Zeit gekostet hat. Zwei davon sind erst im Test aufgefallen:
|
||||||
|
der Prüfbericht, der nach dem Festschreiben entsteht, und die
|
||||||
|
DSGVO-Löschung einer Wehr, die an ihren eigenen festgeschriebenen Buchungen
|
||||||
|
scheiterte.
|
||||||
|
|
||||||
|
## Phase 10 — Mandantenfähigkeit
|
||||||
|
|
||||||
|
**Vergleichsaufwand: 4–6 PT** — aus der Kandidatenliste vorgezogen, am
|
||||||
|
05.09.2026 umgesetzt.
|
||||||
|
|
||||||
|
| Baustein | PT |
|
||||||
|
|---|---:|
|
||||||
|
| `Benutzer` und `Mitgliedschaft`, datenerhaltende Umstellung | 1,5 |
|
||||||
|
| Anmeldung: Konto statt Akte, Wehrwechsel im Sitzungstoken | 1,5 |
|
||||||
|
| Kreisebene mit reinen Leserechten | 1,0 |
|
||||||
|
| Wehrwähler und Verwaltung der Installation | 1,5 |
|
||||||
|
|
||||||
|
Die Schätzung ging von „technisch bereits möglich, Oberfläche fehlt" aus —
|
||||||
|
und das war zur Hälfte falsch. Datenmodell und RLS trugen tatsächlich mehrere
|
||||||
|
Wehren, aber die Anmeldung nicht: Passwort und OIDC-Subject hingen an der
|
||||||
|
Personenakte, und die gehört genau einer Wehr. Das war kein Oberflächenthema,
|
||||||
|
sondern ein Umbau am Fundament.
|
||||||
|
|
||||||
|
**Tatsächlich: 4:20 h.** Getragen hat eine Entscheidung ganz am Anfang: die
|
||||||
|
Sitzung behält ihre Form und entsteht nur anders. Ohne sie wären 61 Dateien
|
||||||
|
mit 414 Fundstellen zu ändern gewesen; so waren es acht. Die Zeit ging
|
||||||
|
stattdessen in die Absicherung — zwei Trigger, vier CHECKs und elf
|
||||||
|
Datenbanktests — und das ist die richtige Verteilung für eine Phase, deren
|
||||||
|
einziger Zweck es ist, dass eine Wehr die andere nicht sieht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Was hier nicht drin ist
|
## Was hier nicht drin ist
|
||||||
|
|||||||
@@ -0,0 +1,283 @@
|
|||||||
|
# FLORIAN — Kurzanleitung
|
||||||
|
|
||||||
|
Alles, was man am Anfang braucht, auf ein paar Seiten. Nach Rollen sortiert:
|
||||||
|
such dir den Abschnitt, der zu dir gehört, und lies den Rest später.
|
||||||
|
|
||||||
|
Ausführlich steht alles in der [Anleitung](ANLEITUNG.md) — 63 Kapitel, zum
|
||||||
|
Nachschlagen, nicht zum Durchlesen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für alle
|
||||||
|
|
||||||
|
### Anmelden
|
||||||
|
|
||||||
|
FLORIAN läuft im Browser. Die Adresse bekommst du vom Kommandanten.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Nach der Anmeldung landest du auf der Übersicht: was ansteht, was fällig ist,
|
||||||
|
was gemeldet wurde.
|
||||||
|
|
||||||
|
**Das Passwort gehört nach der ersten Anmeldung geändert.** Oben rechts auf
|
||||||
|
deinen Namen.
|
||||||
|
|
||||||
|
### Was du überall findest
|
||||||
|
|
||||||
|
| Links in der Leiste | Wofür |
|
||||||
|
|---|---|
|
||||||
|
| **Übersicht** | Die Startseite: Fristen, Mängel, nächste Termine |
|
||||||
|
| **Termine** | Übungsabende, Lehrgänge, Versammlungen — mit Rückmeldung |
|
||||||
|
| **Dienstausweis** | Dein Ausweis mit QR-Code, auch fürs Handy |
|
||||||
|
| **Wissen** | Merkblätter und Dienstanweisungen der Wehr |
|
||||||
|
|
||||||
|
Du siehst nur, was zu deiner Rolle gehört. Fehlt dir etwas, sag dem
|
||||||
|
Kommandanten Bescheid — Rollen werden dort vergeben.
|
||||||
|
|
||||||
|
### Zu einem Termin zurückmelden
|
||||||
|
|
||||||
|
**Termine** öffnen, den Termin anklicken, **Zusagen** oder **Absagen**. Fertig.
|
||||||
|
Wer absagt, kann einen Grund angeben; das hilft dem Ausbilder bei der Planung.
|
||||||
|
|
||||||
|
### Am Terminal im Gerätehaus
|
||||||
|
|
||||||
|
Am Wandtablet meldest du dich mit **PIN oder Ausweiskarte** an — nicht mit
|
||||||
|
E-Mail und Passwort. Deine PIN bekommst du vom Kommandanten.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Dort geht: anmelden zum Dienst, einen Mangel melden, eine Fahrt eintragen,
|
||||||
|
Schutzkleidung zur Reinigung geben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für den Gerätewart
|
||||||
|
|
||||||
|
Das ist der Bereich, in dem FLORIAN am meisten abnimmt.
|
||||||
|
|
||||||
|
### Der wichtigste Bildschirm: Prüfungen
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Unter **Prüfungen** steht, was wann fällig wird. Rot heißt überfällig, gelb
|
||||||
|
heißt demnächst. Die Fristen rechnet FLORIAN selbst aus den Prüfintervallen
|
||||||
|
der Gerätetypen aus — du musst nichts nachhalten.
|
||||||
|
|
||||||
|
### Ein Gerät prüfen
|
||||||
|
|
||||||
|
1. **Prüfungen** → das fällige Gerät anklicken → **Prüfung durchführen**.
|
||||||
|
2. Die Checkliste durchgehen. Sie kommt aus der Wartungsvorlage des Gerätetyps.
|
||||||
|
3. Ergebnis eintragen, unterschreiben, abschließen.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Danach entsteht ein **Prüfprotokoll als PDF** — das ist der Nachweis gegenüber
|
||||||
|
der Unfallkasse.
|
||||||
|
|
||||||
|
> **Achtung:** Ein abgeschlossenes Protokoll lässt sich nicht mehr ändern. Das
|
||||||
|
> ist Absicht — ein nachträglich änderbarer Nachweis wäre keiner. Ist etwas
|
||||||
|
> falsch, wird eine neue Prüfung erfasst.
|
||||||
|
|
||||||
|
### Ein Gerät anlegen
|
||||||
|
|
||||||
|
**Geräte** → **Gerät anlegen**. Pflicht sind nur Inventarnummer und
|
||||||
|
Bezeichnung; der Rest kann nachkommen.
|
||||||
|
|
||||||
|
Wähle möglichst einen **Gerätetyp** — daran hängen Prüfintervall und
|
||||||
|
Lebensdauer, und ohne Typ rechnet FLORIAN keine Frist.
|
||||||
|
|
||||||
|
### Einen Mangel bearbeiten
|
||||||
|
|
||||||
|
Mängel kommen meist vom Terminal — jemand meldet sie beim Einrücken.
|
||||||
|
|
||||||
|
**Mängel** → anklicken → jemandem zuweisen → nach der Behebung **erledigen**.
|
||||||
|
Ein Gerät mit einem schweren Mangel wird automatisch gesperrt.
|
||||||
|
|
||||||
|
### Was noch dazugehört
|
||||||
|
|
||||||
|
| Wo | Wofür |
|
||||||
|
|---|---|
|
||||||
|
| **Rückrufe** | Herstellerrückrufe: welche eurer Geräte betroffen sind |
|
||||||
|
| **Lebensdauer** | Helme, Schläuche, Schutzkleidung mit Höchstalter |
|
||||||
|
| **Fahrzeuge** | Beladung nach Geräteräumen |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für den Atemschutzgerätewart
|
||||||
|
|
||||||
|
> **Der wichtigste Satz zuerst:** Die Überwachungstafel am Fahrzeug bleibt
|
||||||
|
> führend. FLORIAN ergänzt sie, ersetzt sie nicht. Das steht auch dauerhaft
|
||||||
|
> über jeder Ansicht, und es soll dort stehen.
|
||||||
|
|
||||||
|
### Wer darf unter Pressluftatmer?
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Atemschutz** zeigt jeden Träger mit seinem Stand: G26, Belastungsübung,
|
||||||
|
Einsatzübung. Wer eine der Voraussetzungen nicht erfüllt, steht rot da — mit
|
||||||
|
Klartext, woran es liegt, nicht nur mit einer Farbe.
|
||||||
|
|
||||||
|
### Eine G26 eintragen
|
||||||
|
|
||||||
|
**Atemschutz** → **Nachweise** → Person wählen → G26 erfassen. Untersuchungsdatum,
|
||||||
|
Ergebnis und Gültigkeit reichen; den Befund speichert FLORIAN bewusst nicht.
|
||||||
|
|
||||||
|
Dasselbe für Belastungsübungen.
|
||||||
|
|
||||||
|
### Eine Überwachung führen
|
||||||
|
|
||||||
|
**Atemschutz** → **Überwachung eröffnen**. Trupp zusammenstellen, Druckwerte
|
||||||
|
in Intervallen eintragen, Rückzugsdruck im Blick behalten. FLORIAN rechnet den
|
||||||
|
Rückzugsdruck und warnt, bevor es knapp wird.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Nach dem Ende entsteht ein vollständiges Protokoll als PDF.
|
||||||
|
|
||||||
|
> Solange ein Trupp im Einsatz ist, lässt sich die Überwachung nicht beenden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für den Kommandanten
|
||||||
|
|
||||||
|
### Mitglieder und Rollen
|
||||||
|
|
||||||
|
**Mitglieder** → Person anklicken → Rollen vergeben.
|
||||||
|
|
||||||
|
Neun Rollen, jede mit festem Rechteumfang. Die wichtigsten: Gerätewart,
|
||||||
|
Atemschutzgerätewart, Kassier, Schriftführer, Ausbilder.
|
||||||
|
|
||||||
|
**Jemand kommt nicht hinein?** Dann hat die Person eine Akte, aber kein Konto.
|
||||||
|
Konten verwaltet, wer den Server betreibt — unter *Wehren und Konten*.
|
||||||
|
|
||||||
|
### Einen Einsatz erfassen
|
||||||
|
|
||||||
|
**Einsätze** → **Einsatz anlegen**. Die sechs Zeitstempel (Alarmierung,
|
||||||
|
Ausrücken, Eintreffen, Einsatzende, Einrücken, Einsatzbereitschaft), die
|
||||||
|
beteiligten Fahrzeuge und die Mannschaft.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Daraus entsteht der **Einsatzbericht als PDF** und die Statistik fürs Jahr.
|
||||||
|
|
||||||
|
### Kostenersatz abrechnen
|
||||||
|
|
||||||
|
Fehlalarm einer Brandmeldeanlage, Öleinsatz mit Verursacher, vermieteter
|
||||||
|
Schulungsraum: **Kasse** → **Rechnungen** → *Rechnung schreiben*. Die Nummer
|
||||||
|
vergibt FLORIAN fortlaufend.
|
||||||
|
|
||||||
|
### Wenn jemand Auskunft über seine Daten verlangt
|
||||||
|
|
||||||
|
Kommt selten vor, hat aber eine Frist von einem Monat.
|
||||||
|
|
||||||
|
**Mitglieder** → Person → **Datenauskunft** → *Auskunft erstellen*. Daraus
|
||||||
|
entsteht ein PDF mit allem, was über die Person gespeichert ist. Ausdrucken
|
||||||
|
und aushändigen, fertig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für den Kassier
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Buchen
|
||||||
|
|
||||||
|
**Kasse** → **Buchungen** → *Buchung erfassen*. Der Betrag ist immer positiv;
|
||||||
|
ob Einnahme oder Ausgabe, sagen die beiden Knöpfe daneben.
|
||||||
|
|
||||||
|
FLORIAN schlägt eine Kategorie vor — meist die richtige, weil er nachsieht,
|
||||||
|
wie ihr dieselbe Sache zuletzt gebucht habt.
|
||||||
|
|
||||||
|
### Kontoauszug einlesen
|
||||||
|
|
||||||
|
Statt fünfzig Umsätze abzutippen: im Online-Banking den **Umsatzexport**
|
||||||
|
herunterladen (CAMT.053 oder MT940), dann **Kasse** → **Buchungen** →
|
||||||
|
*Kontoauszug*.
|
||||||
|
|
||||||
|
Es entstehen **Entwürfe**, keine fertigen Buchungen — durchsehen musst du sie.
|
||||||
|
Umsätze, die schon in der Kasse stehen, erkennt FLORIAN und hakt sie nicht
|
||||||
|
noch einmal ab.
|
||||||
|
|
||||||
|
### Beiträge
|
||||||
|
|
||||||
|
Einmal im Jahr: **Kasse** → **Beiträge** → *Beitragslauf*. Vorher steht da, wie
|
||||||
|
viele Beiträge entstehen — und wer keinen passenden Satz hat.
|
||||||
|
|
||||||
|
Zahlungen hakst du in der Liste ab; dabei entsteht gleich die Buchung.
|
||||||
|
|
||||||
|
### Festschreiben — und was das bedeutet
|
||||||
|
|
||||||
|
Eine neue Buchung ist erst einmal ein **Entwurf** und änderbar. Mit
|
||||||
|
*Festschreiben* wird sie unveränderbar.
|
||||||
|
|
||||||
|
Ist danach etwas falsch, wird **gegengebucht**: eine zweite Buchung in die
|
||||||
|
andere Richtung, mit Begründung. Beide bleiben stehen. Das sieht nach mehr
|
||||||
|
Aufwand aus und ist genau der Punkt — eine verschwundene Buchung kann ein
|
||||||
|
Kassenprüfer nicht prüfen, eine berichtigte schon.
|
||||||
|
|
||||||
|
### Kassenprüfung
|
||||||
|
|
||||||
|
**Kasse** → **Abschluss** → *Kassenprüfung anlegen*. Links steht der Bestand
|
||||||
|
laut Buch, rechts trägst du ein, was gezählt wurde.
|
||||||
|
|
||||||
|
Bleibt eine Differenz, muss sie erklärt werden — sonst lässt sich der Abschluss
|
||||||
|
nicht festschreiben. In fünf Jahren weiß sonst niemand mehr, woher sie kam.
|
||||||
|
|
||||||
|
Danach entsteht der **Kassenprüfbericht als PDF** mit Unterschriftsfeldern.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Für den, der den Server betreibt
|
||||||
|
|
||||||
|
### Sichern
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run sicherung
|
||||||
|
```
|
||||||
|
|
||||||
|
Sichert Datenbank **und** Dateien. Das Ergebnis gehört auf ein anderes Gerät —
|
||||||
|
eine Sicherung auf derselben Festplatte hilft gegen einen Festplattenfehler
|
||||||
|
nicht.
|
||||||
|
|
||||||
|
### Einmal im Jahr proben
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run wiederherstellen -- --quelle sicherungen/<Ordner>
|
||||||
|
```
|
||||||
|
|
||||||
|
So passiert nichts, es wird nur angezeigt, was geschähe. Erst mit `--wirklich`
|
||||||
|
wird zurückgespielt.
|
||||||
|
|
||||||
|
> Eine Sicherung, die nie zurückgespielt wurde, ist eine Vermutung. Der Ordner
|
||||||
|
> kann leer sein, der Auszug abgeschnitten — man erfährt es sonst an dem Tag,
|
||||||
|
> an dem man es nicht erfahren will.
|
||||||
|
|
||||||
|
### Aktualisieren
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./update.sh --pruefen # nachsehen, ob es etwas Neues gibt
|
||||||
|
./update.sh # einspielen
|
||||||
|
```
|
||||||
|
|
||||||
|
Das Skript sichert von sich aus, bevor es etwas anfasst, und sagt bei einem
|
||||||
|
Fehler, wie der vorherige Stand zurückkommt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Wenn etwas nicht geht
|
||||||
|
|
||||||
|
| Was | Woran es meist liegt |
|
||||||
|
|---|---|
|
||||||
|
| **Anmeldung geht nicht** | Nach fünf Fehlversuchen ist das Konto fünf Minuten gesperrt. Kurz warten. |
|
||||||
|
| **Ein Menüpunkt fehlt** | Die Rolle fehlt. Der Kommandant vergibt sie unter *Mitglieder*. |
|
||||||
|
| **Ein PDF entsteht nicht** | Es wird im Hintergrund erzeugt; nach ein paar Sekunden die Seite neu laden. |
|
||||||
|
| **Das Terminal meldet niemanden an** | PIN falsch, oder das Tablet ist nicht mehr eingerichtet. |
|
||||||
|
| **Eine Buchung lässt sich nicht ändern** | Sie ist festgeschrieben. Gegenbuchen. |
|
||||||
|
|
||||||
|
Steht die Antwort nicht dabei: [Anleitung, letztes Kapitel](ANLEITUNG.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*FLORIAN läuft auf einem Server, den die Feuerwehr selbst betreibt. Es gibt
|
||||||
|
keine Telemetrie, keine Cloud und keine Weitergabe an Dritte — die Daten
|
||||||
|
bleiben im Gerätehaus.*
|
||||||
@@ -10,16 +10,18 @@ Versionszuordnung: [VERSIONIERUNG.md](VERSIONIERUNG.md).
|
|||||||
|
|
||||||
```
|
```
|
||||||
Phase 0 ████████████████████ fertig 0.1.0
|
Phase 0 ████████████████████ fertig 0.1.0
|
||||||
Phase 1 ░░░░░░░░░░░░░░░░░░░░ offen 0.2.0
|
Phase 1 ████████████████████ fertig 0.2.0
|
||||||
Phase 2 ░░░░░░░░░░░░░░░░░░░░ offen 0.3.0
|
Phase 2 ████████████████████ fertig 0.3.0
|
||||||
Phase 3 ░░░░░░░░░░░░░░░░░░░░ offen 0.4.0
|
Phase 3 ████████████████████ fertig 0.4.0
|
||||||
Phase 4 ░░░░░░░░░░░░░░░░░░░░ offen 0.5.0
|
Phase 4 ████████████████████ fertig 0.5.0
|
||||||
Phase 5 ░░░░░░░░░░░░░░░░░░░░ offen 0.6.0
|
Phase 5 ████████████████████ fertig 0.6.0
|
||||||
Phase 6 ░░░░░░░░░░░░░░░░░░░░ offen 0.7.0
|
Phase 6 ████████████████████ fertig 0.7.0
|
||||||
Phase 7 ░░░░░░░░░░░░░░░░░░░░ offen 0.8.0
|
Phase 7 ████████████████████ fertig 0.8.0
|
||||||
Phase 8 ░░░░░░░░░░░░░░░░░░░░ offen 0.9.0
|
Phase 8 ████████████████████ fertig 0.9.0
|
||||||
Phase 9 ░░░░░░░░░░░░░░░░░░░░ offen 0.10.0
|
Phase 9 ████████████████████ fertig 0.10.0
|
||||||
Freigabe ░░░░░░░░░░░░░░░░░░░░ offen 1.0.0
|
Phase 10 ████████████████████ fertig 0.11.0
|
||||||
|
Phase 11 ████████████████████ fertig 0.12.0
|
||||||
|
Freigabe ████████████████████ fertig 1.0.0
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -39,7 +41,7 @@ Alles, worauf der Rest aufsetzt.
|
|||||||
- Basis-Layout mit Seitenleiste und dunkler Darstellung
|
- Basis-Layout mit Seitenleiste und dunkler Darstellung
|
||||||
- Seed mit einer vollständigen Beispielwehr
|
- Seed mit einer vollständigen Beispielwehr
|
||||||
|
|
||||||
## Phase 1 — Stammdaten · als Nächstes
|
## Phase 1 — Stammdaten · fertig
|
||||||
|
|
||||||
Die Wehr trägt ein, was sie hat.
|
Die Wehr trägt ein, was sie hat.
|
||||||
|
|
||||||
@@ -53,27 +55,37 @@ Die Wehr trägt ein, was sie hat.
|
|||||||
- CSV-/Excel-Import mit Spaltenzuordnung und Vorschau
|
- CSV-/Excel-Import mit Spaltenzuordnung und Vorschau
|
||||||
- Gerätehistorie aus dem Audit-Log
|
- Gerätehistorie aus dem Audit-Log
|
||||||
- Qualifikationen mit Ablaufwarnung auf der Übersicht
|
- Qualifikationen mit Ablaufwarnung auf der Übersicht
|
||||||
|
- Formular zum Anlegen und Bearbeiten eines einzelnen Geräts, mit
|
||||||
|
Fristenvorschau beim Ausfüllen
|
||||||
|
|
||||||
**Danach kann die Wehr:** ihren Bestand vollständig führen und jedes Gerät per
|
**Danach kann die Wehr:** ihren Bestand vollständig führen und jedes Gerät per
|
||||||
QR-Code am Regal wiederfinden.
|
QR-Code am Regal wiederfinden.
|
||||||
|
|
||||||
## Phase 2 — Wartung und Prüfung
|
## Phase 2 — Wartung und Prüfung · fertig
|
||||||
|
|
||||||
Das wichtigste Modul. Ab hier ersetzt FLORIAN den Ordner.
|
Das wichtigste Modul. Ab hier ersetzt FLORIAN den Ordner.
|
||||||
|
|
||||||
- Wartungsvorlagen mit Checklisten, Intervall und Vorwarnzeit
|
- Wartungsvorlagen mit Checklisten, Intervall und Vorwarnzeit
|
||||||
- Fristen-Dashboard: überfällig / diese Woche / diesen Monat / nächste 90 Tage
|
- Fristen-Dashboard: überfällig / diese Woche / diesen Monat / nächste 90 Tage,
|
||||||
- Nächtlicher Job erzeugt Termine, setzt Status, erinnert bei T-30, T-7 und T-0
|
gebündelt nach Fahrzeug, Standort, Gerätetyp oder Zuständigem
|
||||||
- Prüfungs-Wizard mit Foto je Checklistenpunkt, Zeiterfassung und Unterschrift
|
- Nächtlicher Lauf erzeugt Termine, zieht den Gerätestatus nach und erinnert bei
|
||||||
- PDF-Prüfprotokoll über Gotenberg, unveränderlich in MinIO
|
T-30, T-7, T-0 und einmal bei Überfälligkeit
|
||||||
|
- Prüfwizard mit Foto je Checklistenpunkt, Zeiterfassung und Unterschrift auf
|
||||||
|
dem Zeichenfeld
|
||||||
|
- PDF-Prüfprotokoll über Gotenberg, unveränderlich in MinIO — die Sperre setzt
|
||||||
|
ein Datenbanktrigger durch, nicht der Anwendungscode
|
||||||
|
- Korrekturen erzeugen ein neues Protokoll mit Verweis auf das alte
|
||||||
- Lebensdauerüberwachung mit Aussonderungsvorschlag
|
- Lebensdauerüberwachung mit Aussonderungsvorschlag
|
||||||
- Rückrufe markieren betroffene Geräte automatisch
|
- Rückrufe suchen betroffene Geräte über Bauart, Modell, Seriennummernbereich
|
||||||
- Mängel-Workflow: melden → zugewiesen → in Arbeit → behoben
|
und Herstellungszeitraum
|
||||||
|
- Mängel-Workflow: melden → zugewiesen → in Arbeit → behoben, mit
|
||||||
|
Gerätesperre bei kritischen Mängeln
|
||||||
|
- Externe Wartung: Fachfirma, Kosten und Fremdprotokoll als PDF
|
||||||
|
|
||||||
**Danach kann die Wehr:** einen DGUV-Nachweis führen, der einer Prüfung durch
|
**Danach kann die Wehr:** einen DGUV-Nachweis führen, der einer Prüfung durch
|
||||||
die Unfallkasse standhält.
|
die Unfallkasse standhält.
|
||||||
|
|
||||||
## Phase 3 — Einsätze und Übungen
|
## Phase 3 — Einsätze und Übungen · fertig
|
||||||
|
|
||||||
- Einsatz mit allen Zeitstempeln: alarmiert, ausgerückt, eingetroffen,
|
- Einsatz mit allen Zeitstempeln: alarmiert, ausgerückt, eingetroffen,
|
||||||
Einsatzende, eingerückt, einsatzbereit
|
Einsatzende, eingerückt, einsatzbereit
|
||||||
@@ -87,28 +99,35 @@ die Unfallkasse standhält.
|
|||||||
|
|
||||||
**Danach kann die Wehr:** ihre Jahresstatistik auf Knopfdruck erzeugen.
|
**Danach kann die Wehr:** ihre Jahresstatistik auf Knopfdruck erzeugen.
|
||||||
|
|
||||||
## Phase 4 — Kiosk-PWA
|
## Phase 4 — Kiosk-PWA · fertig
|
||||||
|
|
||||||
Für die Wandtablets in der Fahrzeughalle.
|
Für die Wandtablets in der Fahrzeughalle.
|
||||||
|
|
||||||
- Eigenes Layout: große Kacheln, mindestens 64px, mit Handschuhen bedienbar
|
- Eigenes Layout: große Kacheln, mindestens 64px, mit Handschuhen bedienbar
|
||||||
- Anmeldung per RFID, PIN oder QR vom Handy, Auto-Logout nach 90 Sekunden
|
- Anmeldung per RFID, PIN oder QR vom Handy, Abmeldung nach Untätigkeit
|
||||||
- Wizards: Wartung, Fahrtenbuch, Mangel melden, Inventur, Wäsche, Anwesenheit
|
(`KIOSK_IDLE_TIMEOUT`, Vorgabe 120 Sekunden)
|
||||||
|
- Wizards: Fahrtenbuch, Mangel melden, Inventur, Wäsche, Anwesenheit. Die
|
||||||
|
Prüfung bleibt beim Wizard aus Phase 2 — Checkliste, Fotos und Unterschrift
|
||||||
|
lassen sich nicht sinnvoll in eine Warteschlange legen; die Kachel sagt
|
||||||
|
„braucht Netz"
|
||||||
- Hallen-Monitor: Vollbild mit Terminen, überfälligen Prüfungen,
|
- Hallen-Monitor: Vollbild mit Terminen, überfälligen Prüfungen,
|
||||||
Fahrzeugstatus, Wetterwarnung
|
Fahrzeugstatus, Wetterwarnung
|
||||||
- Offline-Betrieb mit IndexedDB-Outbox und Synchronisation bei Reconnect
|
- Offline-Betrieb mit IndexedDB-Outbox, Idempotenzschlüssel und
|
||||||
|
Konfliktliste für alles, was sich beim Nachtragen nicht anwenden lässt
|
||||||
|
|
||||||
**Danach kann die Wehr:** einen Mangel in unter 30 Sekunden melden, ohne den
|
**Danach kann die Wehr:** einen Mangel in unter 30 Sekunden melden, ohne den
|
||||||
Rechner im Büro einzuschalten — auch wenn das Netz gerade weg ist.
|
Rechner im Büro einzuschalten — auch wenn das Netz gerade weg ist.
|
||||||
|
|
||||||
## Phase 5 — Atemschutz nach FwDV 7
|
## Phase 5 — Atemschutz nach FwDV 7 · fertig
|
||||||
|
|
||||||
Sicherheitsrelevant. Jede Ansicht zeigt dauerhaft den Hinweis, dass die
|
Sicherheitsrelevant. Jede Ansicht zeigt dauerhaft den Hinweis, dass die
|
||||||
analoge Überwachung führend bleibt — abschaltbar erst über eine Option, die
|
analoge Überwachung führend bleibt — abschaltbar erst über eine Option, die
|
||||||
eine dokumentierte Freigabe durch den Kommandanten voraussetzt.
|
eine dokumentierte Freigabe durch den Kommandanten voraussetzt.
|
||||||
|
|
||||||
- Truppanlage per RFID, Eingangsdruck erfassen
|
- Truppanlage per RFID, Eingangsdruck erfassen
|
||||||
- Serverseitig autoritative Timer, per Socket.IO verteilt
|
- Serverseitig autoritative Zeitstempel; verteilt über Server-Sent Events
|
||||||
|
statt Socket.IO — es geht nur in eine Richtung, und so bleibt es bei
|
||||||
|
`next start` ohne eigenen Node-Server
|
||||||
- Rückzugsdruck-Berechnung mit Ampel und akustischem Alarm
|
- Rückzugsdruck-Berechnung mit Ampel und akustischem Alarm
|
||||||
- Erinnerung zur Druckabfrage im konfigurierbaren Intervall
|
- Erinnerung zur Druckabfrage im konfigurierbaren Intervall
|
||||||
- Mayday-Knopf je Trupp
|
- Mayday-Knopf je Trupp
|
||||||
@@ -118,49 +137,106 @@ eine dokumentierte Freigabe durch den Kommandanten voraussetzt.
|
|||||||
**Danach kann die Wehr:** die Überwachung digital mitführen — parallel zur
|
**Danach kann die Wehr:** die Überwachung digital mitführen — parallel zur
|
||||||
analogen, bis die Wehr ausdrücklich anders entscheidet.
|
analogen, bis die Wehr ausdrücklich anders entscheidet.
|
||||||
|
|
||||||
## Phase 6 — Kartenmodule
|
## Phase 6 — Kartenmodule · fertig
|
||||||
|
|
||||||
- MapLibre GL mit selbst gehosteten Kacheln fürs Ausrückegebiet
|
- MapLibre GL mit selbst gehosteten Kacheln fürs Ausrückegebiet
|
||||||
- Hydranten mit Typ, Nennweite, Durchfluss, letzter Prüfung; Import aus
|
- Hydranten mit Typ, Nennweite, Durchfluss, letzter Prüfung; Import aus
|
||||||
GeoJSON und Stadtwerke-CSV
|
GeoJSON und Stadtwerke-CSV
|
||||||
- Objekte und Feuerwehrpläne nach DIN 14095, vom Einsatz aus erreichbar
|
- Objekte und Feuerwehrpläne nach DIN 14095, vom Einsatz aus erreichbar
|
||||||
- Lagekarte mit taktischen Zeichen nach FwDV 102, Echtzeit-Sync
|
- Lagekarte mit taktischen Zeichen nach FwDV 102, Abgleich über denselben
|
||||||
|
Ereignisstrom wie der Atemschutz
|
||||||
- Wasserförderung: Druckverlust, Verstärkerpumpen, druckbares Schema
|
- Wasserförderung: Druckverlust, Verstärkerpumpen, druckbares Schema
|
||||||
|
|
||||||
**Danach kann die Wehr:** auf der Anfahrt sehen, wo der nächste Hydrant liegt
|
**Danach kann die Wehr:** auf der Anfahrt sehen, wo der nächste Hydrant liegt
|
||||||
und was ihn hergibt.
|
und was ihn hergibt.
|
||||||
|
|
||||||
## Phase 7 — Benachrichtigungen und Termine
|
## Phase 7 — Benachrichtigungen und Termine · fertig
|
||||||
|
|
||||||
- Kanäle: E-Mail, Telegram, ntfy, Web-Push. WhatsApp bewusst nicht.
|
Die Wehr weiß, wann der nächste Dienst ist — ohne dass jemand eine Rundmail
|
||||||
- Vorlagen mit Platzhaltern, je Ereignis und Rolle konfigurierbar
|
tippt.
|
||||||
- Kalender: Dienste, Übungen, Lehrgänge, Arbeitsdienste; Serientermine,
|
|
||||||
Selbstanmeldung mit Teilnehmerlimit, Anwesenheitsliste zum Ausdrucken
|
- Terminkalender mit Dienst, Übung, Lehrgang, Arbeitsdienst, Veranstaltung und
|
||||||
- CalDAV-/ICS-Feed je Person
|
Sitzung. Serientermine werden ausgeschrieben, nicht als Regel gespeichert.
|
||||||
- Digitaler Dienstausweis mit signiertem QR und Prüfseite
|
- Selbstanmeldung mit Platzbegrenzung und einer Warteliste, die von selbst
|
||||||
|
nachrückt. Absage mit Grund, wahlweise für alle folgenden Termine einer Serie.
|
||||||
|
- Anwesenheitsliste über alle aktiven Mitglieder — zum Abhaken am Tablet oder
|
||||||
|
zum Ausdrucken. Zusage und Anwesenheit sind getrennt.
|
||||||
|
- Vier Kanäle hinter Schnittstellen: E-Mail (echtes SMTP statt Platzhalter),
|
||||||
|
ntfy (selbst gehostet), Telegram, Web-Push. Was das Netz der Wehr verlässt,
|
||||||
|
bekommt nur die Kurzfassung.
|
||||||
|
- Vorlagen je Ereignis und Kanal mit Platzhaltern, Vorschau und eingebautem
|
||||||
|
Standardtext für alles.
|
||||||
|
- ICS-Abonnement je Person, Token nur als Hash gespeichert, einzeln widerrufbar.
|
||||||
|
- Digitaler Dienstausweis: signierter QR, fünf Minuten gültig, Prüfseite ohne
|
||||||
|
Anmeldung.
|
||||||
|
|
||||||
|
**Danach kann die Wehr:** ihren Jahresplan führen, Rückmeldungen einsammeln,
|
||||||
|
Anwesenheit nachweisen und jedes Mitglied auf dem Weg erreichen, den es selbst
|
||||||
|
gewählt hat.
|
||||||
|
|
||||||
|
### Kein CalDAV
|
||||||
|
|
||||||
|
CalDAV ist ein Schreib-Lese-Protokoll mit Rechteverwaltung und
|
||||||
|
Konfliktauflösung. Gebraucht wird hier nur Lesen — die Termine entstehen in
|
||||||
|
FLORIAN, nicht im Kalender des Anwenders. Dafür genügt eine Adresse, die eine
|
||||||
|
ICS-Datei ausliefert; jeder Kalender kann das. CalDAV wäre ein Vielfaches an
|
||||||
|
Aufwand für dieselbe Wirkung.
|
||||||
|
|
||||||
### Warum kein WhatsApp
|
### Warum kein WhatsApp
|
||||||
|
|
||||||
Die WhatsApp Business API kostet je Konversation und verlangt einen
|
Die WhatsApp Business API kostet je Konversation und verlangt einen
|
||||||
zugelassenen Anbieter dazwischen — laufende Kosten für eine Wehr, die keinen
|
zugelassenen Anbieter dazwischen — laufende Kosten für eine Wehr, die keinen
|
||||||
Etat dafür hat. Der schwerere Grund ist der Datenschutz: die Nummern aller
|
Etat dafür hat. Der schwerere Grund ist der Datenschutz: die Nachrichten liefen
|
||||||
Mitglieder gingen an Meta, was sich mit einer Anwendung, die bewusst im
|
über Server von Meta, die Wehr müsste einen Vertrag zur Auftragsverarbeitung
|
||||||
Gerätehaus steht, nicht verträgt. Telegram, ntfy und Web-Push decken denselben
|
schließen, und die gewöhnliche App gleicht zusätzlich das Telefonbuch ab. Das
|
||||||
Bedarf ab, ntfy sogar vollständig selbst gehostet.
|
verträgt sich nicht mit einer Anwendung, die bewusst im Gerätehaus steht.
|
||||||
|
Telegram, ntfy und Web-Push decken denselben Bedarf ab, ntfy sogar vollständig
|
||||||
|
selbst gehostet.
|
||||||
|
|
||||||
## Phase 8 — Lokale KI
|
Wer seine Wehr trotzdem über WhatsApp erreichen will, hat den Weg schon: die
|
||||||
|
Gruppe, die es ohnehin gibt. Ein Link zum Termin hineinkopiert kostet nichts.
|
||||||
|
|
||||||
Alles gegen Ollama, kein externer Anbieter, komplett abschaltbar.
|
## Phase 8 — Lokale KI · fertig
|
||||||
|
|
||||||
- Diktat: Aufnahme im Browser → faster-whisper → Transkript im Berichtsfeld
|
Alles gegen selbst betriebene Modelle, kein externer Anbieter, komplett
|
||||||
- Berichtsassistent: aus Stichpunkten einen Entwurf, immer als Vorschlag
|
abschaltbar.
|
||||||
- Wiki mit pgvector: semantische Suche mit Quellenangabe
|
|
||||||
- Belegerfassung: Foto einer Quittung → Vorschlag für Betrag, Datum, Händler
|
|
||||||
|
|
||||||
Jede KI-Ausgabe ist gekennzeichnet und muss bestätigt werden, bevor sie in ein
|
- Wiki mit pgvector: Dienstvorschriften und Ausbildungsunterlagen als Markdown,
|
||||||
Dokument mit Nachweischarakter gelangt.
|
Wortsuche und semantische Suche nebeneinander, Antwort mit Fundstellenangabe
|
||||||
|
- Diktat: Aufnahme im Browser, faster-whisper, Transkript als Vorschlag
|
||||||
|
- Berichtsassistent: aus Stichpunkten ein Einsatzberichtsentwurf, immer mit
|
||||||
|
Übernehmen/Verwerfen
|
||||||
|
- Belegerfassung: Foto einer Quittung, Vorschlag für Betrag, Datum, Händler —
|
||||||
|
nicht Erkanntes bleibt leer statt geraten zu werden
|
||||||
|
- KI-Protokoll und Audit-Log-Ansicht nebeneinander
|
||||||
|
- Eigenes Postgres-Image mit PostGIS **und** pgvector
|
||||||
|
|
||||||
## Phase 9 — Kassenverwaltung
|
**Danach kann die Wehr:** ihr Wissen ablegen und befragen, Berichte diktieren
|
||||||
|
statt tippen und Belege fotografieren statt abzuschreiben — ohne dass eine
|
||||||
|
Zeile Text das Gerätehaus verlässt.
|
||||||
|
|
||||||
|
### Die Regel, die den Aufbau bestimmt
|
||||||
|
|
||||||
|
Jede KI-Ausgabe ist zuerst ein eigener Datensatz und nicht ein Feld im
|
||||||
|
Zieldokument. Sie ist gekennzeichnet, sie steht daneben statt mittendrin, und
|
||||||
|
sie braucht eine Entscheidung, bevor etwas mit ihr geschieht. Die Datenbank
|
||||||
|
hält gegen: Eingabe, Ausgabe und Modell sind unveränderlich, gelöscht wird
|
||||||
|
nicht, und über einen Vorschlag wird genau einmal entschieden.
|
||||||
|
|
||||||
|
Ohne das ließe sich in einem Jahr nicht mehr sagen, welcher Satz in einem
|
||||||
|
Einsatzbericht von einem Menschen stammt.
|
||||||
|
|
||||||
|
### Kein Rückfall auf einen Anbieter
|
||||||
|
|
||||||
|
Fehlen die Container, fehlt die Funktion — sie wird nicht durch einen fremden
|
||||||
|
Dienst ersetzt. Das ist die einzige Bauart, bei der der Satz „die Daten bleiben
|
||||||
|
im Haus" auch dann noch stimmt, wenn der eigene Server aus ist.
|
||||||
|
|
||||||
|
Dafür bleibt **alles andere ohne Modell vollständig benutzbar**: Wiki lesen und
|
||||||
|
schreiben, Wortsuche, Belege ablegen, beide Protokolle. Wo etwas fehlt, steht
|
||||||
|
ein Satz, der sagt was fehlt und wie es ohne geht.
|
||||||
|
|
||||||
|
## Phase 9 — Kassenverwaltung · fertig
|
||||||
|
|
||||||
Am 03.09.2026 ergänzt. Die Rolle `KASSIER` war seit Phase 0 angelegt, ein Modul
|
Am 03.09.2026 ergänzt. Die Rolle `KASSIER` war seit Phase 0 angelegt, ein Modul
|
||||||
dazu fehlte in der ursprünglichen Planung. Spezifikation: `Phase-9.md`.
|
dazu fehlte in der ursprünglichen Planung. Spezifikation: `Phase-9.md`.
|
||||||
@@ -179,20 +255,92 @@ Kassenabschluss friert seinen Zeitraum ein, danach nimmt die Buchungsmaske
|
|||||||
kein Datum daraus mehr an. Beträge durchgängig als `DECIMAL(12,2)`, nie als
|
kein Datum daraus mehr an. Beträge durchgängig als `DECIMAL(12,2)`, nie als
|
||||||
Fließkommazahl.
|
Fließkommazahl.
|
||||||
|
|
||||||
**Danach kann die Wehr:** ihre Kasse führen und den Kassenprüfbericht auf
|
Beides sitzt in Triggern und nicht im Router: ein vergessener Zustandscheck,
|
||||||
|
ein Skript oder ein Import würden sonst reichen, um den geprüften Bestand zu
|
||||||
|
ändern, ohne dass irgendwo etwas auffällt.
|
||||||
|
|
||||||
|
**Die Wehr kann jetzt:** ihre Kasse führen und den Kassenprüfbericht auf
|
||||||
Knopfdruck erzeugen.
|
Knopfdruck erzeugen.
|
||||||
|
|
||||||
## 1.0.0 — Freigabe
|
## Phase 10 — Mandantenfähigkeit · fertig
|
||||||
|
|
||||||
|
Am 05.09.2026 ergänzt. Datenmodell und RLS trugen seit Phase 0 mehrere Wehren;
|
||||||
|
was fehlte, war alles darüber.
|
||||||
|
|
||||||
|
- `Benutzer` und `Mitgliedschaft`: das Anmeldekonto ist von der Personenakte
|
||||||
|
gelöst. Ein Mensch, ein Passwort, beliebig viele Wehren.
|
||||||
|
- Wehrwähler in der Seitenleiste — er erscheint nur, wenn es etwas zu wählen
|
||||||
|
gibt.
|
||||||
|
- Die Rolle `AUFSICHT` für die Kreisebene: Kreisbrandrat und
|
||||||
|
Kreisbrandinspektion sehen die betreuten Wehren und ändern nichts.
|
||||||
|
- Verwaltung der Installation unter `/wehren`: Wehren anlegen, Konten führen,
|
||||||
|
Zuordnungen vergeben.
|
||||||
|
|
||||||
|
Die Entscheidung, die alles andere bestimmt: **das Konto trägt keine
|
||||||
|
Mandantenkennung.** Es kann keine tragen — es ist ja der Angelpunkt, an dem
|
||||||
|
ein Mensch zu mehreren Wehren gehört. Geschützt wird es deshalb wie
|
||||||
|
`Berechtigung` über Rechte statt über Row Level Security, und was ein Konto in
|
||||||
|
einer Wehr darf, entscheidet nicht es selbst, sondern seine `Mitgliedschaft` —
|
||||||
|
und die trägt `organisationId` wie jede andere Tabelle.
|
||||||
|
|
||||||
|
**Bewusst nicht dabei:** wehrübergreifende Auswertungen. Eine Zahl über
|
||||||
|
mehrere Wehren hinweg braucht eine abgestimmte Definition, und ohne die ist
|
||||||
|
sie schlimmer als keine. Die Aufsicht sieht jede Wehr einzeln.
|
||||||
|
|
||||||
|
**Die Wehr kann jetzt:** sich einen Server mit anderen teilen, ohne dass
|
||||||
|
jemand über die Grenze sieht — und die Kreisebene kann hineinschauen, ohne
|
||||||
|
etwas anfassen zu können.
|
||||||
|
|
||||||
|
## Phase 11 — Freigabereife · fertig
|
||||||
|
|
||||||
|
Am 05.09.2026 umgesetzt. Kein Modul, sondern die drei Software-Voraussetzungen
|
||||||
|
für die Freigabe.
|
||||||
|
|
||||||
|
- **Auskunft nach Art. 15 DSGVO.** Das Recht `person.export` stand seit Phase 0
|
||||||
|
im Katalog und war zugewiesen — umgesetzt war nichts. Ohne Auskunftsseite ist
|
||||||
|
ein Löschkonzept nicht abnehmbar.
|
||||||
|
- **Sicherung und Wiederherstellung** mit einer durchgeführten Rückspielprobe.
|
||||||
|
- **Pipeline** in Gitea Actions: typecheck, lint, Migrationen, Seed und Tests.
|
||||||
|
|
||||||
|
Die Rückspielprobe hat sich sofort bezahlt gemacht: der erste Versuch lief
|
||||||
|
sauber durch und hatte dabei sämtliche Unveränderlichkeitsgarantien der
|
||||||
|
Datenbank entfernt. Gefunden haben es die Datenbanktests unmittelbar danach —
|
||||||
|
also genau die Probe, wegen der 1.0.0 sie verlangt.
|
||||||
|
|
||||||
|
**Die Wehr kann jetzt:** eine Selbstauskunft erteilen, ihren Bestand sichern
|
||||||
|
und ihn nachweislich zurückholen.
|
||||||
|
|
||||||
|
## 1.0.0 — Freigabe · am 06.09.2026 erteilt
|
||||||
|
|
||||||
Kein neues Modul, sondern eine Entscheidung: die Wehr setzt FLORIAN im
|
Kein neues Modul, sondern eine Entscheidung: die Wehr setzt FLORIAN im
|
||||||
Echtbetrieb ein. Voraussetzungen:
|
Echtbetrieb ein.
|
||||||
|
|
||||||
- Datenübernahme aus der bisherigen Verwaltung abgeschlossen und abgeglichen
|
**Damit das nicht zur Formalie wird, hier der Stand jeder Voraussetzung — auch
|
||||||
- Sicherungskonzept steht und ist einmal wiederhergestellt worden
|
der offenen.** Eine Freigabe, die alles abhakt, was ohnehin nicht geprüft
|
||||||
- Schulung von Kommandant, Gerätewart und Atemschutzgerätewart erfolgt
|
wurde, ist keine.
|
||||||
- Löschkonzept nach DSGVO abgenommen
|
|
||||||
- Der Hinweis zur analogen Atemschutzüberwachung wird bewusst und
|
| Voraussetzung | Stand |
|
||||||
dokumentiert abgeschaltet — oder bewusst stehen gelassen
|
|---|---|
|
||||||
|
| Datenübernahme aus der bisherigen Verwaltung | **entfällt.** Die Wehr pflegt den Bestand von Hand ein; es gibt kein Altsystem, aus dem übernommen würde. Für Mitglieder, Fahrzeuge und Geräte steht der Import-Assistent bereit, falls sich das ändert. |
|
||||||
|
| Sicherungskonzept steht und ist einmal wiederhergestellt worden | **erledigt** mit Phase 11. Die Probe hat dabei einen Fehler gefunden, der sonst erst im Notfall aufgefallen wäre. |
|
||||||
|
| Löschkonzept nach DSGVO | **technisch vollständig** seit Phase 11: Auskunft nach Art. 15, Anonymisierung im Aufbewahrungslauf, konfigurierbare Fristen. Die Abnahme durch die Wehr ist ein organisatorischer Schritt und steht aus. |
|
||||||
|
| Schulung von Kommandant, Gerätewart und Atemschutzgerätewart | **steht aus.** Sie läuft nach der Freigabe; Grundlage ist die [Kurzanleitung](KURZANLEITUNG.md). |
|
||||||
|
| Hinweis zur analogen Atemschutzüberwachung | **bewusst stehen gelassen.** `ATEMSCHUTZ_HINWEIS=true`. FLORIAN bleibt damit ergänzend; die Tafel am Fahrzeug ist führend. Das Abschalten verlangt ohnehin einen Kommandantenbeschluss samt Fundstelle. |
|
||||||
|
|
||||||
|
Die beiden offenen Punkte sind **organisatorisch, nicht technisch**: an der
|
||||||
|
Software hängt nichts davon ab. Sie stehen hier, damit später nachlesbar ist,
|
||||||
|
was zum Zeitpunkt der Freigabe erledigt war und was nicht.
|
||||||
|
|
||||||
|
### Was 1.0.0 bedeutet — und was nicht
|
||||||
|
|
||||||
|
**Bedeutet:** Der Funktionsumfang ist vollständig, die Datenbank ist stabil,
|
||||||
|
Migrationen sind aufwärtskompatibel, und ab hier gilt semantische
|
||||||
|
Versionierung. Ein Update bricht nichts, ohne dass es in der Hauptnummer
|
||||||
|
steht.
|
||||||
|
|
||||||
|
**Bedeutet nicht:** dass keine Fehler mehr da sind. Es bedeutet, dass die
|
||||||
|
Wehr echte Daten hineingeben kann, ohne befürchten zu müssen, sie beim
|
||||||
|
nächsten Update zu verlieren.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -202,8 +350,11 @@ Am 03.09.2026 entschieden:
|
|||||||
|
|
||||||
| Punkt | Entscheidung |
|
| Punkt | Entscheidung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Kassenmodul | Wird als **Phase 9** ergänzt. Spezifikation in `Phase-9.md`, Aufwand 8–11 PT. |
|
| Kassenmodul | Als **Phase 9** ergänzt und mit 0.10.0 umgesetzt. Spezifikation in `Phase-9.md`. |
|
||||||
| `Berechtigung` ohne `organisationId` | Bleibt so. Die Tabelle ist der systemweite Katalog dessen, was die Software kann, für alle Wehren identisch — mandantenbezogen wird es erst über `RolleBerechtigung`. Für die Anwendungsrolle nur lesbar. |
|
| `Berechtigung` ohne `organisationId` | Bleibt so. Die Tabelle ist der systemweite Katalog dessen, was die Software kann, für alle Wehren identisch — mandantenbezogen wird es erst über `RolleBerechtigung`. Für die Anwendungsrolle nur lesbar. |
|
||||||
|
| `Benutzer` ohne `organisationId` | Notwendige Ausnahme, seit Phase 10. Die Tabelle ist der Angelpunkt der Doppelmitgliedschaft — mit Mandantenkennung könnte sie das nicht sein. Geschützt über Rechte statt RLS, wie `Berechtigung`: die Anwendungsrolle darf lesen und die Anmeldefelder fortschreiben, mehr nicht. |
|
||||||
|
| `AUFSICHT` nicht im Prisma-Enum | Die Kreisebene ergibt sich aus der Mitgliedschaftsart, nicht aus einer Rollenzuweisung. Vergebbar zu machen hiesse, dass jemand innerhalb einer Wehr Rechte an einer anderen verteilen könnte. |
|
||||||
|
| `istVerwalter` als Kontomerkmal | Kein Recht. Rechte gelten je Wehr; wer Wehren anlegt, steht über allen. Ein Recht, das in Wehr A vergeben wird und in Wehr B wirkt, wäre die Hintertür, die Phase 10 vermeiden soll. |
|
||||||
| Löschausnahme im Audit-Log | Bleibt so. `UPDATE` ist dauerhaft gesperrt, `DELETE` nur mit `SET LOCAL app.audit_bereinigung = 'an'` im Aufbewahrungslauf des Workers. Ohne diese Ausnahme wäre das Löschkonzept nach DSGVO nicht umsetzbar. |
|
| Löschausnahme im Audit-Log | Bleibt so. `UPDATE` ist dauerhaft gesperrt, `DELETE` nur mit `SET LOCAL app.audit_bereinigung = 'an'` im Aufbewahrungslauf des Workers. Ohne diese Ausnahme wäre das Löschkonzept nach DSGVO nicht umsetzbar. |
|
||||||
|
|
||||||
## Weitere Kandidaten für später
|
## Weitere Kandidaten für später
|
||||||
@@ -214,10 +365,8 @@ Am 03.09.2026 entschieden:
|
|||||||
| Kleiderkammer und Wäscheverwaltung | in Phase 4 nur als Wizard, ohne Bestandsführung | 4–6 PT |
|
| Kleiderkammer und Wäscheverwaltung | in Phase 4 nur als Wizard, ohne Bestandsführung | 4–6 PT |
|
||||||
| Jugendfeuerwehr mit eigener Abzeichenverwaltung | eigene Fachlichkeit | 5–7 PT |
|
| Jugendfeuerwehr mit eigener Abzeichenverwaltung | eigene Fachlichkeit | 5–7 PT |
|
||||||
| Schnittstelle zur Integrierten Leitstelle | Alarmdaten automatisch übernehmen | 6–10 PT, stark abhängig von der ILS |
|
| Schnittstelle zur Integrierten Leitstelle | Alarmdaten automatisch übernehmen | 6–10 PT, stark abhängig von der ILS |
|
||||||
| Mehrere Wehren je Installation | technisch bereits möglich, Oberfläche fehlt | 4–6 PT |
|
|
||||||
|
|
||||||
Die Mandantenfähigkeit ist der Punkt, der ohne Zusatzaufwand am weitesten
|
Die Mandantenfähigkeit ist mit **Phase 10** umgesetzt: umschalten, Kreisebene,
|
||||||
trägt: das Datenmodell und RLS können bereits mehrere Wehren. Was fehlt, ist
|
Doppelmitgliedschaft, Verwaltung. Offen geblieben sind bewusst die
|
||||||
eine Oberfläche zum Umschalten und ein Konzept für gemeindeübergreifende
|
gemeindeübergreifenden Auswertungen — dafür braucht es zuerst eine abgestimmte
|
||||||
Auswertungen. Für einen Landkreis, der mehrere Wehren betreiben will, wäre das
|
Definition dessen, was verglichen werden soll.
|
||||||
der nächste sinnvolle Schritt.
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 224 KiB |
|
Before Width: | Height: | Size: 189 KiB After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 413 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 430 KiB |
|
After Width: | Height: | Size: 438 KiB |
|
After Width: | Height: | Size: 803 KiB |
|
After Width: | Height: | Size: 824 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 323 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 332 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 139 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 336 KiB |
|
After Width: | Height: | Size: 338 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 263 KiB |
|
After Width: | Height: | Size: 789 KiB |
|
After Width: | Height: | Size: 807 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 193 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 125 KiB |
|
After Width: | Height: | Size: 291 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 400 KiB |
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
After Width: | Height: | Size: 132 KiB |
|
After Width: | Height: | Size: 230 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 175 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 246 KiB |
|
After Width: | Height: | Size: 628 KiB |
|
After Width: | Height: | Size: 639 KiB |
|
After Width: | Height: | Size: 262 KiB |
|
After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 234 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 171 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 174 KiB |