round20: Startseite ueberarbeitet, Favicon-Auswahl bereinigt und Icon-Datenbank integriert, Ping-Schalter repariert

This commit is contained in:
2026-07-23 19:34:09 +02:00
parent ec49534b60
commit 68ca9ec5e1
14 changed files with 381 additions and 176 deletions

View 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` }));
}