round23: Startseite kompakter, Theme-Button entfernt, echte Bildverifikation fuer Favicon-Auswahl, Tooltip-Portal, wortbasierte interaktive Favicon-Vorschlaege

This commit is contained in:
2026-07-24 00:26:08 +02:00
parent b098007742
commit d87885f16f
7 changed files with 252 additions and 108 deletions

View File

@@ -80,31 +80,93 @@ export async function searchIconDb(query: string, limit = 24): Promise<IconSearc
return matches.map((name) => ({ name, url: `${CDN_BASE}/${name}.png` }));
}
/**
* Generische Infrastruktur-/Container-Begriffe, die beim Zerlegen eines
* Dienstnamens in Wörter ignoriert werden - "LXC Adguard" soll als "Adguard"
* gesucht werden, nicht als "LXC" (das nie ein sinnvoller Icon-Treffer wäre).
*/
const STOPWORDS = new Set([
"lxc", "vm", "docker", "container", "server", "service", "app", "box",
"host", "node", "instance", "srv", "ct", "the", "der", "die", "das",
]);
function normalize(s: string): string {
return s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
function significantWords(name: string): string[] {
return name
.split(/[\s_-]+/)
.map((w) => normalize(w))
.filter((w) => w.length >= 3 && !STOPWORDS.has(w));
}
async function exactMatch(candidate: string, names: string[]): Promise<string | null> {
const nameSet = new Set(names);
if (nameSet.has(candidate)) return candidate;
const collapsed = candidate.replace(/-/g, "");
const found = names.find((n) => n.replace(/-/g, "") === collapsed);
return found ?? null;
}
/**
* Sucht den besten Treffer in der Icon-Datenbank für einen Dienst-/
* Software-Namen ("Plex", "Pi-hole", "Adguard Home") - für den Fall, dass
* der Scanner selbst kein Favicon gefunden hat (siehe networkScanner.ts).
* Bewusst konservativ: nur ein Treffer, wenn der normalisierte Name exakt
* (oder nach Entfernen von Bindestrichen) übereinstimmt - eine reine
* Teilstring-Suche würde bei kurzen/generischen Namen zu viele falsche
* Zuordnungen liefern (z. B. "nas" -> "nas4free" wäre noch plausibel, aber
* "web" könnte alles Mögliche treffen).
* Software-Namen ("Plex", "LXC Adguard", "Pi-hole") - für den Fall, dass der
* Scanner selbst kein Favicon gefunden hat (siehe networkScanner.ts). Läuft
* unbeaufsichtigt während eines Scans, liefert deshalb nur EINEN Treffer
* (den ersten gefundenen: erst der ganze Name, dann Wort für Wort, generische
* Begriffe wie "LXC"/"Docker" werden dabei übersprungen) statt einer Auswahl.
*/
export async function findBestIconMatch(name: string): Promise<string | null> {
const normalized = name
.toLowerCase()
.trim()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
if (normalized.length < 2) return null;
const names = await getIconNames();
const nameSet = new Set(names);
if (nameSet.has(normalized)) return `${CDN_BASE}/${normalized}.png`;
const collapsed = normalized.replace(/-/g, "");
const exactCollapsed = names.find((n) => n.replace(/-/g, "") === collapsed);
if (exactCollapsed) return `${CDN_BASE}/${exactCollapsed}.png`;
const wholeName = normalize(name);
if (wholeName.length >= 2) {
const match = await exactMatch(wholeName, names);
if (match) return `${CDN_BASE}/${match}.png`;
}
for (const word of significantWords(name)) {
const match = await exactMatch(word, names);
if (match) return `${CDN_BASE}/${match}.png`;
}
return null;
}
/**
* Wie findBestIconMatch, liefert aber ALLE plausiblen Kandidaten (ganzer
* Name + jedes einzelne aussagekräftige Wort, exakte Treffer UND
* Teilstring-Treffer) statt nur den ersten - für die interaktive
* "Fehlende Favicons ergänzen"-Übersicht, wo der Mensch selbst auswählt,
* welcher Treffer stimmt, statt dass automatisch geraten wird.
*/
export async function suggestIconsForServiceName(name: string, limit = 8): Promise<IconSearchResult[]> {
const names = await getIconNames();
const seen = new Set<string>();
const results: IconSearchResult[] = [];
function addIfNew(iconName: string) {
if (seen.has(iconName)) return;
seen.add(iconName);
results.push({ name: iconName, url: `${CDN_BASE}/${iconName}.png` });
}
const wholeName = normalize(name);
if (wholeName.length >= 2) {
const exact = await exactMatch(wholeName, names);
if (exact) addIfNew(exact);
}
for (const word of significantWords(name)) {
const exact = await exactMatch(word, names);
if (exact) addIfNew(exact);
if (results.length >= limit) break;
for (const n of names) {
if (results.length >= limit) break;
if (n.includes(word)) addIfNew(n);
}
}
return results.slice(0, limit);
}