From eef4ddc775c7b41e9abf1fbfccce81a225a09f96 Mon Sep 17 00:00:00 2001 From: Dicken Date: Sun, 26 Jul 2026 14:09:11 +0200 Subject: [PATCH] round60: Delta-Vergleich zum vorherigen Scan (FritzBox + Sammel-Scan) statt statischer Zahlen --- apps/backend/src/routes/scan.ts | 75 ++++++++++++++++++- .../frontend/src/routes/admin/ScannerPage.tsx | 40 +++++++--- 2 files changed, 103 insertions(+), 12 deletions(-) diff --git a/apps/backend/src/routes/scan.ts b/apps/backend/src/routes/scan.ts index 62d4820..f763157 100644 --- a/apps/backend/src/routes/scan.ts +++ b/apps/backend/src/routes/scan.ts @@ -334,6 +334,10 @@ export async function scanRoutes(app: FastifyInstance): Promise { const allStale: DeviceScanResult["staleServices"] = []; const allNameChanges: DeviceScanResult["nameChanges"] = []; const allNew: DeviceScanResult["newServices"] = []; + // Sammelt die IDs ALLER gefundenen Dienste über den kompletten + // Sammel-Scan hinweg - für den Vergleich mit dem vorherigen Lauf (siehe + // weiter unten bei finishJob). + const allServiceIds: string[] = []; let cancelled = false; try { @@ -536,6 +540,7 @@ export async function scanRoutes(app: FastifyInstance): Promise { allStale.push(...result.staleServices); allNameChanges.push(...result.nameChanges); allNew.push(...result.newServices); + allServiceIds.push(...result.services.map((s) => s.id)); return; } @@ -566,6 +571,7 @@ export async function scanRoutes(app: FastifyInstance): Promise { allStale.push(...result.staleServices); allNameChanges.push(...result.nameChanges); allNew.push(...result.newServices); + allServiceIds.push(...result.services.map((s) => s.id)); } catch (err) { // Einzelnes fehlgeschlagenes Gerät soll den Rest nicht // abbrechen - ABER trotzdem persistent loggen, statt @@ -598,6 +604,38 @@ export async function scanRoutes(app: FastifyInstance): Promise { updateJobProgress("devices", scannedCount, null); } + // NEU (round60): Vergleich mit dem VORHERIGEN Sammel-Scan (siehe + // gleiches Prinzip beim FritzBox-Scan oben) - Mengenvergleich der + // gefundenen Dienst-IDs statt reiner Zahlenvergleich. + const previousServiceIdsRaw = settingsRepo.getSetting("deviceScanLastServiceIds"); + const previousServiceIds: string[] | null = previousServiceIdsRaw ? JSON.parse(previousServiceIdsRaw) : null; + const currentServiceIdSet = new Set(allServiceIds); + + let changesSinceLastScan: string; + if (cancelled) { + changesSinceLastScan = "Abgebrochen - kein vollständiger Vergleich möglich."; + } else if (previousServiceIds === null) { + changesSinceLastScan = "Erster Scan - kein Vergleich zu einem vorherigen Lauf möglich."; + } else { + const previousSet = new Set(previousServiceIds); + const newlyAppeared = allServiceIds.filter((id) => !previousSet.has(id)); + const noLongerSeen = previousServiceIds.filter((id) => !currentServiceIdSet.has(id)); + if (newlyAppeared.length === 0 && noLongerSeen.length === 0) { + changesSinceLastScan = "Keine Änderungen seit dem letzten Sammel-Scan."; + } else { + const parts: string[] = []; + if (newlyAppeared.length > 0) parts.push(`${newlyAppeared.length} neue(r) Dienst(e)`); + if (noLongerSeen.length > 0) parts.push(`${noLongerSeen.length} Dienst(e) fehlen jetzt`); + changesSinceLastScan = `Änderungen seit dem letzten Sammel-Scan: ${parts.join(", ")}.`; + } + } + // Bei einem abgebrochenen Lauf NICHT als Vergleichsbasis speichern - + // sonst würde ein unvollständiger Durchlauf fälschlich "Dienste + // fehlen jetzt" für alle noch gar nicht gescannten Geräte anzeigen. + if (!cancelled) { + settingsRepo.setSetting("deviceScanLastServiceIds", JSON.stringify(allServiceIds)); + } + finishJob("devices", { totalDevices: devices.length, scannedCount, @@ -607,6 +645,7 @@ export async function scanRoutes(app: FastifyInstance): Promise { staleServices: allStale, nameChanges: allNameChanges, newServices: allNew, + changesSinceLastScan, }); } catch (err) { // WICHTIG: Vorher wurde ein Abbruch des GESAMTEN Sammel-Scans nur im @@ -794,10 +833,43 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] { return now - lastSeen >= staleThresholdMs; }); + // NEU (round60): Vergleich mit dem VORHERIGEN Scan-Lauf - vorher + // stand hier bei jedem Scan nur eine absolute Zahl ("45 Einträge"), + // egal ob sich seit dem letzten Mal etwas geändert hatte oder nicht + // (siehe Bugreport: "wird immer nur angezeigt wieviel Einträge beim + // letzten Scan waren, das die ganze Zeit"). Die Geräte-IDs des + // letzten erfolgreichen Laufs werden dafür in den Einstellungen + // (app_settings) gemerkt und mit den JETZT gefundenen verglichen - + // ein einfacher Mengenvergleich (nicht nur ein Zahlenvergleich), + // damit "3 raus, 3 rein" nicht fälschlich als "keine Änderung" + // durchgeht. + const previousDeviceIdsRaw = settingsRepo.getSetting("fritzboxScanLastDeviceIds"); + const previousDeviceIds: string[] = previousDeviceIdsRaw ? JSON.parse(previousDeviceIdsRaw) : null; + const currentDeviceIds = devices.map((d) => d.id); + const currentDeviceIdSet = new Set(currentDeviceIds); + + let changesSinceLastScan: string; + if (previousDeviceIds === null) { + changesSinceLastScan = "Erster Scan - kein Vergleich zu einem vorherigen Lauf möglich."; + } else { + const previousSet = new Set(previousDeviceIds); + const newlyAppeared = currentDeviceIds.filter((id) => !previousSet.has(id)); + const noLongerSeen = previousDeviceIds.filter((id) => !currentDeviceIdSet.has(id)); + if (newlyAppeared.length === 0 && noLongerSeen.length === 0) { + changesSinceLastScan = "Keine Änderungen seit dem letzten Scan."; + } else { + const parts: string[] = []; + if (newlyAppeared.length > 0) parts.push(`${newlyAppeared.length} neue(s) Gerät(e)`); + if (noLongerSeen.length > 0) parts.push(`${noLongerSeen.length} Gerät(e) fehlen jetzt`); + changesSinceLastScan = `Änderungen seit dem letzten Scan: ${parts.join(", ")}.`; + } + } + settingsRepo.setSetting("fritzboxScanLastDeviceIds", JSON.stringify(currentDeviceIds)); + logRepo.logScan({ type: "fritzbox", level: "info", - message: `FritzBox-Scan: ${hosts.length} Eintrag/Einträge (${devices.length} eindeutige Geräte)${staleDevices.length > 0 ? `, ${staleDevices.length} nicht mehr gemeldet` : ""}${deviceChanges.length > 0 ? `, ${deviceChanges.length} Änderung(en) vorgeschlagen` : ""}`, + message: `FritzBox-Scan: ${hosts.length} Eintrag/Einträge (${devices.length} eindeutige Geräte)${staleDevices.length > 0 ? `, ${staleDevices.length} nicht mehr gemeldet` : ""}${deviceChanges.length > 0 ? `, ${deviceChanges.length} Änderung(en) vorgeschlagen` : ""} - ${changesSinceLastScan}`, }); finishJob("fritzbox", { @@ -805,6 +877,7 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] { uniqueDevices: devices.length, staleDevices, deviceChanges, + changesSinceLastScan, }); } catch (err) { const detail = err instanceof Error ? err.message : String(err); diff --git a/apps/frontend/src/routes/admin/ScannerPage.tsx b/apps/frontend/src/routes/admin/ScannerPage.tsx index a95253c..b58c8c5 100644 --- a/apps/frontend/src/routes/admin/ScannerPage.tsx +++ b/apps/frontend/src/routes/admin/ScannerPage.tsx @@ -551,7 +551,7 @@ export function ScannerPage() { [] ); const [fritzboxSummary, setFritzboxSummary] = usePersistedState< - { found: number; uniqueDevices: number } | null + { found: number; uniqueDevices: number; changesSinceLastScan?: string } | null >("scanner:fritzboxSummary", null); const [fritzboxError, setFritzboxError] = usePersistedState( "scanner:fritzboxError", @@ -573,7 +573,11 @@ export function ScannerPage() { if (!s.running && s.lastResult && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) { setHandledResultId("scanner:fritzboxLastHandledAt", s.startedAt); - setFritzboxSummary({ found: s.lastResult.found, uniqueDevices: s.lastResult.uniqueDevices }); + setFritzboxSummary({ + found: s.lastResult.found, + uniqueDevices: s.lastResult.uniqueDevices, + changesSinceLastScan: s.lastResult.changesSinceLastScan, + }); setFritzboxError(null); setStaleDevices(s.lastResult.staleDevices); setDeviceChanges(s.lastResult.deviceChanges); @@ -650,7 +654,8 @@ export function ScannerPage() { `${r.created} neue(r) Dienst(e) gefunden.` + (pending > 0 ? ` ${pending} Änderung(en) warten unten auf deine Bestätigung.` - : " Keine Änderungen an bestehenden Diensten vorgeschlagen.") + : " Keine Änderungen an bestehenden Diensten vorgeschlagen.") + + (r.changesSinceLastScan ? ` ${r.changesSinceLastScan}` : "") ); setBulkStaleServices(r.staleServices); setBulkNameChanges(r.nameChanges); @@ -730,14 +735,27 @@ export function ScannerPage() { {fritzboxRunning ? "Scanne …" : "FritzBox jetzt scannen"} {fritzboxSummary ? ( -

- {fritzboxSummary.found} Eintrag/Einträge von der FritzBox,{" "} - {fritzboxSummary.uniqueDevices} eindeutige Geräte - {fritzboxSummary.found !== fritzboxSummary.uniqueDevices - ? " (mehrere Einträge desselben Geräts wurden zusammengeführt)" - : ""} - . -

+
+ {fritzboxSummary.changesSinceLastScan ? ( +

+ {fritzboxSummary.changesSinceLastScan} +

+ ) : null} +

+ {fritzboxSummary.found} Eintrag/Einträge von der FritzBox,{" "} + {fritzboxSummary.uniqueDevices} eindeutige Geräte + {fritzboxSummary.found !== fritzboxSummary.uniqueDevices + ? " (mehrere Einträge desselben Geräts wurden zusammengeführt)" + : ""} + . +

+
) : null} {fritzboxError ?

{fritzboxError}

: null}