Terminal im Geraetehaus mit Offline-Betrieb ergaenzen

Phase 4: eine eigene Oberflaeche unter /kiosk fuer das Wandtablet in der
Fahrzeughalle — mit Handschuhen bedienbar und arbeitsfaehig, wenn das Netz
weg ist.

Datenmodell: Fahrt, Anwesenheit, Waescheabgabe, Inventurgang,
Inventurposten, KioskVorgang und SyncKonflikt. Die Kennung eines Vorgangs
erzeugt der Browser beim Einreihen und ist damit der Idempotenzschluessel;
kommt derselbe Vorgang nach abgebrochener Uebertragung erneut an, erkennt
ihn der Server wieder. Die Datenbank setzt vier Regeln selbst durch und
verweigert UPDATE und DELETE auf kiosk_vorgang.

Offline: von Hand geschriebener Service Worker, Warteschlange in der
IndexedDB (Dexie), Fotos als Blob daneben, sichtbarer Zustand als Leiste
ueber allem. Was sich beim Nachtragen nicht anwenden laesst, landet als
Konflikt unter /terminal statt verworfen zu werden.

Dazu: Anmeldung per Ausweis, PIN oder signierter QR-Marke vom Handy,
Hallenmonitor mit optionalen DWD-Warnungen, Installation als App.

Behoben: die Warteschlange zog ihren Wiederholungstakt bei jedem Rendern
neu auf und haette einen liegen gebliebenen Vorgang nie ein zweites Mal
geschickt. Gefunden vom Offline-E2E-Test.

