generated from Dicken/dickendock
round20: Startseite ueberarbeitet, Favicon-Auswahl bereinigt und Icon-Datenbank integriert, Ping-Schalter repariert
This commit is contained in:
81
apps/backend/src/scanner/iconDb.ts
Normal file
81
apps/backend/src/scanner/iconDb.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { request } from "node:https";
|
||||
|
||||
const TREE_URL = "https://raw.githubusercontent.com/homarr-labs/dashboard-icons/main/tree.json";
|
||||
const CDN_BASE = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png";
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 1 Tag
|
||||
|
||||
let cachedNames: string[] | null = null;
|
||||
let cachedAt = 0;
|
||||
let inFlight: Promise<string[]> | null = null;
|
||||
|
||||
function fetchJson(url: string): Promise<unknown> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = request(url, { timeout: 8000 }, (res) => {
|
||||
if (res.statusCode && res.statusCode >= 400) {
|
||||
reject(new Error(`HTTP ${res.statusCode}`));
|
||||
res.resume();
|
||||
return;
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (c) => chunks.push(c));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(Buffer.concat(chunks).toString("utf-8")));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("timeout", () => req.destroy(new Error("Timeout")));
|
||||
req.on("error", reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Lädt (und cacht 24h im Speicher) die Liste aller Icon-Namen aus dem
|
||||
* homarr-labs/dashboard-icons-Projekt (freie Icon-Sammlung speziell für
|
||||
* Homelab-/Selfhosted-Dashboards, via jsDelivr-CDN ausgeliefert).
|
||||
*
|
||||
* Bewusst als einzige Stelle im Projekt, die tatsächlich einen externen
|
||||
* Dienst kontaktiert (abgesehen vom eigentlichen Aufruf der Dienste selbst)
|
||||
* - das Icon-Datenbank-Feature wurde ausdrücklich so gewünscht. Schlägt der
|
||||
* Abruf fehl (kein Internetzugang, Dienst nicht erreichbar), liefert die
|
||||
* Suche einfach keine Treffer statt einen Fehler zu werfen.
|
||||
*/
|
||||
async function getIconNames(): Promise<string[]> {
|
||||
if (cachedNames && Date.now() - cachedAt < CACHE_TTL_MS) return cachedNames;
|
||||
if (inFlight) return inFlight;
|
||||
|
||||
inFlight = (async () => {
|
||||
try {
|
||||
const tree = (await fetchJson(TREE_URL)) as { png?: string[] };
|
||||
const names = (tree.png ?? []).map((f) => f.replace(/\.png$/, ""));
|
||||
cachedNames = names;
|
||||
cachedAt = Date.now();
|
||||
return names;
|
||||
} catch {
|
||||
// Kein Absturz, nur keine Treffer - die restliche Favicon-Auswahl
|
||||
// (bereits im System verwendete Favicons) funktioniert unabhängig davon weiter.
|
||||
return cachedNames ?? [];
|
||||
} finally {
|
||||
inFlight = null;
|
||||
}
|
||||
})();
|
||||
|
||||
return inFlight;
|
||||
}
|
||||
|
||||
export interface IconSearchResult {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export async function searchIconDb(query: string, limit = 24): Promise<IconSearchResult[]> {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (q.length < 2) return [];
|
||||
|
||||
const names = await getIconNames();
|
||||
const matches = names.filter((n) => n.includes(q)).slice(0, limit);
|
||||
return matches.map((name) => ({ name, url: `${CDN_BASE}/${name}.png` }));
|
||||
}
|
||||
@@ -136,16 +136,19 @@ export async function scanDeviceServices(
|
||||
});
|
||||
}
|
||||
|
||||
// Fallback-Reihenfolge für Dienste ohne bestätigtes Favicon (kein
|
||||
// <link rel="icon"> im HTML gefunden): zuerst das Favicon eines ANDEREN
|
||||
// Dienstes auf demselben Gerät übernehmen (typischerweise dieselbe
|
||||
// Software/dasselbe Gerät, das Icon passt meist trotzdem) - erst wenn auch
|
||||
// das fehlt, wird als letzter Ausweg "/favicon.ico" am eigenen Port
|
||||
// geraten (kann ins Leere laufen, ist aber besser als gar kein Versuch).
|
||||
// Fallback für Dienste ohne bestätigtes Favicon (kein <link rel="icon">
|
||||
// im HTML gefunden): das Favicon eines ANDEREN Dienstes auf demselben
|
||||
// Gerät übernehmen (typischerweise dieselbe Software/dasselbe Gerät, das
|
||||
// Icon passt meist trotzdem). Gibt es auch das nicht, bleibt favicon
|
||||
// bewusst leer (Buchstaben-Fallback in der UI) statt blind auf
|
||||
// "/favicon.ico" zu raten - eine geratene, unbestätigte Adresse sähe in
|
||||
// der Favicon-Auswahl (Bearbeiten-Formular) wie ein echtes Icon aus,
|
||||
// obwohl sie oft ins Leere läuft.
|
||||
const firstSiblingFavicon = found.find((s) => s.favicon)?.favicon;
|
||||
for (const service of found) {
|
||||
if (service.favicon) continue;
|
||||
service.favicon = firstSiblingFavicon ?? `${service.url}/favicon.ico`;
|
||||
if (firstSiblingFavicon) {
|
||||
for (const service of found) {
|
||||
if (!service.favicon) service.favicon = firstSiblingFavicon;
|
||||
}
|
||||
}
|
||||
|
||||
return { services: found, suggestedHostname };
|
||||
|
||||
Reference in New Issue
Block a user