Atemschutzueberwachung nach FwDV 7 ergaenzen
Phase 5: das sicherheitsrelevanteste Modul des Projekts. Ueber jeder Ansicht steht dauerhaft, dass die analoge Atemschutzueberwachung fuehrend bleibt — abschaltbar nur zusammen mit der Fundstelle einer Freigabe des Kommandanten. Es gibt keinen Timer. In der Datenbank stehen Zeitstempel; wie lange ein Trupp unter Atemschutz ist, rechnet jede Ansicht bei jedem Bild neu aus. Damit ist die Anforderung "uebersteht Neuladen und Verbindungsabbruch" keine Funktion, die gelingen muss, sondern eine Eigenschaft, die sich nicht verlieren laesst. Server und Browser rechnen mit denselben Funktionen. Der Rueckzugsdruck folgt dem doppelten Anmarschverbrauch: was der Weg hinein gekostet hat, kostet der Weg zurueck noch einmal. Bis der Trupp das Einsatzziel meldet, greift die Zwei-Drittel-Regel — sie liegt hoeher und ist damit die vorsichtigere Annahme. Das Protokoll ist doppelt gesichert: entzogene Rechte und ein Trigger auf druckmessung und atemschutz_ereignis. Ein Fehler wird durch einen neuen Eintrag richtiggestellt, nicht durch Aendern. Verteilt wird ueber Server-Sent Events statt Socket.IO — es geht nur in eine Richtung, und so bleibt das Deployment bei "next start". Behoben: ein abgerissener Ereignisstrom meldete sich nicht. Die Anzeige haette weiter "Verbunden" behauptet, waehrend der Draht tot war. Jetzt gilt Stille von acht Sekunden als Abriss. Dazu zwei Altlasten: der Navigationseintrag "Protokoll" fuehrte ins Leere, und die Startseite stand noch auf Phase 1. 68 neue Tests (51 Unit, 17 gegen eine echte Datenbank, 8 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
b2b31ed00a
commit
4e2daaafd4
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
/**
|
||||
* Die laufende Uhr der Überwachung.
|
||||
*
|
||||
* Ein Hook, der einmal je Sekunde einen neuen Zeitpunkt liefert — mehr nicht.
|
||||
* **Er zählt nichts herunter und merkt sich nichts.** Alle Dauern entstehen in
|
||||
* den Ansichten aus zwei Zeitstempeln: dem vom Server und diesem hier.
|
||||
*
|
||||
* Genau daran hängt die wichtigste Anforderung dieser Phase: ein Neuladen des
|
||||
* Browsers und ein Verbindungsabbruch dürfen die Anzeige nicht verfälschen.
|
||||
* Ein heruntergezählter Wert wäre nach einem Neuladen verloren; ein
|
||||
* Zeitstempel aus der Datenbank ist es nicht.
|
||||
*/
|
||||
export function useJetzt(taktMs = 1000): Date {
|
||||
const [jetzt, setJetzt] = React.useState<Date>(() => new Date());
|
||||
|
||||
React.useEffect(() => {
|
||||
const zeitgeber = setInterval(() => setJetzt(new Date()), taktMs);
|
||||
return () => clearInterval(zeitgeber);
|
||||
}, [taktMs]);
|
||||
|
||||
return jetzt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Der Ereignisstrom der Überwachung.
|
||||
*
|
||||
* Verbindet sich mit /api/atemschutz/strom und ruft `beiAenderung` auf, sobald
|
||||
* sich am Serverstand etwas getan hat. Bricht die Verbindung ab, verbindet
|
||||
* `EventSource` von selbst neu — deshalb steht hier keine Zeile
|
||||
* Wiederverbindungslogik.
|
||||
*
|
||||
* Zusätzlich meldet der Hook, ob die Verbindung gerade steht. Das ist kein
|
||||
* Beiwerk: wer am Bildschirm nicht sieht, dass die Verbindung weg ist, hält
|
||||
* einen alten Stand für den aktuellen. Steht sie nicht, fragt die Ansicht in
|
||||
* einem festen Takt nach — der Strom macht sie schneller, nicht erst
|
||||
* funktionsfähig.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Nach dieser Stille gilt die Verbindung als tot.
|
||||
*
|
||||
* Der Server schickt alle zwei Sekunden ein Lebenszeichen. Bleibt es aus, ist
|
||||
* der Draht weg — auch wenn der Browser das nicht meldet. Genau das ist der
|
||||
* Fall, den ein E2E-Test aufgedeckt hat: wird das Netz gekappt, während der
|
||||
* Strom offen ist, schweigt `EventSource` und meldet **keinen** Fehler. Ohne
|
||||
* diese Frist sähe eine tote Verbindung aus wie eine gesunde, und das ist der
|
||||
* gefährlichste Zustand, den diese Ansicht haben kann.
|
||||
*/
|
||||
const STILLE_MS = 8000;
|
||||
|
||||
export function useEreignisstrom(
|
||||
ueberwachungId: string | null,
|
||||
beiAenderung: () => void,
|
||||
): { verbunden: boolean; letzteMeldung: Date | null } {
|
||||
const [verbunden, setVerbunden] = React.useState(false);
|
||||
const [letzteMeldung, setLetzteMeldung] = React.useState<Date | null>(null);
|
||||
|
||||
// Als Referenz, damit die Wache unten nicht bei jeder Meldung neu aufgezogen
|
||||
// wird — dieselbe Falle wie in der Warteschlange des Terminals.
|
||||
const letzteRef = React.useRef<number>(0);
|
||||
|
||||
// Über eine Referenz, damit ein neuer Rückruf nicht die Verbindung neu
|
||||
// aufbaut — sonst risse der Strom bei jedem Rendern ab.
|
||||
const rueckruf = React.useRef(beiAenderung);
|
||||
rueckruf.current = beiAenderung;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!ueberwachungId || typeof window === "undefined") return;
|
||||
|
||||
const quelle = new EventSource(
|
||||
`/api/atemschutz/strom?id=${encodeURIComponent(ueberwachungId)}`,
|
||||
);
|
||||
|
||||
const gehoert = () => {
|
||||
letzteRef.current = Date.now();
|
||||
setVerbunden(true);
|
||||
setLetzteMeldung(new Date());
|
||||
};
|
||||
|
||||
quelle.onopen = gehoert;
|
||||
|
||||
quelle.addEventListener("aenderung", () => {
|
||||
gehoert();
|
||||
rueckruf.current();
|
||||
});
|
||||
|
||||
quelle.addEventListener("takt", gehoert);
|
||||
|
||||
quelle.onerror = () => {
|
||||
// EventSource versucht es von selbst wieder. Hier wird nur vermerkt,
|
||||
// dass der Stand gerade nicht nachgeführt wird.
|
||||
setVerbunden(false);
|
||||
};
|
||||
|
||||
// Die Wache: bleibt das Lebenszeichen aus, gilt die Verbindung als tot —
|
||||
// unabhaengig davon, was EventSource meldet.
|
||||
const wache = setInterval(() => {
|
||||
if (letzteRef.current > 0 && Date.now() - letzteRef.current > STILLE_MS) {
|
||||
setVerbunden(false);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(wache);
|
||||
quelle.close();
|
||||
};
|
||||
}, [ueberwachungId]);
|
||||
|
||||
return { verbunden, letzteMeldung };
|
||||
}
|
||||
Reference in New Issue
Block a user