round60: Delta-Vergleich zum vorherigen Scan (FritzBox + Sammel-Scan) statt statischer Zahlen

This commit is contained in:
2026-07-26 14:09:11 +02:00
parent a7036849b1
commit eef4ddc775
2 changed files with 103 additions and 12 deletions

View File

@@ -334,6 +334,10 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
const allStale: DeviceScanResult["staleServices"] = []; const allStale: DeviceScanResult["staleServices"] = [];
const allNameChanges: DeviceScanResult["nameChanges"] = []; const allNameChanges: DeviceScanResult["nameChanges"] = [];
const allNew: DeviceScanResult["newServices"] = []; 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; let cancelled = false;
try { try {
@@ -536,6 +540,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
allStale.push(...result.staleServices); allStale.push(...result.staleServices);
allNameChanges.push(...result.nameChanges); allNameChanges.push(...result.nameChanges);
allNew.push(...result.newServices); allNew.push(...result.newServices);
allServiceIds.push(...result.services.map((s) => s.id));
return; return;
} }
@@ -566,6 +571,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
allStale.push(...result.staleServices); allStale.push(...result.staleServices);
allNameChanges.push(...result.nameChanges); allNameChanges.push(...result.nameChanges);
allNew.push(...result.newServices); allNew.push(...result.newServices);
allServiceIds.push(...result.services.map((s) => s.id));
} catch (err) { } catch (err) {
// Einzelnes fehlgeschlagenes Gerät soll den Rest nicht // Einzelnes fehlgeschlagenes Gerät soll den Rest nicht
// abbrechen - ABER trotzdem persistent loggen, statt // abbrechen - ABER trotzdem persistent loggen, statt
@@ -598,6 +604,38 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
updateJobProgress("devices", scannedCount, null); 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", { finishJob("devices", {
totalDevices: devices.length, totalDevices: devices.length,
scannedCount, scannedCount,
@@ -607,6 +645,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
staleServices: allStale, staleServices: allStale,
nameChanges: allNameChanges, nameChanges: allNameChanges,
newServices: allNew, newServices: allNew,
changesSinceLastScan,
}); });
} catch (err) { } catch (err) {
// WICHTIG: Vorher wurde ein Abbruch des GESAMTEN Sammel-Scans nur im // WICHTIG: Vorher wurde ein Abbruch des GESAMTEN Sammel-Scans nur im
@@ -794,10 +833,43 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
return now - lastSeen >= staleThresholdMs; 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({ logRepo.logScan({
type: "fritzbox", type: "fritzbox",
level: "info", 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", { finishJob("fritzbox", {
@@ -805,6 +877,7 @@ function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
uniqueDevices: devices.length, uniqueDevices: devices.length,
staleDevices, staleDevices,
deviceChanges, deviceChanges,
changesSinceLastScan,
}); });
} catch (err) { } catch (err) {
const detail = err instanceof Error ? err.message : String(err); const detail = err instanceof Error ? err.message : String(err);

View File

@@ -551,7 +551,7 @@ export function ScannerPage() {
[] []
); );
const [fritzboxSummary, setFritzboxSummary] = usePersistedState< const [fritzboxSummary, setFritzboxSummary] = usePersistedState<
{ found: number; uniqueDevices: number } | null { found: number; uniqueDevices: number; changesSinceLastScan?: string } | null
>("scanner:fritzboxSummary", null); >("scanner:fritzboxSummary", null);
const [fritzboxError, setFritzboxError] = usePersistedState<string | null>( const [fritzboxError, setFritzboxError] = usePersistedState<string | null>(
"scanner:fritzboxError", "scanner:fritzboxError",
@@ -573,7 +573,11 @@ export function ScannerPage() {
if (!s.running && s.lastResult && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) { if (!s.running && s.lastResult && s.startedAt !== getHandledResultId("scanner:fritzboxLastHandledAt")) {
setHandledResultId("scanner:fritzboxLastHandledAt", 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,
changesSinceLastScan: s.lastResult.changesSinceLastScan,
});
setFritzboxError(null); setFritzboxError(null);
setStaleDevices(s.lastResult.staleDevices); setStaleDevices(s.lastResult.staleDevices);
setDeviceChanges(s.lastResult.deviceChanges); setDeviceChanges(s.lastResult.deviceChanges);
@@ -650,7 +654,8 @@ export function ScannerPage() {
`${r.created} neue(r) Dienst(e) gefunden.` + `${r.created} neue(r) Dienst(e) gefunden.` +
(pending > 0 (pending > 0
? ` ${pending} Änderung(en) warten unten auf deine Bestätigung.` ? ` ${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); setBulkStaleServices(r.staleServices);
setBulkNameChanges(r.nameChanges); setBulkNameChanges(r.nameChanges);
@@ -730,7 +735,19 @@ export function ScannerPage() {
{fritzboxRunning ? "Scanne …" : "FritzBox jetzt scannen"} {fritzboxRunning ? "Scanne …" : "FritzBox jetzt scannen"}
</Button> </Button>
{fritzboxSummary ? ( {fritzboxSummary ? (
<p className="mt-2 text-sm text-emerald-600 dark:text-emerald-400"> <div className="mt-2 text-sm">
{fritzboxSummary.changesSinceLastScan ? (
<p
className={
fritzboxSummary.changesSinceLastScan.startsWith("Keine Änderungen")
? "text-black/50 dark:text-white/50"
: "font-medium text-emerald-600 dark:text-emerald-400"
}
>
{fritzboxSummary.changesSinceLastScan}
</p>
) : null}
<p className="text-black/50 dark:text-white/50">
{fritzboxSummary.found} Eintrag/Einträge von der FritzBox,{" "} {fritzboxSummary.found} Eintrag/Einträge von der FritzBox,{" "}
{fritzboxSummary.uniqueDevices} eindeutige Geräte {fritzboxSummary.uniqueDevices} eindeutige Geräte
{fritzboxSummary.found !== fritzboxSummary.uniqueDevices {fritzboxSummary.found !== fritzboxSummary.uniqueDevices
@@ -738,6 +755,7 @@ export function ScannerPage() {
: ""} : ""}
. .
</p> </p>
</div>
) : null} ) : null}
{fritzboxError ? <p className="mt-2 text-sm text-red-500">{fritzboxError}</p> : null} {fritzboxError ? <p className="mt-2 text-sm text-red-500">{fritzboxError}</p> : null}
<DeviceChangesReview <DeviceChangesReview