import type { BausteinStand, Hinweis, HomelabStand, Kennzahl, Lampe, ZielStand } from "@/lib/typen" import { vor } from "@/lib/zeit" /** Die Live-Zahl einer Kachel in Worten (seit 25.09.2026): „12 % geblockt“, „letzte Sicherung vor 13 h“. */ export function kennzahlText(k: Kennzahl | null | undefined): string | null { if (!k?.text) return null return k.zeit ? `${k.text} ${vor(k.zeit)}` : k.text } // Die Homelab-Seite trennt, was vorher auf jeder Gerätekarte zusammenstand: die Lage auf einen Blick, // was zu tun ist (Updates), was wo läuft (Geräte) und was passiert ist (Verlauf). Hier liegt die // Logik dazu, damit die Ansicht nur noch zeichnet. const ART: Record = { "ki-box": "KI-Box", "proxmox-host": "Proxmox-Host", container: "Container", vm: "VM", } /** „Container 104“ aus Art und ID („ct-104“). */ export function artText(ziel: ZielStand): string { const nummer = ziel.id.match(/-(\d+)$/)?.[1] const art = ART[ziel.art] ?? ziel.art return nummer ? `${art} ${nummer}` : art } /** Kurze Namen für die Lampen; der volle Name steht in der Geräteliste. */ const KURZNAME: Record = { "Proxmox-Host": "Proxmox", "AdGuard Home": "AdGuard", "PVE Scripts Local": "PVE Scripts", "Proxmox Backup Server": "PBS", "Arcane (Docker)": "Arcane", } export const kurzname = (ziel: ZielStand) => KURZNAME[ziel.name] ?? ziel.name // Die App eines Geräts (Gitea, AdGuard, Arcane …) und darunter sein System (Pakete, Docker-Images). const APP = new Set(["app", "arcane"]) export const appBaustein = (ziel: ZielStand) => ziel.bausteine.find((b) => APP.has(b.id)) ?? null export const systemBaustein = (ziel: ZielStand) => ziel.bausteine.find((b) => !APP.has(b.id)) ?? null export interface OffenesUpdate { ziel: ZielStand baustein: BausteinStand } /** Alles mit neuer Version — erst, was per Knopf geht, dann, was die App selbst erledigt oder noch wartet. */ export function offeneUpdates(ziele: ZielStand[]): OffenesUpdate[] { const alle = ziele.flatMap((ziel) => ziel.bausteine.filter((b) => b.zustand === "neu").map((baustein) => ({ ziel, baustein })), ) return [...alle.filter((u) => u.baustein.aktion), ...alle.filter((u) => !u.baustein.aktion)] } /** Reihenfolge der Update-Tabelle: 0 = etwas per Knopf, 1 = neu ohne Knopf (wartet, eigene Oberfläche), * 2 = Stand unklar, 3 = alles aktuell. */ export function updateRang(ziel: ZielStand): number { const neu = ziel.bausteine.filter((b) => b.zustand === "neu") if (neu.some((b) => b.aktion)) return 0 if (neu.length > 0) return 1 if (ziel.bausteine.some((b) => b.zustand === "unbekannt")) return 2 return 3 } /** Was vor dem Update gesichert wird, in wenigen Worten. */ export function rueckwegText(ziel: ZielStand): string { const art = ziel.rueckweg_art ?? rueckwegAusText(ziel.rueckweg) if (art === "snapshot") return "Snapshot vorher, bei Rot zurück" if (art === "sicherung") return "Sicherung vorher, bei Rot zurück" if (ziel.art === "proxmox-host") return "Kein Rückweg, Neustart getrennt" return "Kein automatischer Rückweg" } function rueckwegAusText(text: string | null): "snapshot" | "sicherung" | "keiner" { const t = (text ?? "").toLowerCase() if (t.startsWith("snapshot vor")) return "snapshot" if (t.startsWith("sicherung")) return "sicherung" return "keiner" } const istApp = (b: BausteinStand) => APP.has(b.id) /** Worum es in einer Update-Zeile geht: „Container 102 · App“, beim Host nur „Pakete“. */ export function worum(ziel: ZielStand, b: BausteinStand): string { const was = istApp(b) ? "App" : b.id === "docker" ? "Docker-Images" : "Pakete" return ziel.art === "proxmox-host" ? was : `${artText(ziel)} · ${was}` } /** Die Version, wie sie in Listen steht — ein NPMplus-Image heißt nach seinem Datum. */ export const versionText = (v: string) => v.replace(/^Image vom /, "") /** Was die Versionsspalte „Neu“ zeigt: die neue Version einer App, sonst „58 Pakete“. */ export function neuText(b: BausteinStand): string { if (istApp(b) && b.verfuegbar) return b.verfuegbar return `${b.kurz || b.verfuegbar || "neu"}${sicherheitText(b)}` } /** „ · 3 Sicherheit“ — wenn Updates eine Sicherheitslücke schließen (seit 25.09.2026), sonst leer. */ export function sicherheitText(b: BausteinStand): string { return b.sicherheit ? ` · ${b.sicherheit} Sicherheit` : "" } /** Anteil der belegten Platte (0…1) oder null, wo unbekannt. Ab 80 % gelb, ab 90 % rot — wie der Wächter (ext4 hält 5 % zurück). */ export function plattenAnteil(ziel: ZielStand): number | null { const p = ziel.platte return p && p.gesamt > 0 ? p.belegt / p.gesamt : null } export const PLATTE_GELB = 0.8 export const PLATTE_ROT = 0.9 export function lampeFuer(ziel: ZielStand, laeuft: boolean): Lampe { const label = kurzname(ziel) const platte = plattenAnteil(ziel) if (ziel.erreichbar === false) return { id: ziel.id, label, zustand: "fehler", wert: "antwortet nicht" } if (platte !== null && platte >= PLATTE_ROT) return { id: ziel.id, label, zustand: "fehler", wert: "Platte voll" } if (laeuft) return { id: ziel.id, label, zustand: "info", wert: "Update läuft" } if (platte !== null && platte >= PLATTE_GELB) { return { id: ziel.id, label, zustand: "warn", wert: `Platte ${Math.round(platte * 100)} %` } } const neu = ziel.bausteine.filter((b) => b.zustand === "neu") if (neu.length > 0) { const b = neu.find((x) => APP.has(x.id)) ?? neu[0] return { id: ziel.id, label, zustand: "info", wert: APP.has(b.id) && b.verfuegbar ? `→ ${b.verfuegbar}` : neuText(b) } } const app = appBaustein(ziel) return { id: ziel.id, label, zustand: "ok", wert: app?.installiert ? versionText(app.installiert) : "aktuell" } } export interface Lage { art: "ok" | "info" | "gelb" | "rot" | "stumm" oben: string mitte: string unten: string /** Wohin „Drücken zum Ansehen“ springt. */ ziel: "hinweise" | "updates" | "geraete" | null } const hinweisZahl = (n: number) => `${n} Hinweis${n === 1 ? "" : "e"}` /** Die große Leuchte oben links — Störung vor Hinweis vor laufendem Update vor offenen Updates. */ export function lage(stand: HomelabStand, alleHinweise: Hinweis[], laufen: number, berichtZeit: string | null): Lage { // Stumm geschaltete Hinweise (seit 25.09.2026) stehen in der Liste, lösen die Leuchte aber nicht aus. const hinweise = alleHinweise.filter((h) => h.zustand !== "stumm") // Im Wartungsfenster (seit 25.09.2026) ist ein Gerät, das kurz neu startet, geplante Wartung — keine Störung. if (stand.wartung?.laeuft) { return { art: "info", oben: "Wartung", mitte: "Fenster läuft", unten: `${stand.wartung.fenster} · Meldung am Ende`, ziel: "updates" } } const stumm = stand.ziele.filter((z) => z.erreichbar === false) if (stumm.length > 0) { const mitte = stumm.length === 1 ? "1 Gerät stumm" : `${stumm.length} Geräte stumm` return { art: "rot", oben: "Störung", mitte, unten: stumm.map(kurzname).join(", "), ziel: "geraete" } } if (hinweise.some((h) => h.stufe === "rot")) { return { art: "rot", oben: "Störung", mitte: hinweisZahl(hinweise.length), unten: "Drücken zum Ansehen", ziel: "hinweise" } } if (!stand.ausfuehrer.verbunden) { return { art: "gelb", oben: "Achtung", mitte: "Ausführer stumm", unten: "Die Geräte zeigen den letzten Stand", ziel: null } } if (hinweise.length > 0) { return { art: "gelb", oben: "Achtung", mitte: hinweisZahl(hinweise.length), unten: "Drücken zum Ansehen", ziel: "hinweise" } } if (laufen > 0) { return { art: "info", oben: "Wartung", mitte: "Update läuft", unten: "Das Ergebnis kommt als Meldung", ziel: "updates" } } const knopf = offeneUpdates(stand.ziele).filter((u) => u.baustein.aktion).length if (knopf > 0) { return { art: "info", oben: "Homelab", mitte: `${knopf} Update${knopf === 1 ? "" : "s"}`, unten: "Drücken zum Ansehen", ziel: "updates" } } return { art: "ok", oben: "Homelab", mitte: "Alles aktuell", unten: berichtZeit ? `Stand ${berichtZeit}` : "Keine offenen Updates", ziel: null } } /** „unklar · Listen vom 22.10.2025“ statt des ganzen Satzes aus dem Grund. */ export function listenAlter(grund: string | null): string | null { return grund?.match(/vom (\d{2}\.\d{2}\.\d{4})/)?.[1] ?? null }