import { Link, useNavigate } from "@tanstack/react-router" import { ArrowRight } from "lucide-react" import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip" import { Panel } from "@/components/cockpit/Panel" import { Gesundheit } from "@/components/homelab/Gesundheit" import { Lagebild } from "@/components/homelab/Lagebild" import { Speicher } from "@/components/homelab/Speicher" import { AlleKnopf, SammellaufFortschritt } from "@/components/homelab/Sammellauf" import { UpdateTabelle } from "@/components/homelab/UpdateTabelle" import { useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useHomelabSpeicher, useInstanz, usePartner, useSammellauf, useSammellaufQuittieren, useZielAktion, } from "@/lib/abfragen" import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab" import { istSnapshotLauf } from "@/lib/snapshots" import type { Hinweis, HomelabLauf, HomelabStand, PartnerStatus } from "@/lib/typen" import { flugplanZeit } from "@/lib/zeit" // Der Bereich Homelab: das Cockpit zeigt, wie es den Geräten geht (Lage, Hinweise, Netz, CPU, Speicher, // Platte); die Updates-Seite zeigt je Gerät die Versionen, was ansteht und was gelaufen ist. Snapshots, Monitoring // und Protokoll haben eigene Seiten. Aus der KI-Box steht hier nichts — die hat ihren eigenen Bereich. /** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026). */ const GERAETE = ["Proxmox-Host", "AdGuard", "NPMplus", "NetBird", "PVE Scripts Local", "Gitea", "PBS", "Arcane mit Docker"] const ERGEBNIS: Record = { eingespielt: { art: "gruen", text: "eingespielt" }, zurueckgerollt: { art: "bernstein", text: "zurückgerollt" }, fehler: { art: "rot", text: "Fehler" }, neustart: { art: "cyan", text: "Neustart" }, aufgeraeumt: { art: "gruen", text: "aufgeräumt" }, offen: { art: "cyan", text: "Probelauf" }, // Docker über Arcane, solange es nur probt } const zeitAus = (sekunden: number) => flugplanZeit(new Date(sekunden * 1000).toISOString()) function NochNichtEingerichtet() { return ( <>

Der Homelab-Teil läuft als eigene Instanz in einem kleinen Container auf dem Proxmox-PC. Von dort prüft er auch die KI-Box von außen.

Steht er, erscheinen hier alle Geräte mit ihren offenen Updates. Ein Update startest du mit „Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst zurück.

Dazu kommen

