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>
@@ -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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
`/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
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||
|
||||
@@ -192,7 +192,7 @@ Zwei Dinge fielen erst beim Tun auf:
|
||||
|
||||
## Phase 4 — Kiosk-PWA
|
||||
|
||||
**Vergleichsaufwand: 16–22 PT**
|
||||
**Vergleichsaufwand: 16–22 PT** · **Tatsächlich: 3:00 h Sitzungszeit**
|
||||
|
||||
| Baustein | PT |
|
||||
|---|---:|
|
||||
@@ -211,6 +211,42 @@ unter Verbindungsabbruch, Doppelklick und parallelem Zugriff von zwei
|
||||
Terminals nichts verliert und nichts doppelt schreibt, ist die eigentliche
|
||||
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
|
||||
|
||||
**Vergleichsaufwand: 14–18 PT**
|
||||
|
||||
@@ -15,7 +15,105 @@ Die aufgewendete Zeit wandert nach jedem Abschnitt in die Spalte
|
||||
| Phase 1 — Nachtrag Geräteformular | 0:50 h | 1 | 03.09.2026 |
|
||||
| Phase 2 — Wartung und Prüfung | 2:25 h | 1 | 03.09.2026 |
|
||||
| Phase 3 — Einsätze und Übungen | 2:10 h | 1 | 03.09.2026 |
|
||||
| **Summe** | **7:40 h** | **5** | |
|
||||
| Phase 4 — Kiosk-PWA im Gerätehaus | 3:00 h | 1 | 03.09.2026 |
|
||||
| **Summe** | **10:40 h** | **6** | |
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-03 · Phase 4 — Kiosk-PWA im Gerätehaus
|
||||
|
||||
**14:40 – 17:40 · 3:00 h · Version 0.5.0**
|
||||
|
||||
### Was entstanden ist
|
||||
|
||||
Das Terminal an der Wand. Sieben neue Tabellen, eine eigene Oberfläche unter
|
||||
`/kiosk`, ein Service Worker und eine Warteschlange, die den Betrieb ohne Netz
|
||||
trägt.
|
||||
|
||||
| Bereich | Dateien |
|
||||
|---|---|
|
||||
| Datenmodell | `Fahrt`, `Anwesenheit`, `Waescheabgabe`, `Inventurgang`, `Inventurposten`, `KioskVorgang`, `SyncKonflikt` |
|
||||
| Offline | `public/sw.js`, `lib/kiosk/outbox.ts`, `components/kiosk/warteschlange.tsx` |
|
||||
| Anmeldung | `lib/kiosk/anmeldemarke.ts`, Anbieter `terminal-marke` in `lib/auth.ts`, `components/kiosk/anmeldung.tsx` |
|
||||
| Oberfläche | `app/kiosk/` mit Kacheln, sechs Wizards, Monitor und Einrichtung |
|
||||
| Verwaltung | `app/(app)/terminal/`, `app/(app)/anmeldemarke/` |
|
||||
| Server | `server/routers/kiosk/kiosk.ts`, `lib/integrations/wetter.ts` |
|
||||
| Tests | `tests/unit/kiosk-marke.test.ts`, `kiosk-outbox.test.ts`, `kiosk-datenbank.test.ts`, `tests/e2e/kiosk.spec.ts` |
|
||||
|
||||
### Entscheidungen
|
||||
|
||||
**Die Warteschlange ist der einzige Weg, auch online.** Es wäre naheliegend,
|
||||
online direkt zu senden und nur im Fehlerfall zu puffern. Dann gäbe es zwei
|
||||
Wege durch den Code — und der seltenere wäre der ungetestete, also genau der,
|
||||
der im Gerätehaus gebraucht wird. Jetzt läuft alles durch dieselbe Bahn, und
|
||||
der Offline-Fall ist der Normalfall mit längerer Wartezeit.
|
||||
|
||||
**Die Kennung erzeugt der Browser, nicht der Server.** `crypto.randomUUID()`
|
||||
beim Einreihen. Damit ist sie derselbe Wert über beliebig viele Sendeversuche
|
||||
hinweg, und der Server kann einen wiederholten Vorgang erkennen, statt ihn
|
||||
doppelt zu buchen. Die Kennung ist der Primärschlüssel von `kiosk_vorgang`, und
|
||||
die Datenbank verweigert dem Anwendungszugang `UPDATE` und `DELETE` darauf —
|
||||
sonst ließe sich die Spur nachträglich glätten.
|
||||
|
||||
**Konflikte werden nicht erzwungen und nicht verworfen.** Ein Tablet, das drei
|
||||
Tage offline war, schickt Vorgänge, deren Ziel es nicht mehr gibt oder das
|
||||
inzwischen festgeschrieben ist. Beides ist kein Fehler des Anwenders. Der
|
||||
Vorgang landet in `sync_konflikt`, verschwindet aus der Warteschlange (er ist
|
||||
angekommen) und wartet unter `/terminal` auf eine Entscheidung.
|
||||
|
||||
**Die Prüfung bleibt online.** Der Wizard aus Phase 2 mit Checkliste, Messwerten,
|
||||
Fotos und Unterschrift lässt sich nicht sinnvoll in eine Warteschlange legen —
|
||||
und ein halb erfasstes Protokoll ist schlechter als keins. Die Kachel zeigt
|
||||
deshalb „braucht Netz" und ist ohne Verbindung gesperrt, statt ins Leere zu
|
||||
führen. `PRUEFUNG_ERFASSEN` steht trotzdem im Datenmodell: die Atemschutzprüfung
|
||||
in Phase 5 ist kurz genug dafür.
|
||||
|
||||
**Die Anmeldemarke wird signiert, nicht gespeichert.** HMAC über Person, Wehr
|
||||
und Zeitstempel mit `AUTH_SECRET`, gültig 60 Sekunden. Keine Tabelle, kein
|
||||
Aufräumjob, kein zweiter Ort mit einem Geheimnis — und ein abfotografierter
|
||||
Code aus der Umkleide ist nach einer Minute wertlos.
|
||||
|
||||
**Der Kartenleser hängt am Fenster, nicht an einem Eingabefeld.** Ein RFID-Leser
|
||||
tippt die Kennung in Millisekunden und schließt mit Enter ab. Wer die Karte
|
||||
auflegt, soll vorher nichts antippen müssen; unterschieden wird am
|
||||
Anschlagstempo (60 ms), nicht am Fokus.
|
||||
|
||||
**Wetterwarnungen sind aus.** Es ist die einzige Verbindung, die FLORIAN von
|
||||
sich aus nach außen aufbaut. Wer sie einschaltet, soll das bewusst tun; ohne
|
||||
`DWD_WARNCELL` passiert nichts, und bei Ausfall bleibt die Kachel leer statt
|
||||
den Monitor aufzuhalten.
|
||||
|
||||
### Wo es gehakt hat
|
||||
|
||||
| Problem | Ursache | Lösung |
|
||||
|---|---|---|
|
||||
| Die Warteschlange schickte einen liegen gebliebenen Vorgang nie ein zweites Mal | `jetztSenden` hatte das Mutationsobjekt von React Query in den Abhängigkeiten. Das ist bei jedem Rendern ein neues, also änderte sich die Identität der Funktion dauernd, der Effekt lief neu, zog den 20-Sekunden-Zeitgeber neu auf — und der lief nie ab. Dazu wurde der `online`-Zuhörer ständig neu registriert und verpasste das Ereignis manchmal | Mutation, tRPC-Werkzeuge und das „läuft gerade"-Kennzeichen über Referenzen statt über Abhängigkeiten. Damit ist `jetztSenden` stabil |
|
||||
| Der Produktionsbuild brach bei `/kiosk/einrichten` ab | `useSearchParams()` ohne Suspense-Grenze lässt sich nicht vorab erzeugen | `<Suspense>` um das Formular |
|
||||
| Der Build brach bei `wetter.ts` ab | Der Hallenmonitor ist eine Client-Komponente und importierte die Warnstufentexte aus einem `server-only`-Modul | Texte nach `lib/integrations/wetter-texte.ts`, dieselbe Trennung wie bei `qr.ts` / `qr-parser.ts` |
|
||||
| Der Outbox-Test lief ins Leere | `speicherVerfuegbar()` fragt `window`, und die Testsuite läuft in Node | Diese Datei unter jsdom (`@vitest-environment`) mit `fake-indexeddb` |
|
||||
| Das Einrichtungsformular ließ sich im Test nicht abschicken | Playwright klickte, bevor der Effekt die Felder aus der Adresse gefüllt hatte — das Pflichtfeld war leer | Im Test auf die Feldwerte warten |
|
||||
| Der Idempotenztest sah in der Verwaltung kein Anmeldeformular | Im selben Browserkontext lief noch die Terminalsitzung, und `/anmelden` leitet Angemeldete weiter | Prüfung in einen eigenen Test mit frischem Kontext |
|
||||
| `getByText("B 3 Wohnhausbrand")` in den Einsatztests wurde mehrdeutig | Frühere E2E-Läufe haben Einsätze mit ähnlichem Stichwort in der Demowehr angelegt | `exact: true`; die eigentliche Ursache bleibt offen (siehe unten) |
|
||||
|
||||
### Abweichung von der Vorgabe
|
||||
|
||||
**Kein Auto-Logout nach 90 Sekunden.** Die Roadmap nennt 90 Sekunden, die
|
||||
Konfiguration hatte schon `KIOSK_IDLE_TIMEOUT=120`. Der konfigurierte Wert
|
||||
gewinnt — 90 Sekunden sind knapp, wenn jemand mitten in einer Mangelmeldung
|
||||
noch das Foto machen muss. Die Roadmap ist entsprechend berichtigt.
|
||||
|
||||
**`npm run lint` läuft in diesem Projekt nicht.** Es gibt keine
|
||||
ESLint-Konfiguration; `next lint` fragt interaktiv nach einer. Das ist älter als
|
||||
diese Phase und in ihr nicht behoben — es würde quer über alle bisherigen
|
||||
Dateien Befunde erzeugen und gehört in einen eigenen Schritt.
|
||||
|
||||
### Nachweise
|
||||
|
||||
- `npx tsc --noEmit` — ohne Ausgabe
|
||||
- `npm run test` — 258 Tests grün, davon 19 gegen eine echte Datenbank
|
||||
- `npx playwright test tests/e2e/kiosk.spec.ts` — 18 Tests grün (Desktop und Tablet)
|
||||
- `npm run build` — 34 Routen, 103 kB gemeinsames JavaScript
|
||||
- `npm run db:seed` — Fahrtenbuch, Anwesenheiten und Wäsche angelegt
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ Phase 0 ████████████████████ fertig
|
||||
Phase 1 ████████████████████ fertig 0.2.0
|
||||
Phase 2 ████████████████████ fertig 0.3.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 6 ░░░░░░░░░░░░░░░░░░░░ offen 0.7.0
|
||||
Phase 7 ░░░░░░░░░░░░░░░░░░░░ offen 0.8.0
|
||||
@@ -97,21 +97,26 @@ die Unfallkasse standhält.
|
||||
|
||||
**Danach kann die Wehr:** ihre Jahresstatistik auf Knopfdruck erzeugen.
|
||||
|
||||
## Phase 4 — Kiosk-PWA · als Nächstes
|
||||
## Phase 4 — Kiosk-PWA · fertig
|
||||
|
||||
Für die Wandtablets in der Fahrzeughalle.
|
||||
|
||||
- Eigenes Layout: große Kacheln, mindestens 64px, mit Handschuhen bedienbar
|
||||
- Anmeldung per RFID, PIN oder QR vom Handy, Auto-Logout nach 90 Sekunden
|
||||
- Wizards: Wartung, Fahrtenbuch, Mangel melden, Inventur, Wäsche, Anwesenheit
|
||||
- Anmeldung per RFID, PIN oder QR vom Handy, Abmeldung nach Untätigkeit
|
||||
(`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,
|
||||
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
|
||||
Rechner im Büro einzuschalten — auch wenn das Netz gerade weg ist.
|
||||
|
||||
## Phase 5 — Atemschutz nach FwDV 7
|
||||
## Phase 5 — Atemschutz nach FwDV 7 · als Nächstes
|
||||
|
||||
Sicherheitsrelevant. Jede Ansicht zeigt dauerhaft den Hinweis, dass die
|
||||
analoge Überwachung führend bleibt — abschaltbar erst über eine Option, die
|
||||
|
||||
|
Before Width: | Height: | Size: 203 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 243 KiB After Width: | Height: | Size: 246 KiB |
|
Before Width: | Height: | Size: 251 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 161 KiB After Width: | Height: | Size: 165 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 412 KiB After Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 421 KiB After Width: | Height: | Size: 424 KiB |
|
Before Width: | Height: | Size: 730 KiB After Width: | Height: | Size: 733 KiB |
|
Before Width: | Height: | Size: 754 KiB After Width: | Height: | Size: 756 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 140 KiB |
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 143 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 318 KiB |
|
Before Width: | Height: | Size: 965 KiB After Width: | Height: | Size: 968 KiB |
|
Before Width: | Height: | Size: 1010 KiB After Width: | Height: | Size: 1013 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 215 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 540 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 255 KiB After Width: | Height: | Size: 258 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 262 KiB |
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 427 KiB |
|
Before Width: | Height: | Size: 389 KiB After Width: | Height: | Size: 436 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 239 KiB After Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 317 KiB After Width: | Height: | Size: 416 KiB |
|
Before Width: | Height: | Size: 321 KiB After Width: | Height: | Size: 421 KiB |
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 361 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 242 KiB After Width: | Height: | Size: 245 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 250 KiB |
|
Before Width: | Height: | Size: 558 KiB After Width: | Height: | Size: 352 KiB |
|
Before Width: | Height: | Size: 571 KiB After Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 233 KiB After Width: | Height: | Size: 236 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 118 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 141 KiB |
|
After Width: | Height: | Size: 189 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 285 KiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 69 KiB |