Files
FireAdministration/src/components/atemschutz/stand.ts
T
Florian WildeandClaude Opus 5 1c1e141666 Kartenmodule ergaenzen
Phase 6: alles, was einen Ort hat — Loeschwasser, Objekte mit Feuerwehrplan,
die Lagekarte eines Einsatzes und die geplante Foerderstrecke.

Keine Kacheln aus dem Netz. Wo eine Wehr ausrueckt und wo ihre Hydranten
liegen, ist niemandes Geschaeft; der Kartenserver laeuft selbst
(tileserver-gl im Compose-Profil "karte"). Ohne ihn zeigt die Karte einen
leeren Grund und sagt das auch — Punkte, Entfernungen und Umkreissuche
funktionieren trotzdem. Dasselbe gilt fuer einen eingetragenen, aber nicht
erreichbaren Server.

Die Umkreissuche laeuft ueber PostGIS: ST_DWithin auf der Geografie mit
GiST-Index rechnet in Metern und nutzt den Index. Gefragt wird waehrend der
Fahrt, und bei tausend Hydranten waere alles andere ein vollstaendiger
Tabellendurchlauf.

Der Import aus GeoJSON und Stadtwerke-CSV ist nachsichtig, aber ehrlich: er
erkennt "DN 100", deutsche Kommazahlen, Datumsangaben und vertauschte Achsen
— raet aber nichts. Fehlt die Position, wird die Zeile uebersprungen; ohne
Statusangabe steht "ungeprueft", nicht "einsatzbereit".

22 taktische Zeichen nach FwDV 102 selbst gezeichnet, die Lagekarte gleicht
ueber denselben Ereignisstrom ab wie die Atemschutzueberwachung.

Die Wasserfoerderung rechnet Reibung, Hoehe und Pumpenzahl; das Ergebnis wird
nie gespeichert, sondern bei jeder Anzeige neu gerechnet. Geht eine Strecke
nicht auf, sagt die Ausgabe das.

Behoben: die Reibungsbeiwerte lagen im ersten Wurf um den Faktor zehn daneben.
Aufgefallen ist es nur, weil die Tests gegen die Tabellenwerte der Ausbildung
pruefen und nicht gegen die eigene Implementierung.

Dazu ein Abruf fuer abgelegte Dateien (/api/dateien/[id]) — bis hierher gab es
nur den Upload.

59 neue Tests (48 Unit, 11 E2E je Browser).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 01:41:24 +02:00

122 lines
4.2 KiB
TypeScript

"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(
kennung: string | null,
beiAenderung: () => void,
/**
* Adresse des Stroms, an die die Kennung angehängt wird.
*
* Vorgabe ist die Atemschutzüberwachung; die Lagekarte aus Phase 6 benutzt
* denselben Hook mit ihrer eigenen Adresse. Zwei Ereignisströme mit
* unterschiedlichem Verhalten wären ein Ort mehr, an dem sich ein Fehler
* verstecken kann.
*/
adresse = "/api/atemschutz/strom?id=",
): { 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 (!kennung || typeof window === "undefined") return;
const quelle = new EventSource(`${adresse}${encodeURIComponent(kennung)}`);
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();
};
}, [kennung, adresse]);
return { verbunden, letzteMeldung };
}