31 neue Tests (20 Unit, 11 gegen eine echte Datenbank, 9 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Florian Wilde
2026-09-03 18:35:48 +02:00
co-authored by Claude Opus 5
parent d8d692098d
commit 791523027f
126 changed files with 7787 additions and 40 deletions
+204 -11
View File
@@ -1,10 +1,10 @@
# Anleitung
Bedienung von FLORIAN, Stand Phase 3 (Version 0.4.0). Die Bilder zeigen die
Bedienung von FLORIAN, Stand Phase 4 (Version 0.5.0). Die Bilder zeigen die
Beispielwehr aus dem Seed und werden mit `npm run screenshots` neu erzeugt.
> Atemschutz, Karten und die Kiosk-Oberfläche kommen in den folgenden Phasen
> dazu. Siehe [ROADMAP.md](ROADMAP.md).
> Atemschutz und Karten kommen in den folgenden Phasen dazu. Siehe
> [ROADMAP.md](ROADMAP.md).
---
@@ -603,7 +603,7 @@ Bericht — wird später nachgetragen. Der Einsatz bekommt sofort eine
fortlaufende Nummer im Format `JJJJ-NNNN`.
**Koordinaten** lassen sich über „Koordinaten suchen" ermitteln, wenn die Wehr
einen eigenen Nominatim betreibt (Kapitel 24). Ohne den bleibt das Feld leer —
einen eigenen Nominatim betreibt (Kapitel 30). Ohne den bleibt das Feld leer —
das ist in Ordnung, die Karte kommt in Phase 6.
---
@@ -758,7 +758,194 @@ der in der Jahreshauptversammlung gefragt wird und die bei Beförderungen zählt
---
## 24. Adressen zu Koordinaten (Nominatim)
## 24. Das Terminal im Gerätehaus
![Kachelschirm des Terminals](screenshots/25-kiosk-kacheln-light.png)
Das Terminal ist eine **eigene Oberfläche** unter `/kiosk`, gedacht für ein
Wandtablet neben der Fahrzeughalle. Große Kacheln statt Seitenleiste, alles
Bedienbare mindestens 64 Pixel hoch, jede Kachel mit Symbol **und** Text — das
ist kein Geschmack, sondern die Voraussetzung dafür, dass es mit
Feuerwehrhandschuhen bedienbar bleibt.
### Einmal je Tablet einrichten
Bevor sich jemand anmelden kann, muss das Tablet wissen, zu welcher Wehr es
gehört. Das macht der Administrator einmal unter `/kiosk/einrichten`: Kurzname
der Wehr und das Gerätegeheimnis aus der `.env` (`KIOSK_DEVICE_SECRET`).
Damit am Tablet nichts abgetippt werden muss, nimmt die Seite beides auch aus
der Adresse an:
```
http://florian.example/kiosk/einrichten?wehr=ff-musterhausen&schluessel=…
```
Diesen Link schickt sich der Administrator einmal selbst — etwa als QR-Code vom
eigenen Rechner — und ruft ihn am Tablet auf.
> **Das Gerätegeheimnis ist die erste Schranke.** Ohne eingerichtetes Tablet
> nimmt der Server keine PIN und keine Karte an. Ein fremdes Handy, das
> `/kiosk` aufruft, kommt nicht weiter, auch nicht mit richtiger PIN.
### Anmelden
![Anmeldung am Terminal](screenshots/30-kiosk-anmeldung-light.png)
Drei Wege, in dieser Reihenfolge:
1. **Ausweis auflegen** — der Regelfall. Der Leser am Tablet gibt die
Kartenkennung wie eine Tastatur aus; es muss vorher nichts angetippt werden.
2. **QR-Code vom Handy** — für den, der den Ausweis vergessen hat (Kapitel 29).
3. **Mitgliedsnummer und PIN** — wenn beides fehlt.
Nach `KIOSK_IDLE_TIMEOUT` Sekunden ohne Bedienung (Vorgabe: 120) meldet sich das
Terminal von selbst ab, mit sichtbarer Warnung vorher. Ein Terminal, an dem die
letzte Anmeldung stehen bleibt, schreibt irgendwann alles dem Falschen zu.
### Was auf dem Kachelschirm steht
Die Kacheln richten sich nach zwei Dingen: den Rechten der angemeldeten Person
und ihrem aktuellen Zustand.
| Kachel | Wann sie sich ändert |
|---|---|
| Kommen / **Gehen** | Wer eingecheckt ist, sieht „Gehen" samt Zeit, seit wann |
| Fahrt beginnen / **Fahrt beenden** | Wer mit einem Fahrzeug unterwegs ist, sieht dessen Funkrufname |
| Wäsche abgeben | Zeigt an, wie viele Stücke noch nicht zurück sind |
| Inventur | Nur für den Gerätewart |
| Prüfung durchführen | Ohne Netz gesperrt, mit dem Hinweis „braucht Netz" |
---
## 25. Mangel melden am Terminal
![Mangel melden](screenshots/26-kiosk-mangel-light.png)
Der Vorgang, an dem sich das Terminal misst — Anspruch war **unter dreißig
Sekunden**. Deshalb drei Schritte und kein Formular:
1. **QR-Code am Gerät scannen.** Oder die Inventarnummer eintippen, wenn das
Etikett unleserlich ist. Steht die Nummer im Bestand, erscheint sofort
Bezeichnung und Zustand.
2. **Foto und ein Satz dazu.** Das Foto ist kein Pflichtfeld, macht aber die
halbe Arbeit: „Riss im Schlauch, etwa 20 cm von der Kupplung" plus Bild sagt
dem Gerätewart mehr als jedes Formular.
3. **Dringlichkeit antippen.** Damit ist die Meldung abgeschickt.
| Dringlichkeit | Was passiert |
|---|---|
| Kleinigkeit | kann warten |
| Normal | beim nächsten Dienst |
| Dringend | vor dem nächsten Einsatz |
| **Kritisch** | Das Gerät wird **sofort gesperrt** |
Eine unbekannte Inventarnummer hält die Meldung nicht auf. Sie geht trotzdem
raus und landet notfalls in der Konfliktliste (Kapitel 27) — lieber ein Zettel
zu viel als ein Riss im Schlauch, den niemand gemeldet hat.
---
## 26. Ohne Netz weiterarbeiten
In einer Fahrzeughalle ist das Netz regelmäßig weg: Stahlbeton, Tore aus
Metall, ein Repeater, der irgendwann ausfällt. Das Terminal ist dafür gebaut,
nicht dagegen abgesichert.
Über jeder Terminalseite läuft eine Leiste, die den Zustand in Klartext sagt:
| Leiste | Bedeutung |
|---|---|
| **Verbunden · alles übertragen** | Alles, was erfasst wurde, ist beim Server |
| **Verbunden · 2 Änderungen warten** | Wird gerade übertragen |
| **Kein Netz · 1 Änderung wartet** | Erfasst und gespeichert, wartet auf Netz |
Und so funktioniert es: **jede** erfasste Aktion geht zuerst in eine
Warteschlange im Tablet — auch dann, wenn Netz da ist. Erst von dort geht sie
zum Server. Deshalb ist der Ablauf ohne Netz derselbe wie mit Netz, nur dauert
er länger.
Was das im Betrieb bedeutet:
- **Die Bestätigung kommt sofort.** Wer eine Meldung abschickt, kann das Tablet
weglegen. Es ist gespeichert, auch wenn es noch nicht übertragen ist.
- **Ein Neustart des Tablets schadet nicht.** Die Warteschlange liegt im
Speicher des Browsers, nicht im Arbeitsspeicher.
- **Fotos warten mit.** Das Bild liegt neben der Meldung und wird beim
Übertragen zuerst hochgeladen.
- **Nichts wird doppelt gebucht.** Jeder Vorgang bekommt beim Erfassen eine
eigene Kennung. Bricht die Übertragung mitten drin ab und wird wiederholt,
erkennt der Server dieselbe Kennung wieder und bucht nicht erneut.
- **Die Prüfung braucht Netz.** Ein Prüfprotokoll mit Checkliste, Messwerten,
Fotos und Unterschrift lässt sich nicht sinnvoll zwischenspeichern — ein halb
erfasstes Protokoll wäre schlechter als keins. Die Kachel sagt das, statt ins
Leere zu führen.
Bleibt ein Vorgang mehr als fünf Versuche liegen, lässt er sich über „anzeigen"
einsehen und im Notfall verwerfen. Vorher nicht — und das ist Absicht.
---
## 27. Konflikte nachbearbeiten
![Terminalübersicht](screenshots/28-terminal-uebersicht-light.png)
Ein Tablet, das drei Tage offline war, schickt irgendwann Vorgänge, deren Ziel
es nicht mehr gibt: das Gerät wurde ausgesondert, die Prüfung inzwischen
abgeschlossen und damit festgeschrieben.
FLORIAN erzwingt in diesem Fall nichts und verwirft nichts. Der Vorgang landet
als **Konflikt** in der Übersicht unter `/terminal` — mit Zeitpunkt, Art,
Grund und der Person, die ihn erfasst hat. Der Gerätewart entscheidet und
vermerkt, wie es gelöst wurde.
Auf derselben Seite steht das Übrige, was am Terminal zusammenkommt: wer im
Haus ist, welche Wäsche in der Reinigung ist und das Fahrtenbuch.
---
## 28. Hallenmonitor
![Hallenmonitor](screenshots/27-kiosk-monitor-light.png)
`/kiosk/monitor` ist für einen Fernseher in der Fahrzeughalle gedacht: Vollbild,
große Schrift, **keine Bedienung**. Zu sehen sind Uhrzeit und Datum, ein
laufender Einsatz, wer gerade im Haus ist, die Fahrzeuge samt laufenden Fahrten,
fällige Prüfungen und die nächsten Termine.
Optional dazu **Wetterwarnungen des DWD**:
```bash
# .env
WETTER_ENABLED=true
DWD_WARNCELL=809162000
```
> Das ist die einzige Verbindung, die FLORIAN von sich aus nach außen aufbaut,
> und sie ist deshalb standardmäßig aus. Nach draußen geht nur die
> Warnzellen-Nummer, keine Daten der Wehr. Fällt der Abruf aus, bleibt die
> Kachel leer — eine Wetterwarnung ist eine Zugabe, alarmiert wird über die
> Leitstelle.
Die Nummer der eigenen Gemeinde steht in der
[Warnzellenliste des DWD](https://www.dwd.de/DE/leistungen/opendata/help/warnungen/cap_warncellids_csv.csv).
---
## 29. Anmeldemarke fürs Handy
![Anmeldemarke](screenshots/29-anmeldemarke-light.png)
Wer den Ausweis vergessen hat, öffnet in FLORIAN auf dem Handy die Seite
**Anmeldemarke** und hält den QR-Code vor die Kamera des Terminals.
Der Code gilt **60 Sekunden** und wird von der Seite selbst erneuert. Er ist
signiert und nirgends gespeichert: ein abfotografierter Code aus der Umkleide
nützt eine Minute später nichts mehr.
---
## 30. Adressen zu Koordinaten (Nominatim)
FLORIAN löst Einsatzadressen nur auf, wenn die Wehr einen **eigenen** Nominatim
betreibt:
@@ -778,7 +965,7 @@ GEOCODING_LAENDER=de,at,ch
Ohne Konfiguration bleibt die Suche leer. Koordinaten lassen sich dann von Hand
eintragen oder weglassen.
## 25. Rollen und was sie dürfen
## 31. Rollen und was sie dürfen
Neun Rollen, jede mit einem festen Satz Rechte. Die Zuordnung steht zentral in
`src/lib/authz.ts` und wird beim Seed in die Datenbank gespiegelt — eine Wehr
@@ -807,7 +994,7 @@ Das Terminal selbst ist nur die Tür.
---
## 26. Anmeldung über authentik einrichten
## 32. Anmeldung über authentik einrichten
authentik läuft **getrennt** von FLORIAN und ist nicht Teil dieses Stacks.
@@ -860,9 +1047,10 @@ ganze Wehr aus.
---
## 27. Terminal-Anmeldung im Gerätehaus
## 33. PIN und Ausweiskarte verwalten
Die Oberfläche dazu kommt in Phase 4; der Anmeldeweg steht bereits.
Wie sich am Terminal angemeldet wird, steht in Kapitel 24. Hier geht es darum,
was dahinter liegt.
Am Terminal wird zweifach geprüft:
@@ -882,7 +1070,7 @@ Huber), PIN `471203`.
---
## 28. Was protokolliert wird
## 34. Was protokolliert wird
Jede schreibende Änderung erzeugt einen Eintrag mit Zeitpunkt, Person,
betroffenem Datensatz und den geänderten Feldern im Vorher-Nachher-Vergleich.
@@ -907,7 +1095,7 @@ jeweiligen Detailseite sichtbar.
---
## 29. Wenn etwas nicht geht
## 35. Wenn etwas nicht geht
| Beobachtung | Wahrscheinliche Ursache | Was hilft |
|---|---|---|
@@ -922,6 +1110,11 @@ jeweiligen Detailseite sichtbar.
| Umlaute im Import verstümmelt | Die Datei ist weder UTF-8 noch Windows-1252 | In Excel als „CSV UTF-8" speichern |
| Gerät lässt sich nicht speichern | Standort **und** Fahrzeug gesetzt, oder keines von beidem | Genau eines von beiden angeben |
| `docker compose up` startet Traefik mit | Override-Datei wurde nicht mitgeladen | In Produktion `-f docker-compose.yml` angeben |
| Terminal zeigt „Terminal nicht eingerichtet" | Das Tablet kennt keine Wehr, oder der Browserspeicher wurde geleert | `/kiosk/einrichten` erneut aufrufen |
| Terminal weist jede PIN ab | Gerätegeheimnis stimmt nicht mit `KIOSK_DEVICE_SECRET` überein | Einrichtung mit dem aktuellen Wert wiederholen |
| Die Leiste zeigt dauerhaft wartende Änderungen | Server erreichbar, aber der Vorgang wird abgelehnt | Über „anzeigen" den Grund lesen; landet er als Konflikt, steht er unter `/terminal` |
| „Der Code wurde nicht erkannt" beim QR-Login | Marke älter als 60 Sekunden | Auf dem Handy die Seite Anmeldemarke offen halten, sie erneuert den Code selbst |
| Der Monitor zeigt keine Wetterwarnung | `WETTER_ENABLED` aus oder `DWD_WARNCELL` leer | Beides setzen; ohne Warnzelle wird nichts abgerufen |
Zustand aller Randsysteme auf einen Blick: