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:
co-authored by
Claude Opus 5
parent
d8d692098d
commit
791523027f
@@ -27,7 +27,7 @@ Anmelden mit `SEED_ADMIN_EMAIL` und `SEED_ADMIN_PASSWORT` aus der `.env`
|
||||
| Phase 1 — Stammdaten | fertig |
|
||||
| Phase 2 — Wartung und Prüfung | fertig |
|
||||
| Phase 3 — Einsätze und Übungen | fertig |
|
||||
| Phase 4 — Kiosk-PWA | offen |
|
||||
| Phase 4 — Kiosk-PWA im Gerätehaus | fertig |
|
||||
| Phase 5 — Atemschutz (FwDV 7) | offen |
|
||||
| Phase 6 — Kartenmodule | offen |
|
||||
| Phase 7 — Benachrichtigungen und Termine | offen |
|
||||
@@ -402,6 +402,131 @@ nachgetragen werden.
|
||||
| `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 |
|
||||
|
||||
---
|
||||
|
||||
## Befehle
|
||||
|
||||
Reference in New Issue
Block a user