export interface SoftwareSignatureInput { server?: string; body?: string; title?: string; port: number; } export interface SoftwareSignature { name: string; category: string; matches: (input: SoftwareSignatureInput) => boolean; /** Optional: Emoji oder Icon-URL, von Plugins bereitgestellt (siehe apps/backend/plugins). */ icon?: string; } function bodyContains(input: SoftwareSignatureInput, pattern: RegExp): boolean { return !!input.body && pattern.test(input.body); } function titleMatches(input: SoftwareSignatureInput, pattern: RegExp): boolean { return !!input.title && pattern.test(input.title.trim()); } /** * Automatische Softwareerkennung anhand von HTTP-Response-Merkmalen * (Server-Header, HTML-Inhalt). Liste gemäß Spezifikation. * * Grundsatz: der ``-Tag ist ein viel präziseres Signal als der * komplette Seiteninhalt und wird deshalb wo möglich zuerst geprüft. Reine * Volltextsuche im Body (bodyContains) besonders bei Marken-/Produktnamen * aus generischen Alltagswörtern (z. B. "Home Assistant") kann in * minifiziertem JavaScript anderer Web-UIs zufällig zutreffen - siehe * Bugreport: FritzBox-Repeater wurden fälschlich als "Home Assistant" * erkannt, weil beide Wörter irgendwo im ausgelieferten JS vorkamen. */ export const SOFTWARE_SIGNATURES: SoftwareSignature[] = [ { name: "Home Assistant", category: "Smart Home", matches: (i) => titleMatches(i, /^home\s*assistant$/i) || bodyContains(i, /frontend_latest\//i), }, { name: "Synology DSM", category: "NAS", matches: (i) => bodyContains(i, /synology/i) || (!!i.server && /synology/i.test(i.server)), }, { name: "Frigate", category: "Überwachung", matches: (i) => bodyContains(i, /frigate/i) }, { name: "Portainer", category: "Container", matches: (i) => bodyContains(i, /portainer/i) }, { name: "Grafana", category: "Monitoring", matches: (i) => bodyContains(i, /grafana/i) }, { name: "Proxmox VE", category: "Virtualisierung", matches: (i) => bodyContains(i, /proxmox/i) }, { name: "Immich", category: "Fotos", matches: (i) => bodyContains(i, /immich/i) }, { name: "Paperless-ngx", category: "Dokumente", matches: (i) => bodyContains(i, /paperless/i) }, { name: "Gitea", category: "Entwicklung", matches: (i) => bodyContains(i, /gitea/i) }, { name: "Vaultwarden", category: "Passwörter", matches: (i) => bodyContains(i, /vaultwarden|bitwarden/i), }, { 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) || titleMatches(i, /adguard/i), }, { name: "UniFi Network", category: "Netzwerk", matches: (i) => bodyContains(i, /unifi/i) }, ]; /** * Zusätzliche Signaturen, die von Plugins zur Laufzeit registriert werden * (siehe apps/backend/src/plugins/loader.ts). Eingebaute Signaturen haben * Vorrang, falls beide zufällig auf denselben Dienst passen. */ const pluginSignatures: SoftwareSignature[] = []; export function registerSoftwareSignature(signature: SoftwareSignature): void { pluginSignatures.push(signature); } export function detectSoftware(input: SoftwareSignatureInput): SoftwareSignature | null { return ( SOFTWARE_SIGNATURES.find((signature) => signature.matches(input)) ?? pluginSignatures.find((signature) => signature.matches(input)) ?? null ); }