/** * @launchpad/shared * * Gemeinsame Typen und Logik, die sowohl vom Backend (apps/backend) * als auch vom Frontend (apps/frontend) verwendet werden. */ export interface Device { id: string; hostname: string; ip: string; mac: string | null; manufacturer: string | null; model: string | null; online: boolean; source: DeviceSource; lastScan: string | null; // ISO-8601 Zeitstempel } export type DeviceSource = "fritzbox" | "dns" | "http" | "https" | "portscan" | "manual"; export interface Service { id: string; deviceId: string; displayName: string; hostname: string; url: string; https: boolean; port: number; category: string | null; icon: string | null; favicon: string | null; description: string | null; favorite: boolean; alias: string[]; order: number; } export interface HealthStatus { status: "ok" | "error"; timestamp: string; uptimeSeconds: number; version: string; } /** * Ranking-Stufen für die Suche, gemäß Spezifikation: * 1. Displayname beginnt mit Suchtext * 2. Alias beginnt mit Suchtext * 3. Hostname beginnt mit Suchtext * 4. Displayname enthält Suchtext * 5. Alias enthält Suchtext * 6. Beschreibung enthält Suchtext * * Niedrigere Werte sind relevanter. `null` bedeutet: kein Treffer. */ export function rankService(service: Service, query: string): number | null { const q = query.trim().toLowerCase(); if (q.length === 0) return null; const displayName = service.displayName.toLowerCase(); const hostname = service.hostname.toLowerCase(); const description = (service.description ?? "").toLowerCase(); const alias = service.alias.map((a) => a.toLowerCase()); if (displayName.startsWith(q)) return 1; if (alias.some((a) => a.startsWith(q))) return 2; if (hostname.startsWith(q)) return 3; if (displayName.includes(q)) return 4; if (alias.some((a) => a.includes(q))) return 5; if (description.includes(q)) return 6; return null; } /** * Sortiert und filtert eine Liste von Diensten anhand des Suchtexts. * Favoriten werden bei gleichem Rang bevorzugt, danach die definierte Reihenfolge. */ export function rankServices(services: Service[], query: string): Service[] { const q = query.trim(); if (q.length === 0) { return [...services].sort((a, b) => { if (a.favorite !== b.favorite) return a.favorite ? -1 : 1; return a.order - b.order; }); } return services .map((service) => ({ service, rank: rankService(service, q) })) .filter((entry): entry is { service: Service; rank: number } => entry.rank !== null) .sort((a, b) => { if (a.rank !== b.rank) return a.rank - b.rank; if (a.service.favorite !== b.service.favorite) return a.service.favorite ? -1 : 1; return a.service.order - b.service.order; }) .map((entry) => entry.service); }