generated from Dicken/dickendock
round12: FritzBox-Merge-Bug vor Doppel-Ausloesung fixen, Scan-Meldungen entschaerfen, Dienstname-Fallback aus Hostname statt IP
This commit is contained in:
@@ -136,6 +136,20 @@ function findByMacOrIp(mac: string | null | undefined, ip: string): Device | nul
|
||||
if (mac) {
|
||||
const byMac = all.find((d) => d.mac && d.mac.toLowerCase() === mac.toLowerCase());
|
||||
if (byMac) return byMac;
|
||||
|
||||
// Keine MAC-Übereinstimmung: nur noch über die IP abgleichen, wenn das per
|
||||
// IP gefundene Gerät selbst KEINE eigene MAC-Adresse gespeichert hat. Hat
|
||||
// es bereits eine (andere) MAC, handelt es sich nachweislich um ein
|
||||
// anderes physisches Gerät, das (noch) dieselbe IP gemeldet bekommt - z. B.
|
||||
// weil die FritzBox eine wiederverwendete/gerade neu vergebene IP kurzzeitig
|
||||
// sowohl im alten als auch im neuen Host-Eintrag führt. Ohne diese Prüfung
|
||||
// würde ein per Scan neu entdecktes Gerät fälschlich mit einem bereits
|
||||
// bekannten, anderen Gerät zusammengeführt und als "Namensänderung"
|
||||
// vorgeschlagen (siehe Bugreport: leere Datenbank, trotzdem sofort eine
|
||||
// vorgeschlagene Änderung bei der allerersten FritzBox-Abfrage).
|
||||
const byIp = all.find((d) => d.ip === ip);
|
||||
if (byIp && !byIp.mac) return byIp;
|
||||
return null;
|
||||
}
|
||||
return all.find((d) => d.ip === ip) ?? null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,52 @@ import * as serviceRepo from "../db/repositories/services.js";
|
||||
import * as categoryRepo from "../db/repositories/categories.js";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
import { scanDeviceServices } from "../scanner/networkScanner.js";
|
||||
import { fetchFritzBoxHosts } from "../scanner/fritzbox.js";
|
||||
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
|
||||
|
||||
/**
|
||||
* Erkennt den von der FritzBox selbst generierten Platzhalter-Namen für
|
||||
* Geräte ohne bekannten Hostnamen (Muster "PC-192-168-1-73"). Ein solcher
|
||||
* Name ist nie einer echten Namensänderung wert, sondern nur ein Rückfall.
|
||||
*/
|
||||
function isGenericFritzboxHostname(hostname: string, ip: string): boolean {
|
||||
return hostname === `PC-${ip.replace(/\./g, "-")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Die FritzBox liefert für ein einzelnes physisches Gerät oft MEHRERE
|
||||
* TR-064-Einträge (IPv4+IPv6, WLAN+Mesh, oder ein alter Platzhaltername neben
|
||||
* dem aktuellen) - alle mit derselben MAC-Adresse. Werden diese einzeln
|
||||
* nacheinander upgeserted, legt der erste Eintrag das Gerät an und der
|
||||
* zweite wird sofort als "Änderung" gegen das gerade erst selbst angelegte
|
||||
* Gerät vorgeschlagen - verwirrend, besonders bei einer leeren Datenbank
|
||||
* (siehe Bugreport). Deshalb hier VOR dem Upsert je MAC zu einem Eintrag
|
||||
* zusammenführen und dabei den aussagekräftigsten Hostnamen wählen (echter
|
||||
* Name statt "PC-<ip>"-Platzhalter, sofern vorhanden).
|
||||
*/
|
||||
function mergeFritzboxHosts(hosts: FritzBoxHost[]): FritzBoxHost[] {
|
||||
const byMac = new Map<string, FritzBoxHost[]>();
|
||||
const withoutMac: FritzBoxHost[] = [];
|
||||
|
||||
for (const host of hosts) {
|
||||
if (!host.mac) {
|
||||
withoutMac.push(host);
|
||||
continue;
|
||||
}
|
||||
const key = host.mac.toLowerCase();
|
||||
const group = byMac.get(key);
|
||||
if (group) group.push(host);
|
||||
else byMac.set(key, [host]);
|
||||
}
|
||||
|
||||
const merged: FritzBoxHost[] = Array.from(byMac.values()).map((group) => {
|
||||
const named = group.find((h) => !isGenericFritzboxHostname(h.hostname, h.ip));
|
||||
const chosen = named ?? group[0];
|
||||
const online = group.find((h) => h.online) ?? group[0];
|
||||
return { ip: online.ip, mac: chosen.mac, hostname: chosen.hostname, online: online.online };
|
||||
});
|
||||
|
||||
return [...merged, ...withoutMac];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan-Endpunkte. Werden ausschließlich manuell per Knopfdruck ("Jetzt
|
||||
@@ -139,7 +184,11 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
const devicesBeforeScan = deviceRepo.listDevices().filter((d) => d.source === "fritzbox");
|
||||
|
||||
const hosts = await fetchFritzBoxHosts({ host, port, username, password });
|
||||
const results = hosts.map((h) =>
|
||||
// Mehrere TR-064-Einträge für dasselbe physische Gerät (gleiche MAC)
|
||||
// werden VOR dem Upsert zusammengeführt (siehe mergeFritzboxHosts oben) -
|
||||
// deshalb kann "gefundene Einträge" > Anzahl eindeutiger Geräte sein.
|
||||
const mergedHosts = mergeFritzboxHosts(hosts);
|
||||
const results = mergedHosts.map((h) =>
|
||||
deviceRepo.upsertDeviceFromScan({
|
||||
hostname: h.hostname,
|
||||
ip: h.ip,
|
||||
@@ -149,18 +198,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
})
|
||||
);
|
||||
|
||||
// Mehrere TR-064-Einträge können auf dasselbe Gerät zusammengeführt
|
||||
// werden (z. B. IPv4+IPv6 oder WLAN+Mesh-Eintrag desselben Geräts,
|
||||
// abgeglichen über MAC/IP) - deshalb kann "gefundene Einträge" > Anzahl
|
||||
// eindeutiger Geräte sein. Für die Rückgabe nach Geräte-ID dedupliziert.
|
||||
const seenIds = new Set<string>();
|
||||
const devices = results
|
||||
.map((r) => r.device)
|
||||
.filter((d) => {
|
||||
if (seenIds.has(d.id)) return false;
|
||||
seenIds.add(d.id);
|
||||
return true;
|
||||
});
|
||||
const devices = results.map((r) => r.device);
|
||||
|
||||
// Vorschläge für abweichende Hostnamen/IPs bereits bekannter Geräte
|
||||
// einsammeln (siehe upsertDeviceFromScan) - werden NICHT automatisch
|
||||
|
||||
@@ -37,6 +37,27 @@ function isLikelyJunkTitle(title: string | undefined): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Prüft, ob ein String eine reine IPv4-Adresse ist (nicht als "Name" brauchbar). */
|
||||
function isIpAddress(value: string): boolean {
|
||||
return /^\d{1,3}(\.\d{1,3}){3}$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Macht aus einem technischen Hostnamen ("plug-shelly-heiz-links",
|
||||
* "nas_synology-01") einen lesbaren Anzeigenamen ("Plug Shelly Heiz Links").
|
||||
* Liefert null, wenn der Hostname selbst keine brauchbare Information trägt
|
||||
* (leer oder reine IP) - dann bleibt nur der IP:Port-Fallback übrig.
|
||||
*/
|
||||
function humanizeHostname(hostname: string): string | null {
|
||||
if (!hostname || isIpAddress(hostname)) return null;
|
||||
const words = hostname
|
||||
.replace(/\.(home|local|fritz\.box)$/i, "")
|
||||
.split(/[-_.\s]+/)
|
||||
.filter(Boolean);
|
||||
if (words.length === 0) return null;
|
||||
return words.map((w) => (w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w)).join(" ");
|
||||
}
|
||||
|
||||
export interface DeviceScanResult {
|
||||
services: DiscoveredService[];
|
||||
/**
|
||||
@@ -87,7 +108,14 @@ export async function scanDeviceServices(
|
||||
});
|
||||
|
||||
const titleIsJunk = isLikelyJunkTitle(probe.title);
|
||||
const fallbackName = `${address}:${port}`;
|
||||
// Kein brauchbarer Titel gefunden (z. B. weil eine SPA ihren echten Titel
|
||||
// erst per JavaScript setzt, das der Scanner nicht ausführt) - dann lieber
|
||||
// den bereits bekannten Gerätenamen ("plug-shelly-heiz-links" ->
|
||||
// "Plug Shelly Heiz Links") als Name verwenden statt einer nichtssagenden
|
||||
// IP:Port-Kombination. Nur wenn auch der Gerätename keine Information
|
||||
// trägt (leer/reine IP, z. B. bei unbenannten Geräten), bleibt IP:Port
|
||||
// als letzter Ausweg übrig.
|
||||
const fallbackName = humanizeHostname(device.hostname) ?? `${address}:${port}`;
|
||||
|
||||
// Nur sichtbar, wenn Software erkannt wurde oder eine erfolgreiche Antwort
|
||||
// (Status < 400) mit einem brauchbaren Titel vorliegt. Fehlerseiten und
|
||||
|
||||
Reference in New Issue
Block a user