Wiki mit semantischer Suche ueber pgvector, Diktat ueber faster-whisper, Berichtsassistent und Belegerfassung — alles gegen selbst betriebene Modelle. Dazu zwei Protokollansichten: was die Maschine getan hat und was Menschen getan haben. Nichts, was ein Modell erzeugt, gelangt ungeprueft in einen Nachweis. Jede Ausgabe ist zuerst ein eigener Datensatz, gekennzeichnet, mit zwei gleich grossen Knoepfen davor. Die Datenbank haelt gegen: Eingabe, Ausgabe und Modell sind unveraenderlich, geloescht wird nicht, entschieden wird einmal. Kein Rueckfall auf einen Anbieter: fehlen die Container, fehlt die Funktion. Dafuer bleibt alles andere ohne Modell vollstaendig benutzbar — geprueft von vierzehn E2E-Tests, die ohne laufendes Modell durchlaufen. Postgres bringt jetzt PostGIS und pgvector mit (eigenes Image), Ollama und faster-whisper stehen im Compose-Profil "ki". 73 neue Unit-Tests, 14 neue E2E-Tests, Version 0.9.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1232 lines
53 KiB
Markdown
1232 lines
53 KiB
Markdown
# FLORIAN — Feuerwehr-Verwaltungsplattform
|
||
|
||
Selbstgehostete Verwaltungssoftware für Freiwillige Feuerwehren. Läuft im
|
||
Gerätehaus, nicht in der Cloud. Keine Telemetrie, keine externen CDNs.
|
||
|
||
## Schnellstart
|
||
|
||
```bash
|
||
git clone https://git.wilde-server.de/florian.wilde/FireAdministration.git && cd FireAdministration
|
||
cp .env.example .env # Passwörter und AUTH_SECRET ersetzen
|
||
npm ci # Abhängigkeiten
|
||
docker compose up -d # Postgres, Redis, MinIO, Gotenberg
|
||
npm run db:migrate && npm run db:seed # Schema und Beispielwehr
|
||
npm run dev # http://localhost:3100
|
||
```
|
||
|
||
Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
||
(Vorgabe: `admin@beispielwehr.de` / `Feuerwehr2026`). Passwort danach ändern.
|
||
|
||
---
|
||
|
||
## Was drin ist
|
||
|
||
| Bereich | Stand |
|
||
|---|---|
|
||
| Phase 0 — Fundament | fertig |
|
||
| Phase 1 — Stammdaten | fertig |
|
||
| Phase 2 — Wartung und Prüfung | fertig |
|
||
| Phase 3 — Einsätze und Übungen | fertig |
|
||
| Phase 4 — Kiosk-PWA im Gerätehaus | fertig |
|
||
| Phase 5 — Atemschutz (FwDV 7) | fertig |
|
||
| Phase 6 — Kartenmodule | fertig |
|
||
| Phase 7 — Benachrichtigungen und Termine | fertig |
|
||
| Phase 8 — Lokale KI | fertig |
|
||
| Phase 9 — Kassenverwaltung | offen |
|
||
|
||
Vollständige Planung mit Aufwänden: [docs/ROADMAP.md](docs/ROADMAP.md) und
|
||
[docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md).
|
||
|
||
## Weiterführende Dokumentation
|
||
|
||
| Datei | Inhalt |
|
||
|---|---|
|
||
| [docs/ANLEITUNG.md](docs/ANLEITUNG.md) | Bedienung mit Bildschirmfotos |
|
||
| [docs/ROADMAP.md](docs/ROADMAP.md) | Phasenplan und was jeweils entsteht |
|
||
| [docs/AUFWANDSSCHAETZUNG.md](docs/AUFWANDSSCHAETZUNG.md) | Aufwand je Phase, Schätzung gegen Ist |
|
||
| [docs/DEVLOG.md](docs/DEVLOG.md) | Entwicklungstagebuch mit Zeiterfassung |
|
||
| [docs/VERSIONIERUNG.md](docs/VERSIONIERUNG.md) | Versions- und Branch-Regeln |
|
||
| [CHANGELOG.md](CHANGELOG.md) | Was sich wann geändert hat |
|
||
|
||
---
|
||
|
||
## Phase 0 — Fundament
|
||
|
||
### Aufbau
|
||
|
||
```
|
||
FireAdministration/
|
||
├─ docker/ Dockerfile, Entrypoint, Postgres-Initialisierung
|
||
├─ prisma/ Schema, Migrationen, Seed
|
||
├─ scripts/ Hilfsskripte (Bildschirmfotos)
|
||
├─ src/
|
||
│ ├─ app/ Next.js App Router
|
||
│ │ ├─ (app)/ alles, was Anmeldung verlangt
|
||
│ │ ├─ anmelden/ Anmeldeseite
|
||
│ │ └─ api/ Auth.js, tRPC, Health
|
||
│ ├─ components/ shadcn/ui-Bausteine und Layout
|
||
│ ├─ lib/ authz, db, storage, audit, env, auth, warteschlange
|
||
│ ├─ server/ tRPC-Router und Kontext
|
||
│ └─ worker/ BullMQ-Hintergrundprozess
|
||
├─ tests/ Vitest (unit) und Playwright (e2e)
|
||
└─ docs/ Anleitung, Roadmap, Devlog, Aufwände
|
||
```
|
||
|
||
### Mandantentrennung
|
||
|
||
Jede Tabelle trägt `organisationId`, und Postgres setzt die Trennung mit Row
|
||
Level Security durch. Es gibt zwei Datenbankverbindungen:
|
||
|
||
- **`florian_app`** (`DATABASE_URL`) — RLS aktiv, `NOBYPASSRLS`. Über diese
|
||
Verbindung arbeitet die Anwendung. Ohne gesetztes
|
||
`app.current_organisation` liefert jede Abfrage **null Zeilen**.
|
||
- **Eigentümer** (`DIRECT_DATABASE_URL`) — umgeht RLS. Nur für Migrationen,
|
||
Seed, Anmeldung (die E-Mail-Suche muss über alle Wehren gehen) und
|
||
Wartungsjobs.
|
||
|
||
Fachdaten laufen ausschließlich über `fuerOrganisation()` aus
|
||
[`src/lib/db.ts`](src/lib/db.ts):
|
||
|
||
```ts
|
||
const personen = await fuerOrganisation(orgId, (db) =>
|
||
db.person.findMany({ where: { aktiv: true } }),
|
||
);
|
||
```
|
||
|
||
Ein vergessener Mandantenfilter kostet damit im schlimmsten Fall ein leeres
|
||
Ergebnis — nicht die Daten einer fremden Wehr.
|
||
`tests/unit/mandantentrennung.test.ts` prüft das gegen eine echte Datenbank.
|
||
|
||
### Audit-Log
|
||
|
||
Jede schreibende Operation erzeugt einen Eintrag mit Akteur, Zeitpunkt,
|
||
Entität und Vorher/Nachher als JSONB. Die Tabelle ist append-only:
|
||
|
||
- `UPDATE` ist per Trigger **immer** gesperrt.
|
||
- `DELETE` nur im gekennzeichneten Aufbewahrungslauf
|
||
(`SET LOCAL app.audit_bereinigung = 'an'`), den ausschließlich der Worker
|
||
ausführt — nachdem er exportiert hat. Ohne diese Ausnahme ließe sich das
|
||
Löschkonzept nach DSGVO nicht umsetzen.
|
||
|
||
Geheimnisse (Passwort-Hashes, PINs, Token) werden vor dem Schreiben entfernt.
|
||
|
||
### Berechtigungen
|
||
|
||
Alles in [`src/lib/authz.ts`](src/lib/authz.ts) — keine Rollenprüfung in
|
||
Komponenten. 9 Rollen, 38 Einzelrechte. Der Seed spiegelt den Katalog in die
|
||
Datenbank, damit eine Wehr Rechte nachträglich zuschneiden kann, ohne dass
|
||
jemand Code ändert.
|
||
|
||
```ts
|
||
export const geraetSpeichern = mitRecht("geraet.bearbeiten").mutation(/* … */);
|
||
```
|
||
|
||
### Anmeldung
|
||
|
||
Drei Wege, eine Sitzung:
|
||
|
||
1. **OIDC gegen authentik** — der Regelweg. authentik wird **separat**
|
||
betrieben und ist nicht Teil dieses Stacks. Im Provider einzutragen:
|
||
Redirect-URI `${APP_URL}/api/auth/callback/authentik`, Scopes
|
||
`openid profile email`.
|
||
2. **E-Mail und Passwort** — Rückfalloption für den Erststart und für den Fall,
|
||
dass authentik nicht erreichbar ist. Über `AUTH_LOCAL_ENABLED` abschaltbar.
|
||
3. **Terminal (PIN oder RFID)** — für das Kiosk-Gerät im Gerätehaus. Zwei
|
||
Schranken: erst weist sich das *Gerät* mit `KIOSK_DEVICE_SECRET` aus, dann
|
||
die *Person* mit PIN oder Karte. Die vollständige Oberfläche dazu kommt in
|
||
Phase 4.
|
||
|
||
Rollen aus authentik-Gruppen wirken als **Vorbelegung beim ersten Login**
|
||
(`AUTH_OIDC_GROUP_MAPPING`). Sobald jemand die Rolle in FLORIAN ändert, ist
|
||
FLORIAN führend und OIDC überschreibt nicht mehr.
|
||
|
||
### Dateien
|
||
|
||
Alles liegt in MinIO, nie im Dateisystem und nie in der Datenbank. Zugriff
|
||
ausschließlich über [`src/lib/storage.ts`](src/lib/storage.ts). Objektschlüssel:
|
||
|
||
```
|
||
<wehr-slug>/<entitaet>/<entitaetId>/<uuid>-<dateiname>
|
||
```
|
||
|
||
Downloads laufen über presignte Links mit begrenzter Gültigkeit
|
||
(`S3_URL_TTL`, Vorgabe 15 Minuten).
|
||
|
||
---
|
||
|
||
## Phase 2 — Wartung und Prüfung
|
||
|
||
Das Modul, wegen dem eine Wehr so ein Programm überhaupt einführt: Prüffristen
|
||
im Blick behalten und im Streitfall nachweisen können, dass geprüft wurde.
|
||
|
||
### Der Weg durch das Modul
|
||
|
||
```
|
||
Wartungsvorlage was an einem Gerätetyp in welchem Takt geprüft wird
|
||
└─ Checklistenpunkt die einzelne Frage: ja/nein, Messwert, Text, Foto
|
||
↓ nächtlicher Lauf
|
||
Wartungstermin ein fälliger Termin an einem konkreten Gerät
|
||
↓ Prüfwizard
|
||
Pruefung durchgeführte Prüfung, unterschrieben, festgeschrieben
|
||
├─ Pruefergebnis je Checklistenpunkt eine Antwort
|
||
├─ Signatur Unterschrift aus dem Zeichenfeld, PNG in MinIO
|
||
└─ Mangel was aufgefallen ist und behoben werden muss
|
||
```
|
||
|
||
Ohne Vorlage gibt es keine Termine, ohne Termine kein Fristen-Dashboard. Die
|
||
erste Seite, die eine Wehr in Phase 2 ausfüllt, ist deshalb
|
||
`/pruefungen/vorlagen`.
|
||
|
||
### Das Protokoll ist unveränderbar
|
||
|
||
Ein abgeschlossenes Prüfprotokoll ist der Nachweis gegenüber der Unfallkasse.
|
||
Wäre es nachträglich änderbar, wäre es als Nachweis wertlos. Die Sperre sitzt
|
||
deshalb in der Datenbank (Trigger `pruefung_unveraenderlich`, Migration
|
||
`20260903000300_wartung_rls`) und nicht nur im Anwendungscode — ein vergessener
|
||
Zustandscheck in einem Router würde sonst reichen, um sie auszuhebeln.
|
||
|
||
Nach dem Abschluss sind genau drei Änderungen zulässig, und alle drei ergänzen
|
||
das Protokoll, statt es zu verfälschen:
|
||
|
||
| Änderung | Warum |
|
||
|---|---|
|
||
| `ABGESCHLOSSEN` → `ERSETZT` | eine Korrekturprüfung ist nachgefolgt |
|
||
| `protokollDateiId`, solange leer | das PDF entsteht im Hintergrund und wird nachgetragen |
|
||
| `geaendertAm` | setzt Prisma bei jedem Schreibvorgang mit |
|
||
|
||
Dasselbe gilt für die Bestandteile: an einem abgeschlossenen Protokoll lassen
|
||
sich weder Einzelergebnisse ändern noch Unterschriften nachtragen. **Korrekturen
|
||
erzeugen eine neue Prüfung** mit Verweis auf die alte; beide bleiben lesbar.
|
||
|
||
Gelöscht wird ein Protokoll nie — außer im ausdrücklich gekennzeichneten
|
||
Aufbewahrungslauf, demselben Schalter wie beim Audit-Log
|
||
(`SET LOCAL app.audit_bereinigung = 'an'`).
|
||
|
||
### Der nächtliche Lauf
|
||
|
||
Der Worker trägt sich beim Start je Wehr zwei wiederkehrende Aufträge ein:
|
||
|
||
| Auftrag | Vorgabe | Was er tut |
|
||
|---|---|---|
|
||
| `fristen.pruefen` | 03:15 Uhr | Termine aus den Vorlagen planen, Frist und Status an jedem Gerät nachziehen |
|
||
| `erinnerung.senden` | 03:45 Uhr | Vorwarnungen zu offenen Terminen verschicken |
|
||
|
||
Beide sind wiederholbar: doppelte Termine verhindert die Eindeutigkeit über
|
||
Gerät, Vorlage und Fälligkeitstag, doppelte Mails ein Erinnerungseintrag je
|
||
Termin und Stufe. Erinnert wird bei T-30 (bzw. der Vorwarnzeit der Vorlage),
|
||
T-7, T-0 und einmal bei Überfälligkeit — als **eine** Sammelnachricht je
|
||
Empfänger, nicht als eine Mail je Gerät.
|
||
|
||
„Überfällig" ist bewusst kein gespeicherter Zustand, sondern wird aus dem
|
||
Fälligkeitsdatum und dem heutigen Tag gerechnet. Gespeicherte Zustände laufen
|
||
der Uhr hinterher; berechnete nicht.
|
||
|
||
### Prüfwizard
|
||
|
||
Zugeschnitten auf das Tablet im Gerätehaus: ein Prüfpunkt je Bildschirm, große
|
||
Schaltflächen, Foto direkt aus der Kamera, Unterschrift auf einem Zeichenfeld,
|
||
das Finger, Stift und Maus gleichermaßen versteht.
|
||
|
||
Jede Antwort geht sofort zum Server. Bricht das WLAN weg, ist höchstens die
|
||
letzte Antwort verloren, nicht die ganze Prüfung — beim erneuten Öffnen wird der
|
||
Entwurf fortgesetzt.
|
||
|
||
Die Bewertung rechnet der Server, nicht der Browser
|
||
([`src/lib/wartung/bewertung.ts`](src/lib/wartung/bewertung.ts)):
|
||
|
||
- ein Messwert außerhalb des zulässigen Bereichs gilt als beanstandet,
|
||
- ein beanstandeter **Pflichtpunkt** bedeutet „nicht bestanden" und sperrt das
|
||
Gerät,
|
||
- ein beanstandeter Kann-Punkt bedeutet „bestanden mit Mängeln".
|
||
|
||
Das Ergebnis ist ein Vorschlag; die Entscheidung trifft der Prüfer, denn er
|
||
trägt die Verantwortung.
|
||
|
||
### Prüfprotokoll als PDF
|
||
|
||
Erzeugt der Worker über Gotenberg
|
||
([`src/lib/wartung/protokoll.ts`](src/lib/wartung/protokoll.ts)) und legt es in
|
||
MinIO ab. Kopf mit Wehr, Gerät und Prüfer-Kennung, die Checkliste mit Soll, Ist
|
||
und Befund, eingebettete Fotos und Unterschriften, Fußzeile mit Prüfvorschrift,
|
||
Prüfdatum und Seitenzahl.
|
||
|
||
Alles ist eingebettet — keine externen Schriften, keine nachgeladenen Bilder.
|
||
Ein Protokoll muss in zehn Jahren noch genauso aussehen wie heute.
|
||
|
||
Die laufende Protokollnummer (`JJJJ-NNNN`) wird je Wehr und Jahr unter einer
|
||
Transaktionssperre vergeben; zwei gleichzeitig abschließende Prüfer bekommen
|
||
verschiedene Nummern.
|
||
|
||
### Mängel und Rückrufe
|
||
|
||
Der Mängelweg ist immer derselbe: melden → zugewiesen → in Arbeit → behoben.
|
||
**Melden darf jedes Mitglied** — der Mangel fällt dem auf, der das Gerät
|
||
benutzt, nicht dem Gerätewart am Schreibtisch. Ein kritischer Mangel sperrt das
|
||
Gerät sofort; beim Beheben wird die Sperre wieder aufgehoben, aber nur, wenn
|
||
kein anderer offener Mangel dasselbe Gerät sperrt.
|
||
|
||
Bei einem Rückruf sucht FLORIAN anhand von Bauart, Modell, Seriennummernbereich
|
||
und Herstellungszeitraum nach betroffenen Geräten. Das Ergebnis ist ein
|
||
**Vorschlag**, keine automatische Sperre: eine Chargenangabe des Herstellers
|
||
passt selten eins zu eins auf die Seriennummern im eigenen Bestand.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `WARTUNG_NACHTLAUF` | `15 3 * * *` | wann Termine geplant werden (cron, Europe/Berlin) |
|
||
| `WARTUNG_ERINNERUNG_LAUF` | `45 3 * * *` | wann Erinnerungen verschickt werden |
|
||
| `WARTUNG_PLANUNGSHORIZONT_TAGE` | `180` | wie weit im Voraus Termine angelegt werden |
|
||
|
||
|
||
---
|
||
|
||
## Phase 3 — Einsätze und Übungen
|
||
|
||
Was die Wehr tut, wird hier dokumentiert — für die Gemeinde, für die
|
||
Versicherung und für die eigene Mannschaft.
|
||
|
||
### Der Weg durch das Modul
|
||
|
||
```
|
||
Schnellerfassung Stichwort, Ort, Fahrzeuge — im Alarmfall in 20 Sekunden
|
||
↓ während des Einsatzes
|
||
Zeitstempel alarmiert, ausgerückt, eingetroffen, Ende, eingerückt,
|
||
einsatzbereit — je ein Knopf
|
||
Lagemeldungen aus der Wehr und über das Portal einer Fremdwehr
|
||
↓ danach, in Ruhe
|
||
Kräfte und Material wer war dabei, was wurde verbraucht
|
||
Bericht der Text des Einsatzleiters
|
||
↓ Freigabe
|
||
Einsatzbericht als PDF, festgeschrieben, an die Gemeinde
|
||
```
|
||
|
||
### Die sechs Zeitstempel
|
||
|
||
Sie sind der Kern jeder Einsatzauswertung. Aus ihnen ergeben sich drei Zahlen:
|
||
|
||
| Kennzahl | Rechnung | sagt etwas aus über |
|
||
|---|---|---|
|
||
| **Ausrückzeit** | Alarm bis Ausrücken | die Wehr selbst |
|
||
| **Anfahrtszeit** | Ausrücken bis Eintreffen | Lage und Wegstrecke |
|
||
| **Hilfsfrist** | Alarm bis Eintreffen | das, wonach die Gemeinde fragt |
|
||
|
||
In Bayern gilt als Ziel: **in 80 Prozent der Fälle innerhalb von zehn Minuten**.
|
||
Genau dieser Wert steht in der Statistik — nicht der Durchschnitt, denn der
|
||
verdeckt die Ausreißer, um die es geht.
|
||
|
||
### Stärke nach FwDV 3
|
||
|
||
Aus den Funktionen der erfassten Kräfte errechnet FLORIAN die Stärke in der
|
||
üblichen Schreibweise `Führer/Unterführer/Mannschaft/Gesamt`:
|
||
|
||
| Einheit | Stärke | Besetzung |
|
||
|---|---|---|
|
||
| Trupp | 0/1/1/2 | Truppführer, Truppmann |
|
||
| Staffel | 1/1/4/6 | Gruppenführer, Maschinist, zwei Trupps |
|
||
| Gruppe | 1/3/5/9 | Gruppenführer, Maschinist, Melder, drei Trupps |
|
||
| Zug | 1/5/12/18 | Zugführer, Melder, Gruppe und Staffel |
|
||
|
||
**Regional unterschiedlich.** Manche Wehren zählen den Maschinisten zu den
|
||
Unterführern, andere schreiben dreistellig ohne Unterführer (Gruppe = 1/8/9).
|
||
Die Zuordnung steht an einer Stelle — [`src/lib/einsatz/staerke.ts`](src/lib/einsatz/staerke.ts) —
|
||
und ist bewusst leicht zu ändern.
|
||
|
||
### Der freigegebene Bericht ist unveränderbar
|
||
|
||
Dieselbe Logik wie beim Prüfprotokoll aus Phase 2, denn der Bericht geht an
|
||
die Gemeinde und im Schadensfall an die Versicherung. Bis zur Freigabe ist
|
||
alles änderbar — ein Einsatz wird tagelang ergänzt, und genau dafür ist der
|
||
Entwurfsstand da. Mit der Freigabe ist Schluss; der Trigger
|
||
`einsatz_freigabe_schuetzen` setzt das durch.
|
||
|
||
Zwei Ausnahmen bleiben, beide nachtragend: das PDF, solange keines hinterlegt
|
||
ist, und `geaendertAm`.
|
||
|
||
**Lagemeldungen sind ausdrücklich ausgenommen.** Sie sind das Protokoll des
|
||
Verlaufs, kein Bestandteil des Berichts — und eine Meldung aus dem Portal kann
|
||
eintreffen, wenn der Bericht längst geschrieben ist.
|
||
|
||
### Einsatzportal für Fremdwehren
|
||
|
||
Bei einer größeren Sache sind Nachbarwehren, THW oder Rettungsdienst mit vor
|
||
Ort. Die haben kein Konto in FLORIAN und sollen auch keines bekommen. Der
|
||
Einsatzleiter erzeugt stattdessen einen Link, den er als QR-Code zeigt oder
|
||
weiterschickt.
|
||
|
||
Vier Schranken sichern ihn ab:
|
||
|
||
1. **Zufall** — 32 zufällige Bytes im Link.
|
||
2. **Nur ein Hash in der Datenbank** — wer sie liest, bekommt keinen gültigen Link.
|
||
3. **Zeitlich begrenzt** — Vorgabe 24 Stunden, jederzeit widerrufbar, jeder
|
||
Aufruf wird mitgezählt.
|
||
4. **Zugriffsbegrenzung** — 120 Aufrufe je Link und Stunde, 30 Meldungen, 10
|
||
Fotos.
|
||
|
||
Und eine fünfte, die keine technische ist: das Portal zeigt Stichwort, Ort,
|
||
Zeitstempel und Lagemeldungen. **Keine Namen von Einsatzkräften, keine
|
||
Berichte, kein Weg in den Rest der Anwendung.**
|
||
|
||
### Adressen zu Koordinaten
|
||
|
||
Über einen **selbst betriebenen** Nominatim (`NOMINATIM_URL`). Einsatzadressen
|
||
sagen, wo es gebrannt hat und wer betroffen war; solche Anfragen gehören nicht
|
||
an einen fremden Dienst, und die Nutzungsbedingungen des öffentlichen Nominatim
|
||
erlauben es ohnehin nicht.
|
||
|
||
Ohne Konfiguration bleibt das Feld leer und die Koordinaten werden von Hand
|
||
eingetragen — kein Fehler, sondern der Normalfall in einer frischen
|
||
Installation. Die Karte dazu kommt in Phase 6.
|
||
|
||
Gespeichert wird doppelt: als Dezimalgrad, die Prisma lesen kann, und als
|
||
PostGIS-Punkt für die Karte. Ein Trigger hält beides zusammen.
|
||
|
||
### Statistik und Jahresexport
|
||
|
||
`/einsaetze/statistik` beantwortet beide Fragen: die der Gemeinde (Anzahl nach
|
||
Kategorie der Landesstatistik, Hilfsfristen) und die der Wehr (Beteiligung je
|
||
Person, Auslastung je Fahrzeug). Der Jahresexport als PDF liegt unter
|
||
`/api/einsaetze/jahresexport?jahr=…` und wird bewusst **nicht** abgelegt — er
|
||
ist eine Auswertung, keine Urkunde, und ändert sich, solange noch Einsätze
|
||
nachgetragen werden.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `GEOCODING_ENABLED` | `false` | Adresssuche einschalten |
|
||
| `NOMINATIM_URL` | — | Adresse des eigenen Nominatim |
|
||
| `GEOCODING_LAENDER` | `de,at,ch` | Eingrenzung der Suche |
|
||
| `PORTAL_ENABLED` | `true` | Einsatzportal für Fremdwehren |
|
||
| `PORTAL_GUELTIGKEIT_STUNDEN` | `24` | Lebensdauer eines Links |
|
||
| `PORTAL_MAX_ZUGRIFFE` | `120` | Aufrufe je Link und Stunde |
|
||
|
||
|
||
---
|
||
|
||
## Phase 4 — Das Terminal im Gerätehaus
|
||
|
||
Ein Wandtablet neben der Fahrzeughalle. Es muss zwei Dinge können, die eine
|
||
Verwaltungsoberfläche am Schreibtisch nicht können muss: **mit Handschuhen
|
||
bedienbar** sein und **weiterarbeiten, wenn das Netz weg ist**. Stahlbeton,
|
||
Metalltore und ein Repeater, der irgendwann ausfällt — das ist der Normalfall,
|
||
nicht die Ausnahme.
|
||
|
||
Die Oberfläche liegt unter `/kiosk` und ist eine eigene: große Kacheln statt
|
||
Navigationsleiste, Schaltflächen ab 64 Pixel Höhe, Symbol **und** Text.
|
||
|
||
### Der Weg durch das Modul
|
||
|
||
1. **Einmal einrichten** — `/kiosk/einrichten` verlangt den Kurznamen der Wehr
|
||
und das Gerätegeheimnis aus der `.env` (`KIOSK_DEVICE_SECRET`). Beides nimmt
|
||
die Seite auch aus der Adresse an, damit am Tablet nichts abgetippt werden
|
||
muss:
|
||
|
||
```
|
||
/kiosk/einrichten?wehr=ff-musterhausen&schluessel=…
|
||
```
|
||
|
||
2. **Anmelden** — Ausweis auflegen, QR-Code vom Handy oder Mitgliedsnummer und
|
||
PIN. Ohne eingerichtetes Gerät kommt niemand hinein, auch nicht mit richtiger
|
||
PIN: das Gerätegeheimnis ist die erste Schranke.
|
||
3. **Erfassen** — Mangel melden, Kommen/Gehen, Fahrt beginnen und beenden,
|
||
Wäsche abgeben, Inventurgang.
|
||
4. **Von selbst abmelden** — nach `KIOSK_IDLE_TIMEOUT` Sekunden Untätigkeit,
|
||
mit sichtbarer Warnung vorher. Ein Terminal, an dem die letzte Anmeldung
|
||
stehen bleibt, erfasst irgendwann alles auf den Falschen.
|
||
|
||
Was dabei zusammenkommt, steht in der Verwaltung unter `/terminal`.
|
||
|
||
### Mangel melden in unter dreißig Sekunden
|
||
|
||
Der Vorgang, an dem sich das Terminal misst. Drei Schritte, kein Formular:
|
||
|
||
1. QR-Code am Gerät scannen — oder die Inventarnummer eintippen.
|
||
2. Foto aufnehmen und einen Satz dazu.
|
||
3. Dringlichkeit antippen. Damit ist es abgeschickt.
|
||
|
||
„Kritisch" sperrt das Gerät sofort. Eine unbekannte Inventarnummer hält die
|
||
Meldung **nicht** auf: sie geht raus und landet notfalls in der Konfliktliste —
|
||
lieber ein Zettel zu viel als ein Riss im Schlauch, den niemand gemeldet hat.
|
||
|
||
### Offline: wie nichts verloren geht
|
||
|
||
Drei Bausteine, und keiner davon ist optional:
|
||
|
||
**Service Worker** (`public/sw.js`, von Hand geschrieben, keine Bibliothek):
|
||
App-Shell und Symbole liegen im Zwischenspeicher, Seitenaufrufe gehen
|
||
`network-first` mit Ausweichseite `/kiosk/offline`, lesende tRPC-Aufrufe
|
||
`stale-while-revalidate`. Schreibende Anfragen und alles unter `/api/auth`
|
||
fasst er **nicht** an — eine zwischengespeicherte Anmeldung wäre eine Lücke,
|
||
kein Komfort.
|
||
|
||
**Warteschlange in der IndexedDB** (`src/lib/kiosk/outbox.ts`, Dexie): jede
|
||
schreibende Aktion geht zuerst dorthin — auch online. Sonst gäbe es zwei Wege
|
||
durch den Code, und der seltenere wäre der ungetestete. Fotos liegen als Blob
|
||
daneben und werden beim Übertragen zuerst hochgeladen.
|
||
|
||
**Idempotenzschlüssel**: die Kennung erzeugt der Browser beim Einreihen
|
||
(`crypto.randomUUID()`), nicht der Server beim Empfangen. Bricht die
|
||
Übertragung ab und wird wiederholt, erkennt der Server denselben Vorgang an
|
||
derselben Kennung wieder und antwortet mit dem Ergebnis von damals. Die Tabelle
|
||
`kiosk_vorgang` hat diese Kennung als Primärschlüssel, und die Datenbank
|
||
verweigert dem Anwendungszugang `UPDATE` und `DELETE` darauf.
|
||
|
||
Ein Vorgang verschwindet aus der Warteschlange, wenn der Server ihn angenommen
|
||
**oder** als bereits verarbeitet erkannt **oder** in die Konfliktliste gelegt
|
||
hat. In allen drei Fällen ist er angekommen. Liegen bleibt nur, was technisch
|
||
nicht durchging.
|
||
|
||
Der Zustand steht als Leiste über allem — verbunden, wie viele Änderungen
|
||
warten, kein Netz — in Klartext und nicht nur als Farbe.
|
||
|
||
**Wem ein Vorgang zugeschrieben wird.** Der Server nimmt dafür die Person, die
|
||
beim Empfang angemeldet ist. Im Regelfall ist das dieselbe, die erfasst hat —
|
||
wer offline erfasst, bleibt angemeldet, weil auch das Abmelden Netz braucht.
|
||
Damit dazwischen kein Fenster bleibt, überträgt das Terminal vor jedem
|
||
Abmelden — von Hand wie durch die Abmeldeuhr — noch einmal.
|
||
|
||
### Konflikte
|
||
|
||
Ein Vorgang, der sich nicht anwenden lässt (Gerät zwischenzeitlich ausgesondert,
|
||
Prüfung inzwischen festgeschrieben), wird nicht verworfen und nicht erzwungen,
|
||
sondern in `sync_konflikt` gelegt. Der Gerätewart sieht ihn unter `/terminal`
|
||
und entscheidet. Das ist der ehrliche Umgang mit einem Tablet, das drei Tage
|
||
offline war.
|
||
|
||
### Anmeldemarke fürs Handy
|
||
|
||
Wer den Ausweis vergessen hat, öffnet `/anmeldemarke` und hält den QR-Code vor
|
||
die Kamera des Terminals. Die Marke ist mit `AUTH_SECRET` signiert, **nicht
|
||
gespeichert**, und gilt 60 Sekunden — ein abfotografierter Code aus der Umkleide
|
||
nützt danach nichts mehr. Die Seite erneuert ihn von selbst.
|
||
|
||
### Hallenmonitor
|
||
|
||
`/kiosk/monitor` läuft im Vollbild ohne Bedienung: Uhrzeit, laufender Einsatz,
|
||
wer im Haus ist, Fahrzeuge samt laufenden Fahrten, fällige Prüfungen und die
|
||
nächsten Termine. Optional Wetterwarnungen des DWD — die einzige Verbindung,
|
||
die FLORIAN von sich aus nach außen aufbaut, und deshalb standardmäßig aus.
|
||
|
||
### Als App installieren
|
||
|
||
Das Manifest liegt unter `/manifest.webmanifest`, die Symbole entstehen mit
|
||
`npm run icons` und liegen unter `public/icons/`. Startseite ist `/kiosk`. Im
|
||
Kiosk-Browser des Tablets wird daraus eine Vollbildanwendung ohne Adresszeile.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `KIOSK_ENABLED` | `true` | Terminalanmeldung freischalten |
|
||
| `KIOSK_DEVICE_SECRET` | — | Geheimnis, mit dem sich ein Tablet ausweist |
|
||
| `KIOSK_IDLE_TIMEOUT` | `120` | Abmeldung nach Untätigkeit, in Sekunden |
|
||
| `WETTER_ENABLED` | `false` | Wetterwarnungen auf dem Monitor |
|
||
| `DWD_WARNCELL` | — | Warnzellen-Nummer der eigenen Gemeinde |
|
||
| `DWD_WARNUNGEN_URL` | DWD-Warnapp | Quelle der Warnungen |
|
||
| `WETTER_TIMEOUT` | `5` | Zeitlimit des Abrufs, in Sekunden |
|
||
| `WETTER_ZWISCHENSPEICHER` | `300` | Gültigkeit einer Antwort, in Sekunden |
|
||
|
||
---
|
||
|
||
## Phase 5 — Atemschutz nach FwDV 7
|
||
|
||
Das sicherheitsrelevanteste Modul des Projekts. Über jeder Ansicht steht
|
||
deshalb dauerhaft:
|
||
|
||
> **Ergänzendes System — die analoge Atemschutzüberwachung bleibt führend.**
|
||
|
||
Der Hinweis lässt sich abschalten, aber nur zusammen mit einer Fundstelle:
|
||
`ATEMSCHUTZ_HINWEIS=false` wirkt erst, wenn unter `ATEMSCHUTZ_FREIGABE` steht,
|
||
worauf sich die Freigabe stützt — etwa „Kommandantenbeschluss vom 12.03.2026".
|
||
Fehlt sie, bleibt der Hinweis stehen. Eine Freigabe ohne Beschluss ist keine.
|
||
|
||
### Die Zeit gehört dem Server
|
||
|
||
Das ist die eine Entscheidung, aus der alles Weitere folgt. **Nirgends wird
|
||
eine Restzeit heruntergezählt.** In der Datenbank stehen Zeitstempel; wie lange
|
||
ein Trupp drin ist, rechnet jede Ansicht bei jedem Bild neu aus dem
|
||
Einstiegszeitpunkt und der aktuellen Uhr.
|
||
|
||
Daraus ergibt sich von selbst, was Phase-5.md als wichtigste Anforderung nennt:
|
||
|
||
| Was passiert | Was die Anzeige macht |
|
||
|---|---|
|
||
| Browser neu laden | Zeigt dieselbe Zeit weiter — es gab nichts zu verlieren |
|
||
| Zweiter Bildschirm daneben | Zeigt dieselbe Zeit, ohne Abgleich |
|
||
| Verbindung bricht ab | Die Uhr läuft weiter, und der Abriss steht sichtbar da |
|
||
| Server neu gestartet | Nach der ersten Antwort ist alles wieder da |
|
||
|
||
Server und Browser rechnen mit **denselben** Funktionen
|
||
(`src/lib/atemschutz/berechnung.ts`). Was das Tablet anzeigt, weist das
|
||
Protokoll später genauso aus.
|
||
|
||
### Rückzugsdruck
|
||
|
||
Gerechnet wird nach dem **doppelten Anmarschverbrauch**: was der Weg hinein an
|
||
Luft gekostet hat, kostet der Weg zurück noch einmal.
|
||
|
||
```
|
||
Rückzug = 2 × (Eingangsdruck − Druck am Einsatzziel) + Warndruck + Zuschlag
|
||
```
|
||
|
||
Dafür meldet der Trupp beim Erreichen des Einsatzziels einmal seinen Druck.
|
||
Solange das nicht geschehen ist, greift die **Zwei-Drittel-Regel** — sie liegt
|
||
in aller Regel höher und ist damit die vorsichtigere Annahme; die Karte sagt
|
||
dann „vorläufig" dazu.
|
||
|
||
Maßgeblich ist immer der ungünstigste Wert im Trupp: der Trupp geht gemeinsam
|
||
zurück, also zählt, wer am wenigsten Luft hat.
|
||
|
||
Die Ampel steht schon zehn Prozent über der Grenze auf Gelb — der Trupp soll
|
||
losgehen, bevor der Wert erreicht ist, nicht wenn er erreicht ist. Farbe **und**
|
||
Text, wie überall in FLORIAN.
|
||
|
||
### Der Weg durch eine Überwachung
|
||
|
||
1. **Eröffnen** — Bezeichnung genügt. Die Zuordnung zu einem Einsatz lässt sich
|
||
nachtragen; im Ernstfall wird zuerst überwacht.
|
||
2. **Trupp anlegen** — zwei oder drei Personen, für jede der Eingangsdruck, eine
|
||
davon Truppführer. Daneben steht, ob die Person nach FwDV 7 einsetzbar ist.
|
||
**Gesperrt wird nichts:** im Einsatz entscheidet der Einheitsführer, und ein
|
||
Programm, das ihm im entscheidenden Moment eine Person verweigert, wird
|
||
umgangen — dann steht am Ende gar nichts im Protokoll.
|
||
3. **Einstieg** — ab hier läuft die Uhr.
|
||
4. **Druckabfrage** — für den ganzen Trupp in einer Zeile. Nach
|
||
`ATEMSCHUTZ_ABFRAGE_INTERVALL` Sekunden ohne Meldung wird erinnert; gerechnet
|
||
ab der letzten Meldung, nicht ab dem Einstieg.
|
||
5. **Rückweg und heraus** — beim Herauskommen die Ausgangsdrücke.
|
||
6. **Beenden** — geht erst, wenn alle Trupps als draußen gemeldet sind. Eine
|
||
beendete Überwachung mit einem Trupp, der noch drin steht, wäre die
|
||
gefährlichste Zeile im ganzen Protokoll.
|
||
|
||
Dazu jederzeit: **Rückzug anordnen**, **Mayday** (aufzuheben, nicht
|
||
wegzuklicken) und Notizen ins Protokoll.
|
||
|
||
### Der Ereignisstrom
|
||
|
||
Änderungen kommen über **Server-Sent Events** unter `/api/atemschutz/strom` —
|
||
kein WebSocket, kein eigener Node-Server, `next start` bleibt wie es ist. Der
|
||
Strom meldet nur „es hat sich etwas geändert"; die Ansicht holt daraufhin den
|
||
Stand. Geschrieben wird wie überall über tRPC.
|
||
|
||
Zwei Dinge machen ihn belastbar:
|
||
|
||
- **Er verbindet sich selbst neu.** `EventSource` bringt das mit.
|
||
- **Stille gilt als Abriss.** Der Server schickt alle zwei Sekunden ein
|
||
Lebenszeichen; bleibt es acht Sekunden aus, steht die Verbindung sichtbar auf
|
||
„unterbrochen" — auch wenn der Browser keinen Fehler meldet. Dieser Fall ist
|
||
real: wird das Netz gekappt, während der Strom offen ist, schweigt
|
||
`EventSource` einfach.
|
||
|
||
Fällt der Strom ganz aus, bleibt die Ansicht bedienbar — sie fragt zusätzlich im
|
||
festen Takt nach. Der Strom macht sie schneller, nicht erst funktionsfähig.
|
||
|
||
### Das Protokoll
|
||
|
||
Ein PDF mit Rahmen, Trupps und dem Verlauf — Ereignisse und Druckmessungen in
|
||
**einer** Zeitachse, damit niemand zwei Spalten gleichzeitig lesen muss. Es
|
||
entsteht beim Beenden im Hintergrundprozess und, falls das nicht geklappt hat,
|
||
beim Abruf unter `/api/atemschutz/protokoll?id=…`.
|
||
|
||
Im Kopf steht, ob die digitale Überwachung freigegeben war. Wer das Blatt
|
||
später in der Hand hält, soll wissen, was er da hat.
|
||
|
||
Verändern lässt sich nichts: `druckmessung` und `atemschutz_ereignis` sind für
|
||
den Anwendungszugang gegen `UPDATE` und `DELETE` gesperrt, und zusätzlich sitzt
|
||
auf beiden ein Trigger. Ein Fehler wird durch einen neuen Eintrag
|
||
richtiggestellt.
|
||
|
||
### Wer darf unter Atemschutz?
|
||
|
||
Drei Nachweise, die gleichzeitig gelten müssen:
|
||
|
||
| Nachweis | Frist |
|
||
|---|---|
|
||
| **G26.3** | nach Alter: bis 50 Jahre drei Jahre, danach eines |
|
||
| **Belastungsübung** | jährlich (Übungsstrecke) |
|
||
| **Einsatzübung unter Atemschutz** | jährlich — ein echter Einsatz zählt |
|
||
|
||
Dazu die Qualifikation als Atemschutzgeräteträger aus Phase 1. Die Übersicht
|
||
zeigt eine Ampel **mit Klartext**, was fehlt: eine rote Ampel ohne Begründung
|
||
hilft am Fahrzeug niemandem. Auf der Startseite steht die Zahl, nach der im
|
||
Einsatz als Erstes gefragt wird — wie viele Träger heute einsetzbar sind.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `ATEMSCHUTZ_HINWEIS` | `true` | Sicherheitshinweis über jeder Ansicht |
|
||
| `ATEMSCHUTZ_FREIGABE` | — | Fundstelle der Freigabe; ohne sie bleibt der Hinweis |
|
||
| `ATEMSCHUTZ_ABFRAGE_INTERVALL` | `600` | Abstand der Druckabfrage in Sekunden |
|
||
| `ATEMSCHUTZ_WARNDRUCK` | `55` | Ansprechdruck der Warneinrichtung in bar |
|
||
| `ATEMSCHUTZ_ZUSCHLAG` | `10` | Sicherheitszuschlag auf den Rückzugsdruck |
|
||
|
||
---
|
||
|
||
## Phase 6 — Kartenmodule
|
||
|
||
Die Frage, wegen der es das Modul gibt, wird auf der Anfahrt gestellt: **Was
|
||
liegt in der Nähe, und was gibt es her?**
|
||
|
||
### 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. FLORIAN lädt deshalb **keine** Kartenkacheln von
|
||
einem öffentlichen Dienst — auch nicht „nur zum Ausprobieren". Der Kartenserver
|
||
läuft selbst:
|
||
|
||
```bash
|
||
docker compose --profile karte up -d
|
||
```
|
||
|
||
Dafür wird eine MBTiles-Datei des eigenen Ausrückegebiets gebraucht; woher man
|
||
sie bekommt, steht in [docker/karten/LIESMICH.md](docker/karten/LIESMICH.md).
|
||
Danach in der `.env`:
|
||
|
||
```
|
||
KARTE_STIL_URL=http://localhost:8085/styles/basic/style.json
|
||
```
|
||
|
||
**Ohne Kartenserver läuft alles trotzdem** — die Karte zeigt einen leeren Grund
|
||
mit einem Hinweis darauf. Punkte liegen an der richtigen Stelle, Entfernungen
|
||
stimmen, die Umkreissuche rechnet; es fehlen nur die Straßen darunter. Ist ein
|
||
Server eingetragen, aber nicht erreichbar, sieht es genauso aus — mit
|
||
entsprechendem Hinweis.
|
||
|
||
### Löschwasser
|
||
|
||
Hydranten, Saugstellen, Zisternen und offene Gewässer mit Typ, Nennweite,
|
||
Durchfluss, Zustand und dem, was man vor Ort wissen muss („im Winter oft
|
||
zugeschoben").
|
||
|
||
Die **Umkreissuche** ist der eigentliche Zweck: in die Karte tippen, und es
|
||
steht da, was in 500 Metern erreichbar ist — nach Entfernung sortiert, mit
|
||
Nennweite und Durchfluss. Sie läuft über PostGIS (`ST_DWithin` auf der
|
||
Geografie, GiST-Index), nicht über die Dezimalgrade: bei tausend Hydranten wäre
|
||
alles andere ein vollständiger Tabellendurchlauf, und gefragt wird während der
|
||
Fahrt.
|
||
|
||
### Import
|
||
|
||
Zwei Quellen, die eine Wehr tatsächlich bekommt:
|
||
|
||
| Quelle | Was erkannt wird |
|
||
|---|---|
|
||
| **GeoJSON** | FeatureCollection mit Punkten, Eigenschaften wie in der CSV |
|
||
| **CSV der Stadtwerke** | Spaltennamen wie `DN`, `Lage`, `Latitude`, `Pruefdatum`, `Bauart` |
|
||
|
||
Der Import ist nachsichtig, aber ehrlich: „DN 100" wird zu 100, „48,5031" zu
|
||
48.5031, „24.05.2026" zu einem Datum, und vertauschte Achsen werden erkannt und
|
||
zurückgedreht. Was nicht lesbar ist, wird mit Zeilennummer und Grund gemeldet —
|
||
und der Rest läuft durch. Eine Liste, die zu 90 Prozent stimmt, ist im Einsatz
|
||
mehr wert als keine.
|
||
|
||
**Geraten wird nichts.** Fehlt die Position, wird die Zeile übersprungen: ein
|
||
Hydrant ohne Ort ist kein Hydrant. Und ohne Statusangabe steht „ungeprüft",
|
||
nicht „einsatzbereit".
|
||
|
||
### Objekte und Feuerwehrpläne
|
||
|
||
Was hier gepflegt wird, wird auf der Anfahrt gelesen — oft vom Beifahrersitz,
|
||
oft in zwanzig Sekunden. Deshalb sind **Zufahrt**, **Gefahren** und
|
||
**Schlüsseldepot** eigene Felder und stehen ganz oben, nicht in einer
|
||
Fließtextnotiz.
|
||
|
||
Dazu Ansprechpartner mit der einzigen Angabe, die im Einsatz zählt — wer nachts
|
||
ans Telefon geht — und die Planblätter nach DIN 14095 mit ihrem Stand.
|
||
|
||
Gesucht wird über alles: Name, Straße, Gefahrenstichwort, Ansprechpartner. Wer
|
||
im Einsatz sucht, weiß selten, wie das Objekt im System heißt.
|
||
|
||
### Lagekarte
|
||
|
||
Eine je Einsatz, unterhalb der Einsatzdaten. 22 taktische Zeichen nach
|
||
FwDV 102, selbst gezeichnet als SVG — fertige Sätze sind selten frei
|
||
verwendbar, und nachgeladen wird ohnehin nichts.
|
||
|
||
Ein Werkzeug, ein Klick: Zeichen wählen, in die Karte tippen. Dazu Linienzüge
|
||
(Schlauchleitung, Absperrung), Flächen (Bereitstellungsraum) und
|
||
Beschriftungen.
|
||
|
||
Der Abgleich zwischen Geräten läuft über denselben Ereignisstrom wie die
|
||
Atemschutzüberwachung, und wie dort steht der Zustand der Verbindung sichtbar
|
||
da. Zurückgenommene Elemente verschwinden von der Karte, nicht aus dem
|
||
Datenbestand: eine Lagekarte ist die Mitschrift einer Entscheidung.
|
||
|
||
### Wasserförderung
|
||
|
||
Die Rechnung, die sonst mit Tabelle und Bleistift auf der Motorhaube entsteht.
|
||
Strecke in die Karte tippen, Durchmesser und Fördermenge einstellen:
|
||
|
||
```
|
||
Reibungsverlust je 100 m = Beiwert × (Fördermenge / 100)²
|
||
Höhenverlust = Höhenmeter × 0,1 bar
|
||
```
|
||
|
||
Daraus: Zahl der Verstärkerpumpen, Schlauchlängen je Abschnitt, Restdruck und
|
||
der Druck, der an der Einsatzstelle ankommt. Die Beiwerte treffen die
|
||
Tabellenwerte der Ausbildung — 1,5 bar je 100 m im B-Schlauch bei 800 l/min.
|
||
|
||
Reicht ein Abschnitt selbst mit voller Pumpe nicht, sagt die Ausgabe das,
|
||
statt eine Zahl auszuwerfen, die niemand halten kann. Und unter jeder Rechnung
|
||
steht, was sie ist: **eine Planungshilfe. Was zählt, ist der Druck am
|
||
Verteiler.**
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `KARTE_STIL_URL` | — | Stil des eigenen Kartenservers; leer = leerer Grund |
|
||
| `KARTE_BREITENGRAD` | `48.5` | Mittelpunkt, falls kein Standort verortet ist |
|
||
| `KARTE_LAENGENGRAD` | `12.15` | dito |
|
||
| `KARTE_ZOOM` | `13` | Zoomstufe beim Öffnen |
|
||
| `KARTE_PORT` | `8085` | Port des tileserver-gl (Profil `karte`) |
|
||
|
||
---
|
||
|
||
## Phase 7 — Benachrichtigungen und Termine
|
||
|
||
Der Satz, der über der ganzen Phase steht und der auch die Technik darunter
|
||
bestimmt: **Niemand wird ungefragt benachrichtigt.** Wer nichts einträgt,
|
||
bekommt nichts. Es gibt keine Voreinstellung, die stillschweigend alle
|
||
anmeldet — ein neu eingerichteter Zustellweg hat eine leere Ereignisliste, und
|
||
erst wer ankreuzt, wofür er ihn will, bekommt etwas.
|
||
|
||
### Der Terminkalender
|
||
|
||
Übungsabende, Lehrgänge, Arbeitsdienste, Sitzungen. Drei Entscheidungen prägen
|
||
ihn, und alle drei kommen aus dem Alltag einer Wehr:
|
||
|
||
**Anmeldung ist die Ausnahme, nicht die Regel.** Beim normalen Übungsabend
|
||
meldet sich niemand an, man kommt oder man kommt nicht. Erst beim Lehrgang mit
|
||
zwölf Plätzen und beim Ausflug mit Busbestellung wird eine Rückmeldung
|
||
verlangt.
|
||
|
||
**Abgesagt, nicht gelöscht.** Ein Termin, der ohne ein Wort verschwindet, führt
|
||
dazu, dass am Dienstagabend jemand vor dem verschlossenen Gerätehaus steht.
|
||
Eine Absage braucht einen Grund, bleibt im Kalender stehen und erreicht die,
|
||
die zugesagt hatten — nicht die ganze Wehr.
|
||
|
||
**Die Warteliste rückt von selbst nach.** Wer bei belegten Plätzen zusagt,
|
||
landet auf der Warteliste; sagt jemand ab, rückt der Erste nach und erfährt es.
|
||
Sonst führt jemand die Liste von Hand, und das tut niemand zuverlässig.
|
||
|
||
Serientermine werden **ausgeschrieben**, nicht als Regel gespeichert: jeder
|
||
Abend ist ein eigener Datensatz. Der Grund ist derselbe Alltag — der Übungsabend
|
||
fällt im August aus, im Dezember ist er eine Woche früher, und für jeden
|
||
einzelnen gibt es Zusagen und eine Anwesenheitsliste. Mit einer Regel wäre jede
|
||
dieser Ausnahmen ein Sonderfall. Vor dem Anlegen steht in der Maske, wie viele
|
||
Termine daraus werden.
|
||
|
||
Die Anwesenheitsliste zeigt **alle aktiven Mitglieder**, nicht nur die
|
||
Angemeldeten: auf einem Blatt zum Abhaken muss jeder stehen. Zusage und
|
||
Anwesenheit sind getrennt — wer zugesagt hat und nicht kam, steht genau so in
|
||
der Liste.
|
||
|
||
### Der Kalender fürs Handy
|
||
|
||
Eine Adresse, die man im Handy oder im Mailprogramm abonniert; die Termine
|
||
stehen dann neben allem anderen, ohne dass jemand FLORIAN öffnen muss. Das ist
|
||
der Kanal mit der größten Wirkung pro Zeile Code in dieser Phase.
|
||
|
||
**Bewusst kein CalDAV.** Das ist ein Schreib-Lese-Protokoll mit
|
||
Rechteverwaltung und Konfliktauflösung; gebraucht wird hier nur Lesen, und dafür
|
||
genügt eine Adresse, die eine ICS-Datei ausliefert. Jeder Kalender kann das.
|
||
|
||
Die Adresse ist der Schlüssel — Kalenderprogramme können sich nicht anmelden,
|
||
sie rufen stumpf eine URL ab. Daraus folgt:
|
||
|
||
- Gespeichert wird nur der SHA-256 des Tokens; die Adresse steht **genau
|
||
einmal** auf dem Bildschirm.
|
||
- Jeder Zugang lässt sich einzeln widerrufen. Ein verlorenes Handy kostet einen
|
||
Klick, nicht ein neues Passwort für alle.
|
||
- Ausgeliefert werden **nur Termine** — keine Anmeldelisten, keine Personen
|
||
außer der Leitung, keine Mängel, keine Einsätze.
|
||
- Wer aus der Wehr austritt, verliert das Abonnement.
|
||
|
||
### Die vier Kanäle
|
||
|
||
| Kanal | Läuft über | Bekommt |
|
||
|---|---|---|
|
||
| **E-Mail** (SMTP) | den eigenen bzw. den Mailserver der Gemeinde | den ausführlichen Text |
|
||
| **ntfy** (selbst gehostet) | den eigenen Server, Profil `push` | den ausführlichen Text |
|
||
| **Telegram** | einen fremden Server | nur die Kurzfassung |
|
||
| **Web-Push** | den Push-Dienst des Browserherstellers | nur die Kurzfassung |
|
||
|
||
Die Spalte ganz rechts ist keine Bequemlichkeit, sondern die Regel: was das Netz
|
||
der Wehr verlässt, bekommt einen Hinweis ohne Namen, Gerätebezeichnungen und
|
||
Einsatzdetails — „3 Prüfungen fällig", nicht welche. In der Oberfläche steht das
|
||
neben jedem externen Kanal, und die Vorlagenmaske sagt es noch einmal, bevor
|
||
jemand einen Namen in eine Telegram-Vorlage tippt.
|
||
|
||
**ntfy** ist der interessanteste Weg für eine Wehr: ein Container, eine App im
|
||
Store, kein Konto, keine Telefonnummer, kein Telefonbuchabgleich. Man abonniert
|
||
ein Thema — einen frei gewählten Namen — und bekommt Meldungen darauf. Läuft
|
||
der Server im Gerätehaus, verlässt keine Nachricht das Haus. Der Preis: ein
|
||
Thema ist nur so geheim wie sein Name, deshalb schlägt FLORIAN Namen mit
|
||
Zufallsanteil vor.
|
||
|
||
```bash
|
||
docker compose --profile push up -d # ntfy startet auf Port 8086
|
||
npm run vapid # Schlüsselpaar für Web-Push
|
||
```
|
||
|
||
### Warum kein WhatsApp
|
||
|
||
Es gibt keine `WHATSAPP_`-Variablen, und das ist eine Entscheidung, keine
|
||
Lücke. Drei Gründe, in dieser Reihenfolge:
|
||
|
||
**Datenschutz.** Der einzige zulässige Weg für eine Organisation ist die
|
||
WhatsApp Business Platform. Nachrichten laufen über Server von Meta, und die
|
||
Wehr müsste einen Vertrag zur Auftragsverarbeitung schließen — für eine
|
||
Freiwillige Feuerwehr, deren Daten ausdrücklich im Gerätehaus bleiben sollen,
|
||
ist das der falsche Weg. Die gewöhnliche App scheidet ohnehin aus: sie gleicht
|
||
das Telefonbuch ab und gibt damit die Kontakte aller Mitglieder weiter.
|
||
|
||
**Kosten.** Abgerechnet wird je Konversation. Eine Wehr mit fünfzig Mitgliedern
|
||
und zwei Terminerinnerungen im Monat zahlt dauerhaft für etwas, das über den
|
||
eigenen Mailserver nichts kostet.
|
||
|
||
**Aufwand.** Business-Konto, verifizierte Telefonnummer, genehmigte
|
||
Nachrichtenvorlagen, ein Meta-Entwicklerkonto. Das ist mehr Verwaltung als der
|
||
gesamte Rest dieser Phase.
|
||
|
||
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
|
||
und verrät niemandem etwas.
|
||
|
||
### Der digitale Dienstausweis
|
||
|
||
Ein QR-Code auf dem Handy, den man an der Absperrung, beim Lehrgang oder bei
|
||
einer Kontrolle vorzeigt. Wer ihn scannt, landet auf einer Prüfseite und sieht
|
||
Name, Wehr, Funktion, Foto und die gültigen Qualifikationen — **ohne sich
|
||
anzumelden**, denn wer prüft, hat keinen Zugang zu FLORIAN.
|
||
|
||
- **Signiert, nicht gespeichert.** HMAC über Person, Wehr und Zeitstempel.
|
||
Keine Tabelle, kein Aufräumjob.
|
||
- **Fünf Minuten gültig.** Er wird vorgezeigt, abgelesen und weggesteckt; ein
|
||
abfotografierter Code ist danach wertlos. Die Prüfseite nennt deshalb den
|
||
Zeitpunkt der Ausstellung — ein Ausweis, der „gültig" sagt, ohne zu sagen
|
||
seit wann, ist wertlos.
|
||
- **Er öffnet nichts.** Die Prüfseite zeigt an, sie lässt nicht hinein. Wer den
|
||
Code hat, bekommt keinen Zugang zu einem einzigen Datensatz.
|
||
- Was **nicht** darauf steht: Anschrift, Geburtsdatum, Telefonnummer,
|
||
Mitgliedsnummer. Ein Ausweis weist aus; er ist keine Personalakte.
|
||
- Ein gültig signierter Code für jemanden, der ausgetreten ist, wird abgewiesen.
|
||
|
||
### Was von selbst benachrichtigt
|
||
|
||
Sechs Anlässe, an die vorhandenen Abläufe angehängt:
|
||
|
||
| Anlass | Wann | An wen |
|
||
|---|---|---|
|
||
| Prüfung fällig | im Erinnerungslauf | wer es abonniert hat |
|
||
| Mangel gemeldet | sofort | wer es abonniert hat |
|
||
| Qualifikation läuft ab | 90, 30 und 7 Tage vorher | die Person selbst |
|
||
| Neuer Termin | beim Anlegen | wer es abonniert hat |
|
||
| Terminerinnerung | 7 Tage und 1 Tag vorher | Angemeldete, sonst Abonnenten |
|
||
| Einsatzbericht freigegeben | bei der Freigabe | wer es abonniert hat |
|
||
|
||
Die Sammelmail an den zuständigen Gerätewart bleibt, wie sie war — sie geht an
|
||
den, der zuständig ist, unabhängig davon, ob er irgendwo ein Kreuzchen gesetzt
|
||
hat. Ein Prüftermin ist eine Pflicht, kein Abo. Wer sie ohnehin bekommt, wird
|
||
von der Benachrichtigung ausgenommen und hat sie damit nicht doppelt.
|
||
|
||
Erinnert wird um sieben Uhr morgens, nicht nachts: eine Push-Meldung um drei Uhr
|
||
weckt Leute, die am nächsten Tag arbeiten müssen. Zugestellt wird dagegen jede
|
||
Minute — eine Absage am Dienstagabend muss ankommen, solange sie noch etwas
|
||
nützt.
|
||
|
||
**Anlegen und Zustellen sind getrennt.** Wo ein Ereignis entsteht, wird nur eine
|
||
Zeile geschrieben; der Worker schickt sie weg und vermerkt, wie es ausging.
|
||
Sonst hinge das Speichern eines Termins am Antwortverhalten von Telegram. Eine
|
||
fehlgeschlagene Zustellung bleibt sichtbar stehen und wird nicht nach Frist
|
||
abgeräumt — sie ist der Hinweis darauf, dass ein Kanal kaputt ist, und der
|
||
verliert seinen Wert nicht durch Zeitablauf.
|
||
|
||
Der Probeknopf steht direkt an jedem Zustellweg und läuft durch dieselbe
|
||
Maschinerie wie alles andere. Ohne ihn merkt jemand erst in drei Wochen, dass
|
||
sein ntfy-Thema einen Tippfehler hat — nämlich dann, wenn die Nachricht wichtig
|
||
gewesen wäre.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `NTFY_ENABLED` | `false` | Push über den eigenen ntfy |
|
||
| `NTFY_URL` | — | Adresse des eigenen ntfy, z. B. `http://ntfy:80` |
|
||
| `NTFY_TOKEN` | — | nur bei eingeschaltetem Zugriffsschutz |
|
||
| `NTFY_PORT` | `8086` | Port des ntfy-Containers (Profil `push`) |
|
||
| `WEBPUSH_ENABLED` | `false` | Push in den Browser |
|
||
| `WEBPUSH_PUBLIC_KEY` | — | aus `npm run vapid`; **ins Backup** |
|
||
| `WEBPUSH_PRIVATE_KEY` | — | dito |
|
||
| `WEBPUSH_KONTAKT` | `APP_URL` | Kontaktweg für den Push-Dienst |
|
||
| `BENACHRICHTIGUNG_LAUF` | `0 7 * * *` | wann erinnert wird |
|
||
| `TERMIN_ERINNERUNG_TAGE` | `7,1` | Vorlauf der Terminerinnerungen |
|
||
| `BENACHRICHTIGUNG_VERSUCHE` | `3` | Zustellversuche, bevor es stehen bleibt |
|
||
| `BENACHRICHTIGUNG_AUFBEWAHRUNG_TAGE` | `90` | Frist für **zugestellte** Nachrichten |
|
||
|
||
Ein neuer öffentlicher VAPID-Schlüssel macht alle bestehenden Push-Abos
|
||
wertlos — jeder müsste Push von Hand neu erlauben, und niemand merkt es, die
|
||
Meldungen bleiben einfach aus. Beide Schlüssel gehören deshalb ins Backup.
|
||
|
||
---
|
||
|
||
## Phase 8 — Lokale KI
|
||
|
||
Ein Satz bestimmt alles, was hier gebaut wurde:
|
||
|
||
> **Nichts, was ein Modell erzeugt, gelangt ungeprüft in einen Nachweis.**
|
||
|
||
Daraus folgt der Aufbau: jede KI-Ausgabe ist zuerst ein eigener Datensatz und
|
||
nicht ein Feld im Zieldokument. Ein Einsatzbericht bekommt seinen Text erst,
|
||
wenn ein Mensch „übernehmen" gedrückt hat — bis dahin liegt der Entwurf daneben
|
||
und ist als Entwurf erkennbar, mit gestricheltem Rahmen und dem Modellnamen
|
||
darüber. Das ist der Unterschied zwischen einer Hilfe und einer Fälschung.
|
||
|
||
Der zweite Grundsatz: **es läuft alles im Haus.** Ollama und faster-whisper als
|
||
Container daneben, kein Anbieter, kein Schlüssel, keine Zeile Text, die das
|
||
Gerätehaus verlässt. Es gibt **keinen Rückfall** auf einen Cloud-Dienst: fehlen
|
||
die Container, fehlt die Funktion. Das ist die einzige Bauart, bei der der Satz
|
||
„die Daten bleiben im Haus" auch dann noch stimmt, wenn der eigene Server aus
|
||
ist.
|
||
|
||
### Ohne Modelle geht alles andere weiter
|
||
|
||
Die wichtigste Eigenschaft der Phase, und die am meisten Arbeit gemacht hat:
|
||
**FLORIAN bleibt ohne Sprachmodell vollständig benutzbar.** Wiki lesen und
|
||
schreiben, Wortsuche, Belege ablegen und von Hand ausfüllen, beide Protokolle —
|
||
alles läuft. Wo etwas fehlt, steht ein Satz, der sagt *was* fehlt und *wie es
|
||
ohne geht*, statt eines kaputten Knopfes.
|
||
|
||
Das ist kein Zugeständnis, sondern der Normalfall: eine Wehr setzt FLORIAN auf,
|
||
lange bevor sie über eine Grafikkarte nachdenkt.
|
||
|
||
### Das Wiki
|
||
|
||
Dienstvorschriften, Ausbildungsunterlagen, Merkblätter — das, was sonst als PDF
|
||
im Ordner steht und im Einsatz niemand findet. Inhalt ist **Markdown**, weil es
|
||
sich ohne Werkzeug lesen lässt, auch in zwanzig Jahren, auch ohne FLORIAN.
|
||
|
||
**Zwei Suchen nebeneinander**, und beide werden gebraucht:
|
||
|
||
| Suche | Findet | Braucht ein Modell |
|
||
|---|---|---|
|
||
| **Wortsuche** | was dasteht — „FwDV 7" findet die Seite mit dieser Zeichenfolge | nein |
|
||
| **Frage** | was gemeint ist — „wie lange hält ein Schlauch" findet den Abschnitt über Ablegereife | ja |
|
||
|
||
Die Frage wird **ausschließlich aus den eigenen Seiten** beantwortet, mit
|
||
Fundstellen darunter. Steht die Antwort nicht drin, sagt FLORIAN genau das. Ein
|
||
Modell, das auf die Frage nach der Prüffrist von Steckleitern eine Zahl nennt,
|
||
die nirgends steht, wäre gefährlicher als gar keine Antwort — deshalb wird ohne
|
||
passende Auszüge gar nicht erst gefragt.
|
||
|
||
Zerlegt wird an den **Überschriften**, nicht nach Zeichenzahl: eine Fundstelle
|
||
soll ein Kapitel sein, kein aus der Mitte gerissener Absatz. Und ein kurzer
|
||
Abschnitt wird nie unter eine fremde Überschrift gehängt — sonst zitierte die
|
||
Antwort eine Fundstelle, in der der Satz gar nicht steht.
|
||
|
||
Jede Änderung schreibt die vorherige Fassung weg. Eine Dienstvorschrift, die
|
||
sich unbemerkt ändert, ist schlimmer als keine.
|
||
|
||
### Diktat
|
||
|
||
Der Weg, den es in jeder Wehr schon gibt: der Einsatzleiter spricht auf der
|
||
Rückfahrt in sein Telefon, und daraus wird abends ein Bericht. Neu ist nur, dass
|
||
abends schon ein Text dasteht.
|
||
|
||
Ein Knopf, keine Einstellungen. Die Aufnahme bleibt abgelegt, solange über den
|
||
Text nicht entschieden ist — wer dem Transkript nicht traut, hört nach. Und das
|
||
Modell darf **nichts verbessern**: „Hohlstrahlrohr" bleibt „Hohlstrahlrohr",
|
||
auch wenn es das Wort nicht kennt.
|
||
|
||
### Berichtsassistent
|
||
|
||
Stichpunkte hinein, Entwurf heraus. Was das Modell **nicht** bekommt, ist ebenso
|
||
wichtig: Zeitstempel und Einsatznummer stehen schon als Felder im Bericht und
|
||
stünden sonst zweimal da, beim zweiten Mal womöglich falsch.
|
||
|
||
Der Entwurf landet nicht im Berichtsfeld. Er steht daneben, gekennzeichnet, mit
|
||
zwei gleich großen Knöpfen — „Übernehmen" ist nicht der Standardweg und sieht
|
||
auch nicht so aus.
|
||
|
||
### Belegerfassung
|
||
|
||
Quittung fotografieren, Angaben prüfen, gegenzeichnen. Alle Felder sind
|
||
änderbar, und was das Modell nicht erkannt hat, bleibt **leer und gelb
|
||
hinterlegt** statt geraten zu werden. Ein leeres Feld ist eine Aufforderung; ein
|
||
geratener Betrag ist eine falsche Buchung, die niemand bemerkt.
|
||
|
||
Die Buchung daraus kommt mit der Kasse in Phase 9 — hier entsteht erst einmal
|
||
ein sauber erfasster Beleg mit Foto.
|
||
|
||
### Zwei Protokolle nebeneinander
|
||
|
||
| Ansicht | Beantwortet |
|
||
|---|---|
|
||
| **KI-Protokoll** | Was hat die Maschine getan? Jede Befragung, jedes Modell, jede Entscheidung mit Namen und Zeitpunkt. |
|
||
| **Protokoll** (Audit-Log) | Was haben Menschen getan? Wird seit Phase 0 geschrieben; die Ansicht dazu kam jetzt. |
|
||
|
||
Eingabe, Ausgabe, Modell und Art eines KI-Vorgangs sind nach dem Anlegen
|
||
**unveränderlich** — ein Trigger hält dagegen. Löschen ist der Anwendungsrolle
|
||
entzogen, 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.
|
||
|
||
### Einrichtung
|
||
|
||
Postgres braucht seit dieser Phase **pgvector** neben PostGIS. Es gibt kein
|
||
offizielles Image mit beidem, deshalb baut FLORIAN eines
|
||
(`docker/postgres/Dockerfile`):
|
||
|
||
```bash
|
||
docker compose build postgres # bei einer bestehenden Installation
|
||
docker compose up -d postgres # Datenverzeichnis bleibt unberührt
|
||
npm run db:migrate
|
||
```
|
||
|
||
Dann die Modelle:
|
||
|
||
```bash
|
||
docker compose --profile ki up -d
|
||
|
||
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 in die `docker-compose.override.yml`:
|
||
|
||
```yaml
|
||
services:
|
||
ollama:
|
||
deploy:
|
||
resources:
|
||
reservations:
|
||
devices:
|
||
- driver: nvidia
|
||
count: all
|
||
capabilities: [gpu]
|
||
```
|
||
|
||
### Ein anderes Einbettungsmodell
|
||
|
||
Die Spalte `wikiabschnitt.einbettung` ist auf **768 Dimensionen** festgelegt —
|
||
das liefert `nomic-embed-text`. Ein Modell mit anderer Länge braucht eine
|
||
Migration:
|
||
|
||
```sql
|
||
ALTER TABLE "wikiabschnitt" DROP COLUMN "einbettung";
|
||
ALTER TABLE "wikiabschnitt" ADD COLUMN "einbettung" vector(1024);
|
||
CREATE INDEX "wikiabschnitt_einbettung_idx"
|
||
ON "wikiabschnitt" USING hnsw ("einbettung" vector_cosine_ops);
|
||
```
|
||
|
||
Danach `LLM_EINBETTUNG_DIMENSIONEN` anpassen und alle Seiten neu einbetten. Der
|
||
nächtliche Lauf holt das von selbst nach; wer nicht warten will, drückt auf
|
||
jeder Seite „jetzt nachziehen".
|
||
|
||
Ohne Anpassung weist FLORIAN das Modell mit einer klaren Meldung ab, statt eine
|
||
Fehlermeldung der Datenbank durchzureichen, die niemand versteht.
|
||
|
||
### Zusätzliche Umgebungsvariablen
|
||
|
||
| Variable | Vorgabe | Bedeutung |
|
||
|---|---|---|
|
||
| `LLM_ENABLED` | `false` | Schaltet alle Textfunktionen ein |
|
||
| `LLM_BASE_URL` | — | OpenAI-kompatibler Endpunkt, z. B. `http://ollama:11434/v1` |
|
||
| `LLM_MODELL` | — | Modell für Berichte und Antworten |
|
||
| `LLM_TIMEOUT` | `120` | Zeitlimit in Sekunden |
|
||
| `LLM_EINBETTUNG_MODELL` | `nomic-embed-text` | Modell für die semantische Suche |
|
||
| `LLM_EINBETTUNG_DIMENSIONEN` | `768` | **Muss zur Spalte passen** |
|
||
| `LLM_VISION_MODELL` | — | Modell für die Belegerfassung |
|
||
| `WHISPER_ENABLED` | `false` | Schaltet das Diktat ein |
|
||
| `WHISPER_URL` | — | z. B. `http://whisper:8000` |
|
||
| `WHISPER_MODELL` | `base` | `tiny`, `base`, `small` |
|
||
| `DIKTAT_MAX_SEKUNDEN` | `600` | Höchstlänge einer Aufnahme |
|
||
| `OLLAMA_PORT` | `11435` | Port des Ollama-Containers (Profil `ki`) |
|
||
| `WHISPER_PORT` | `8087` | Port des Whisper-Containers (Profil `ki`) |
|
||
|
||
---
|
||
|
||
## Befehle
|
||
|
||
```bash
|
||
npm run dev # Entwicklungsserver auf Port 3100
|
||
npm run build # Produktionsbuild
|
||
npm run worker:dev # Hintergrundprozess mit Neuladen
|
||
npm run test # Vitest
|
||
npm run test:e2e # Playwright
|
||
npm run typecheck # TypeScript ohne Ausgabe
|
||
npm run db:migrate # ausstehende Migrationen anwenden
|
||
npm run db:seed # Beispielwehr einspielen
|
||
npm run db:studio # Prisma Studio
|
||
npm run screenshots # Bilder für docs/ANLEITUNG.md erzeugen
|
||
npm run vapid # VAPID-Schlüsselpaar für Web-Push erzeugen
|
||
```
|
||
|
||
## Dienste und Ports (Entwicklung)
|
||
|
||
Bewusst nicht die Standardports, damit FLORIAN neben anderen lokalen Stacks
|
||
laufen kann. Alle Werte stehen in der `.env`.
|
||
|
||
| Dienst | Adresse |
|
||
|---|---|
|
||
| Anwendung | http://localhost:3100 |
|
||
| PostgreSQL 17 + PostGIS | `localhost:5442` |
|
||
| Redis | `localhost:6389` |
|
||
| MinIO API | http://localhost:9010 |
|
||
| MinIO Weboberfläche | http://localhost:9011 |
|
||
| Gotenberg | http://localhost:3101 |
|
||
| Mailpit (nur Entwicklung) | http://localhost:8125 |
|
||
| ntfy (Profil `push`) | http://localhost:8086 |
|
||
| Ollama (Profil `ki`) | http://localhost:11435 |
|
||
| faster-whisper (Profil `ki`) | http://localhost:8087 |
|
||
| Adminer (nur Entwicklung) | http://localhost:8081 |
|
||
|
||
## Produktionsbetrieb
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml up -d
|
||
```
|
||
|
||
Die Angabe von `-f` ist wichtig: ohne sie legt Docker Compose automatisch
|
||
`docker-compose.override.yml` darüber, und das ist die Entwicklungsvariante
|
||
ohne Traefik. In Produktion terminiert Traefik TLS und verteilt auf
|
||
`APP_DOMAIN` und `MINIO_DOMAIN`.
|
||
|
||
Der App-Container wendet beim Start ausstehende Migrationen an; der Worker
|
||
tut das bewusst nicht, damit nicht zwei Prozesse gleichzeitig migrieren.
|
||
|
||
## Datenschutz
|
||
|
||
- Keine Telemetrie, keine externen CDNs, keine Google Fonts. Es werden nur
|
||
Schriften verwendet, die auf dem Gerät vorhanden sind.
|
||
- Aufbewahrungsfristen konfigurierbar (`AUFBEWAHRUNG_JAHRE`,
|
||
`AUDIT_AUFBEWAHRUNG_TAGE`, `PERSON_ANONYMISIERUNG_TAGE`).
|
||
- Externe Dienste (Mail, Telegram, ntfy, Web-Push) sitzen hinter
|
||
Schnittstellen in `src/lib/integrations/` und sind einzeln abschaltbar. Ohne
|
||
Freischaltung wird nur ins Log geschrieben.
|
||
- **Die KI-Funktionen laufen ausschließlich gegen selbst betriebene Modelle.**
|
||
Es gibt keinen Rückfall auf einen Anbieter: fehlt der eigene Endpunkt, fehlt
|
||
die Funktion. Kein Text verlässt das Gerätehaus.
|
||
- Jede KI-Ausgabe ist gekennzeichnet, wird als eigener Datensatz festgehalten
|
||
und muss von einem Menschen übernommen werden, bevor sie in einen Nachweis
|
||
gelangt. Nachvollziehbar im KI-Protokoll.
|
||
- Über Kanäle, die das Netz der Wehr verlassen (Telegram, Web-Push), gehen nur
|
||
Kurzfassungen ohne Namen, Gerätebezeichnungen und Einsatzdetails.
|
||
- Kein WhatsApp — die Begründung steht oben unter „Warum kein WhatsApp".
|
||
|
||
## Sicherheitshinweis
|
||
|
||
Atemschutzüberwachung und Alarmmonitor sind sicherheitsrelevant. Solange die
|
||
Wehr das System nicht ausdrücklich freigegeben hat, bleibt die **analoge
|
||
Überwachung führend**. Jede entsprechende Ansicht zeigt diesen Hinweis
|
||
dauerhaft an.
|