homelab: Seite neu geordnet — Lage auf einen Blick, Updates, Geraete, Verlauf getrennt

Vorher stand alles auf Geraetekarten durcheinander (KI-Box zwischen den Containern, Paketstand, App-Version,
Knoepfe und Rueckweg je Karte). Jetzt: oben die Lage wie das Warnpanel der Startseite (grosse Leuchte + eine
Lampe je Geraet), darunter nur die offenen Updates als Tabelle mit Rueckweg und Knopf, dann die Geraeteliste
(App, Pakete, Netz) und der Verlauf. Die KI-Box steht nur noch als Hinweis auf ihre eigene Updates-Seite.
Solange der Homelab-Teil laedt, sagt die Seite nicht mehr voreilig "aktuell".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-09-24 20:15:49 +02:00
co-authored by Claude Opus 5.5
parent 40d2840b16
commit a32f55645c
12 changed files with 710 additions and 231 deletions
+137
View File
@@ -0,0 +1,137 @@
import type { BausteinStand, Hinweis, HomelabStand, Lampe, ZielStand } from "@/lib/typen"
// 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<string, string> = {
"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<string, string> = {
"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)]
}
/** 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.includes("snapshot vor")) return "snapshot"
if (t.includes("sicherung vor")) 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"
}
export function lampeFuer(ziel: ZielStand, laeuft: boolean): Lampe {
const label = kurzname(ziel)
if (ziel.erreichbar === false) return { id: ziel.id, label, zustand: "fehler", wert: "antwortet nicht" }
if (laeuft) return { id: ziel.id, label, zustand: "info", wert: "Update läuft" }
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, hinweise: Hinweis[], laufen: number, berichtZeit: string | null): Lage {
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
}