From b32a60be64f4cdcfe09ad8fc654a7b09f69d7774 Mon Sep 17 00:00:00 2001 From: Dicken Date: Fri, 24 Jul 2026 22:16:12 +0200 Subject: [PATCH] round36: Bestaetigte Scan-Ergebnisse erscheinen nach Neuladen der Seite nicht mehr erneut (betraf alle drei Scanner) --- .../frontend/src/routes/admin/ScannerPage.tsx | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/apps/frontend/src/routes/admin/ScannerPage.tsx b/apps/frontend/src/routes/admin/ScannerPage.tsx index 6f3fdd7..1a508fa 100644 --- a/apps/frontend/src/routes/admin/ScannerPage.tsx +++ b/apps/frontend/src/routes/admin/ScannerPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useRef } from "react"; +import { useState, useEffect } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -71,6 +71,35 @@ function nameChangeKey(c: ScanNameChange): string { 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 * explizit die *EditedManually-Flag auf false zurück, damit der Dienst @@ -311,8 +340,6 @@ function ApiScannerCard() { const [lastNewFindings, setLastNewFindings] = usePersistedState< { serviceId: string; serviceName: string; apis: DetectedApiEntry[] }[] >("scanner:apiNewFindings", []); - const lastHandledStartedAt = useRef(null); - useEffect(() => { let cancelled = false; @@ -328,8 +355,8 @@ function ApiScannerCard() { setStatus( `Prüfe ${s.progress.currentLabel ?? "…"} (${s.progress.current + 1} von ${s.progress.total})` ); - } else if (s.lastResult && s.startedAt !== lastHandledStartedAt.current) { - lastHandledStartedAt.current = s.startedAt; + } else if (s.lastResult && s.startedAt !== getHandledResultId("scanner:apiLastHandledAt")) { + setHandledResultId("scanner:apiLastHandledAt", s.startedAt); const r = s.lastResult; const newCount = r.newFindings.reduce( (sum: number, f: { apis: unknown[] }) => sum + f.apis.length, @@ -456,7 +483,6 @@ export function ScannerPage() { ); const [fritzboxRunning, setFritzboxRunning] = useState(false); - const lastFritzboxHandledAt = useRef(null); useEffect(() => { let cancelled = false; @@ -469,16 +495,16 @@ export function ScannerPage() { setFritzboxRunning(s.running); - if (!s.running && s.lastResult && s.startedAt !== lastFritzboxHandledAt.current) { - lastFritzboxHandledAt.current = s.startedAt; + if (!s.running && s.lastResult && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) { + setHandledResultId("scanner:fritzboxLastHandledAt", s.startedAt); setFritzboxSummary({ found: s.lastResult.found, uniqueDevices: s.lastResult.uniqueDevices }); setFritzboxError(null); setStaleDevices(s.lastResult.staleDevices); setDeviceChanges(s.lastResult.deviceChanges); queryClient.invalidateQueries({ queryKey: ["devices"] }); queryClient.invalidateQueries({ queryKey: ["logs"] }); - } else if (!s.running && s.lastError && s.startedAt !== lastFritzboxHandledAt.current) { - lastFritzboxHandledAt.current = s.startedAt; + } else if (!s.running && s.lastError && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) { + setHandledResultId("scanner:fritzboxLastHandledAt", s.startedAt); setFritzboxError(s.lastError); } } catch { @@ -512,7 +538,6 @@ export function ScannerPage() { // 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 // Backend, ist also für jeden sichtbar, der die Seite gerade offen hat. - const lastHandledResultAt = useRef(null); useEffect(() => { let cancelled = false; @@ -529,8 +554,8 @@ export function ScannerPage() { setBulkStatus( `Scanne ${status.progress.currentLabel ?? "…"} (${status.progress.current + 1} von ${status.progress.total})` ); - } else if (status.lastResult && status.startedAt !== lastHandledResultAt.current) { - lastHandledResultAt.current = status.startedAt; + } else if (status.lastResult && status.startedAt !== getHandledResultId("scanner:bulkLastHandledAt")) { + setHandledResultId("scanner:bulkLastHandledAt", status.startedAt); const r = status.lastResult; const pending = r.nameChanges.length + r.staleServices.length; setBulkStatus(