round36: Bestaetigte Scan-Ergebnisse erscheinen nach Neuladen der Seite nicht mehr erneut (betraf alle drei Scanner)

This commit is contained in:
2026-07-24 22:16:12 +02:00
parent 645dcf3af8
commit b32a60be64

View File

@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Link } from "@tanstack/react-router"; import { Link } from "@tanstack/react-router";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@@ -71,6 +71,35 @@ function nameChangeKey(c: ScanNameChange): string {
return `${c.serviceId}:${c.field}`; return `${c.serviceId}:${c.field}`;
} }
/**
* Merkt sich (dauerhaft über sessionStorage, NICHT über React-State), welches
* Backend-Scan-Ergebnis (identifiziert über seinen startedAt-Zeitstempel)
* bereits in die lokale Anzeige übernommen wurde. Bewusst kein
* usePersistedState/useState hier: der Poll-Callback läuft in einem
* setInterval, das nur einmal erzeugt wird (leeres deps-Array) - ein
* React-State-Wert wäre in diesem Callback für immer auf seinem Anfangswert
* eingefroren (stale closure). Gleichzeitig darf der Wert NICHT nur im
* Speicher (useRef) leben, sonst würde ein Neuladen der Seite ihn auf null
* zurücksetzen und dieselbe Meldung (inkl. bereits bestätigter Einträge)
* einfach erneut aus dem immer noch unveränderten Backend-Ergebnis
* importieren - genau das gemeldete Problem.
*/
function getHandledResultId(key: string): string | null {
try {
return sessionStorage.getItem(`launchpad:${key}`);
} catch {
return null;
}
}
function setHandledResultId(key: string, value: string): void {
try {
sessionStorage.setItem(`launchpad:${key}`, value);
} catch {
// Speicher evtl. nicht verfügbar (Privatmodus o. ä.) - dann greift die
// Deduplizierung für die aktuelle Sitzung eben nicht, kein Absturz nötig.
}
}
/** /**
* Übernimmt einen Namens-/Kategorie-Vorschlag des Scanners. Setzt dabei * Übernimmt einen Namens-/Kategorie-Vorschlag des Scanners. Setzt dabei
* explizit die *EditedManually-Flag auf false zurück, damit der Dienst * explizit die *EditedManually-Flag auf false zurück, damit der Dienst
@@ -311,8 +340,6 @@ function ApiScannerCard() {
const [lastNewFindings, setLastNewFindings] = usePersistedState< const [lastNewFindings, setLastNewFindings] = usePersistedState<
{ serviceId: string; serviceName: string; apis: DetectedApiEntry[] }[] { serviceId: string; serviceName: string; apis: DetectedApiEntry[] }[]
>("scanner:apiNewFindings", []); >("scanner:apiNewFindings", []);
const lastHandledStartedAt = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -328,8 +355,8 @@ function ApiScannerCard() {
setStatus( setStatus(
`Prüfe ${s.progress.currentLabel ?? "…"} (${s.progress.current + 1} von ${s.progress.total})` `Prüfe ${s.progress.currentLabel ?? "…"} (${s.progress.current + 1} von ${s.progress.total})`
); );
} else if (s.lastResult && s.startedAt !== lastHandledStartedAt.current) { } else if (s.lastResult && s.startedAt !== getHandledResultId("scanner:apiLastHandledAt")) {
lastHandledStartedAt.current = s.startedAt; setHandledResultId("scanner:apiLastHandledAt", s.startedAt);
const r = s.lastResult; const r = s.lastResult;
const newCount = r.newFindings.reduce( const newCount = r.newFindings.reduce(
(sum: number, f: { apis: unknown[] }) => sum + f.apis.length, (sum: number, f: { apis: unknown[] }) => sum + f.apis.length,
@@ -456,7 +483,6 @@ export function ScannerPage() {
); );
const [fritzboxRunning, setFritzboxRunning] = useState(false); const [fritzboxRunning, setFritzboxRunning] = useState(false);
const lastFritzboxHandledAt = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -469,16 +495,16 @@ export function ScannerPage() {
setFritzboxRunning(s.running); setFritzboxRunning(s.running);
if (!s.running && s.lastResult && s.startedAt !== lastFritzboxHandledAt.current) { if (!s.running && s.lastResult && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) {
lastFritzboxHandledAt.current = s.startedAt; setHandledResultId("scanner:fritzboxLastHandledAt", s.startedAt);
setFritzboxSummary({ found: s.lastResult.found, uniqueDevices: s.lastResult.uniqueDevices }); setFritzboxSummary({ found: s.lastResult.found, uniqueDevices: s.lastResult.uniqueDevices });
setFritzboxError(null); setFritzboxError(null);
setStaleDevices(s.lastResult.staleDevices); setStaleDevices(s.lastResult.staleDevices);
setDeviceChanges(s.lastResult.deviceChanges); setDeviceChanges(s.lastResult.deviceChanges);
queryClient.invalidateQueries({ queryKey: ["devices"] }); queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] }); queryClient.invalidateQueries({ queryKey: ["logs"] });
} else if (!s.running && s.lastError && s.startedAt !== lastFritzboxHandledAt.current) { } else if (!s.running && s.lastError && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) {
lastFritzboxHandledAt.current = s.startedAt; setHandledResultId("scanner:fritzboxLastHandledAt", s.startedAt);
setFritzboxError(s.lastError); setFritzboxError(s.lastError);
} }
} catch { } catch {
@@ -512,7 +538,6 @@ export function ScannerPage() {
// die Seite offen ist - egal ob DIESER Browser den Scan gestartet hat oder // die Seite offen ist - egal ob DIESER Browser den Scan gestartet hat oder
// ein anderes Gerät: der Fortschritt/das letzte Ergebnis kommt direkt vom // ein anderes Gerät: der Fortschritt/das letzte Ergebnis kommt direkt vom
// Backend, ist also für jeden sichtbar, der die Seite gerade offen hat. // Backend, ist also für jeden sichtbar, der die Seite gerade offen hat.
const lastHandledResultAt = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -529,8 +554,8 @@ export function ScannerPage() {
setBulkStatus( setBulkStatus(
`Scanne ${status.progress.currentLabel ?? "…"} (${status.progress.current + 1} von ${status.progress.total})` `Scanne ${status.progress.currentLabel ?? "…"} (${status.progress.current + 1} von ${status.progress.total})`
); );
} else if (status.lastResult && status.startedAt !== lastHandledResultAt.current) { } else if (status.lastResult && status.startedAt !== getHandledResultId("scanner:bulkLastHandledAt")) {
lastHandledResultAt.current = status.startedAt; setHandledResultId("scanner:bulkLastHandledAt", status.startedAt);
const r = status.lastResult; const r = status.lastResult;
const pending = r.nameChanges.length + r.staleServices.length; const pending = r.nameChanges.length + r.staleServices.length;
setBulkStatus( setBulkStatus(