diff --git a/apps/backend/src/scanner/iconDb.ts b/apps/backend/src/scanner/iconDb.ts index 265b9da..2aefc23 100644 --- a/apps/backend/src/scanner/iconDb.ts +++ b/apps/backend/src/scanner/iconDb.ts @@ -172,6 +172,26 @@ export async function findBestIconMatch(name: string): Promise { if (match) return `${CDN_BASE}/${match}.png`; } + // Letzter, bewusst noch immer präziser Versuch: ein Name wie "adguard" + // trifft "adguard-home" nicht exakt (auch nicht nach Bindestrich-Vergleich), + // ist aber ein eindeutiger Wortanfang-Treffer, wenn KEIN anderer Icon-Name + // mit "-" beginnt. Nur dann automatisch übernehmen - bei mehreren + // möglichen Treffern bleibt es bei "kein Favicon" (dafür gibt es die + // interaktive Auswahl mit mehreren Kandidaten). + for (const candidate of [wholeName, ...byPriority(words)]) { + if (candidate.length < 3) continue; + const prefixMatches = names.filter((n) => n === candidate || n.startsWith(`${candidate}-`)); + if (prefixMatches.length === 1) return `${CDN_BASE}/${prefixMatches[0]}.png`; + if (prefixMatches.length > 1) { + // Mehrere Treffer (z. B. "adguard-home" UND "adguard-home-sync" für + // "adguard") - den kürzesten (meist der "Haupt"-Name der Software) + // automatisch nehmen, aber NUR wenn er eindeutig der kürzeste ist, + // sonst bleibt es zweideutig (dafür gibt es die interaktive Auswahl). + const sorted = [...prefixMatches].sort((a, b) => a.length - b.length); + if (sorted[0].length < sorted[1].length) return `${CDN_BASE}/${sorted[0]}.png`; + } + } + return null; } diff --git a/apps/backend/src/scanner/networkScanner.ts b/apps/backend/src/scanner/networkScanner.ts index d94f3e7..1b1e562 100644 --- a/apps/backend/src/scanner/networkScanner.ts +++ b/apps/backend/src/scanner/networkScanner.ts @@ -49,12 +49,21 @@ function isIpAddress(value: string): boolean { * Liefert null, wenn der Hostname selbst keine brauchbare Information trägt * (leer oder reine IP) - dann bleibt nur der IP:Port-Fallback übrig. */ +/** + * Generische Infrastruktur-Präfixe/-Suffixe, die in Proxmox/Docker-Umfeld + * üblich, aber inhaltlich bedeutungslos sind ("lxc-adguard" soll als + * "Adguard" vorgeschlagen werden, nicht als "Lxc Adguard") - dieselbe Logik + * wie die STOPWORDS in scanner/iconDb.ts, hier auf den Hostnamen angewendet. + */ +const GENERIC_INFRA_WORDS = new Set(["lxc", "vm", "ct", "docker", "container"]); + 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); + .filter(Boolean) + .filter((w) => !GENERIC_INFRA_WORDS.has(w.toLowerCase())); if (words.length === 0) return null; return words.map((w) => (w.length > 0 ? w[0].toUpperCase() + w.slice(1) : w)).join(" "); } @@ -227,11 +236,23 @@ export async function scanDeviceServices( // ein falsch zugeordnetes. for (const service of found) { if (service.favicon) continue; - const nameToMatch = service.category ? service.suggestedDisplayName : null; - if (!nameToMatch) continue; + // Vorher: nur versucht, wenn Software erkannt wurde. Jetzt auch bei + // einem aus dem Gerätenamen abgeleiteten Namen ("lxc-adguard" -> + // "Adguard", siehe humanizeHostname) - blieb Softwareerkennung aus, + // konnte trotzdem korrekt "adguard-home" gefunden werden. Nur der reine + // IP:Port-Nietenwert (keine Information vorhanden) wird übersprungen. + const isBareAddressFallback = /^[\d.:]+$/.test(service.suggestedDisplayName); + if (isBareAddressFallback) continue; try { - const match = await findBestIconMatch(nameToMatch); - if (match) service.favicon = match; + const match = await findBestIconMatch(service.suggestedDisplayName); + if (match) { + service.favicon = match; + // Ein eindeutiger Icon-Datenbank-Treffer ist selbst ein starkes + // Erkennungssignal - auch wenn Titel/Software-Erkennung nichts + // Brauchbares geliefert haben (z. B. bei einer reinen + // Client-seitig-gerenderten Login-Seite ohne Server-Titel). + service.visible = true; + } } catch { // Icon-Datenbank nicht erreichbar (kein Internetzugang o. ä.) - der // Scan soll deswegen nicht fehlschlagen, einfach ohne Favicon weiter. diff --git a/apps/backend/src/scanner/softwareDetection.ts b/apps/backend/src/scanner/softwareDetection.ts index 36b922b..b66580b 100644 --- a/apps/backend/src/scanner/softwareDetection.ts +++ b/apps/backend/src/scanner/softwareDetection.ts @@ -60,7 +60,11 @@ export const SOFTWARE_SIGNATURES: SoftwareSignature[] = [ { name: "Jellyfin", category: "Medien", matches: (i) => bodyContains(i, /jellyfin/i) }, { name: "Nextcloud", category: "Cloud", matches: (i) => bodyContains(i, /nextcloud/i) }, { name: "Pi-hole", category: "DNS", matches: (i) => bodyContains(i, /pi-?hole/i) }, - { name: "AdGuard Home", category: "DNS", matches: (i) => bodyContains(i, /adguard/i) }, + { + name: "AdGuard Home", + category: "DNS", + matches: (i) => bodyContains(i, /adguard/i) || titleMatches(i, /adguard/i), + }, { name: "UniFi Network", category: "Netzwerk", matches: (i) => bodyContains(i, /unifi/i) }, ];