round31: Infrastruktur-Praefixe (lxc/vm/docker) aus Namen und Suche entfernt, Icon-Datenbank-Abgleich auch ohne erkannte Software, Sichtbarkeit bei eindeutigem Icon-Treffer

This commit is contained in:
2026-07-24 15:30:03 +02:00
parent 87fed21348
commit e4cc6987be
3 changed files with 51 additions and 6 deletions

View File

@@ -172,6 +172,26 @@ export async function findBestIconMatch(name: string): Promise<string | null> {
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 "<wort>-" 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;
}

View File

@@ -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.

View File

@@ -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) },
];