Der Container wird erst nach deiner Freigabe angelegt.

) } function HomelabFehlt({ partner }: { partner: PartnerStatus }) { const chip = !partner.eingerichtet ? Noch nicht eingerichtet : Antwortet nicht return ( {partner.eingerichtet ? (

Der Homelab-Teil ({partner.url}) ist {partner.fehler ?? "nicht erreichbar"}. Hält das an, meldet der Wächter es nach fünf Minuten auf Telegram.

) : ( )}
) } /** Hinweise des Homelab-Wächters (die der KI-Box stehen in deren Cockpit). */ function Hinweise({ hinweise }: { hinweise: Hinweis[] }) { const rot = hinweise.some((h) => h.stufe === "rot") return ( {hinweise.length} offen}> ) } /** Die letzten Läufe — gebaut wie der Verlauf der KI-Box: Zeit, Gerät, Ergebnis, der Satz darunter. Die Aktionen der * Seite Snapshots sind keine Updates und stehen dort. */ function Verlauf({ laeufe }: { laeufe: HomelabLauf[] }) { const fertig = laeufe.filter((l) => l.status !== "laeuft" && !istSnapshotLauf(l)).slice(0, 12) if (fertig.length === 0) return

Noch kein Update im Homelab eingespielt.

return (
    {fertig.map((l) => { // Ein Ergebnis ohne eigenen Eintrag riss die Seite vorher um (ERGEBNIS[…] war undefined). const e = l.ergebnis ? (ERGEBNIS[l.ergebnis] ?? { art: "grau" as ChipArt, text: l.ergebnis }) : { art: "grau" as ChipArt, text: "unterbrochen" } return (
  1. {zeitAus(l.start)} {l.name} {e.text}
    {l.text &&

    {l.text}

    }
  2. ) })}
) } /** Wenn der Ausführer schweigt oder sein Bericht alt ist — auf beiden Seiten gleich. */ function AusfuehrerHinweis({ stand }: { stand: HomelabStand }) { if (!stand.ausfuehrer.verbunden) { return (

Der Ausführer auf dem Proxmox-Host meldet sich nicht {stand.ausfuehrer.zuletzt ? ` (zuletzt ${zeitAus(stand.ausfuehrer.zuletzt)})` : ""}. Die Geräte zeigen den letzten bekannten Stand; Updates starten erst, wenn er wieder da ist.

) } if (!stand.bericht) return

Der Ausführer hat noch keinen Bericht geschickt.

if (stand.bericht.veraltet) { return (

Der letzte Bericht ist von {zeitAus(stand.bericht.empfangen)}; die Angaben können veraltet sein.

) } return null } /** Gemeinsame Daten beider Homelab-Seiten — oder was stattdessen zu zeigen ist. */ function useHomelabBereich() { const instanz = useInstanz() const partner = usePartner() const homelabHier = instanz.data?.rolle === "homelab" const aktiv = homelabHier || !!(partner.data?.eingerichtet && partner.data.erreichbar) const homelab = useHomelab(aktiv) const laeufe = useHomelabLaeufe(aktiv) const hinweise = useHomelabHinweise(aktiv) const aktion = useZielAktion() let ersatz = null if (instanz.isPending || partner.isPending) { ersatz = } else if (!aktiv) { ersatz = partner.data ? : } else if (homelab.isPending) { // Solange der erste Bericht unterwegs ist, weiß die Seite nichts — also auch nicht „aktuell“ sagen. ersatz = } else if (!homelab.data) { ersatz = } const laufend = new Map() for (const l of laeufe.data?.laeufe ?? []) { if (l.status === "laeuft") laufend.set(l.ziel, l.schritte.at(-1) ?? "startet …") } return { ersatz, stand: homelab.data, laufend, laeufe: laeufe.data?.laeufe ?? [], hinweise: hinweise.data?.hinweise ?? [], aktion, } } /** Homelab · Cockpit: die Lage auf einen Blick, Hinweise und wie es jedem Gerät geht. */ export function HomelabSeite() { const navigate = useNavigate() const { ersatz, stand, laufend, laeufe, hinweise, aktion } = useHomelabBereich() const mess = useHomelabMesswerte("1h", !!stand) const speicher = useHomelabSpeicher(!!stand) if (ersatz || !stand) return ersatz const berichtZeit = stand.bericht ? zeitAus(stand.bericht.empfangen) : null // „Update läuft“ nur für Updates — ein Snapshot anlegen oder löschen ist keins. const imUpdate = new Set(laeufe.filter((x) => x.status === "laeuft" && !istSnapshotLauf(x)).map((x) => x.ziel)) const l = lage(stand, hinweise, imUpdate.size, berichtZeit) return ( <> lampeFuer(z, imUpdate.has(z.id)))} onAnsehen={() => { if (l.ziel === "updates") void navigate({ to: "/homelab/updates" }) else if (l.ziel) document.getElementById(l.ziel)?.scrollIntoView({ behavior: "smooth", block: "start" }) }} /> {hinweise.length > 0 && } {berichtZeit && Stand {berichtZeit}} Monitoring } > {mess.isError && (

CPU und Speicher erscheinen, sobald der Ausführer Messwerte schickt.

)}
{speicher.data?.bericht && ( aktion.mutate(a)} snapshotsLink={ Zu den Snapshots } /> )} ) } /** Homelab · Updates: je Gerät die Versionen, „Alle aktualisieren“ und was gelaufen ist. */ export function HomelabUpdatesSeite() { const { ersatz, stand, laufend, laeufe, aktion } = useHomelabBereich() const sammel = useSammellauf(!!stand) const quittieren = useSammellaufQuittieren() if (ersatz || !stand) return ersatz const lauf = sammel.data?.sammellauf ?? null const sammelLaeuft = lauf?.status === "laeuft" const knopf = offeneUpdates(stand.ziele).filter((u) => u.baustein.aktion).length return ( <> } >

Je Gerät, was läuft und was neu ist. Du startest jedes Update selbst — einzeln oder alle nacheinander. Vorher wird gesichert, danach geprüft; ist die Prüfung rot, geht es von selbst zurück.

{/* Ein fertiger Lauf bleibt sechs Stunden stehen — gemessen am letzten Bericht, nicht an der Uhr beim Zeichnen. */} {lauf && (sammelLaeuft || (lauf.ende && (stand.bericht?.empfangen ?? 0) - lauf.ende < 6 * 3600)) && quittieren.mutate(lauf.id)} />} aktion.mutate(a)} />
) }