From 1104200a30610ddc6169c2ae8448bf732d605da5 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Thu, 24 Sep 2026 22:18:56 +0200 Subject: [PATCH] oberflaeche v4: Uebersicht als Startseite, KI-Box und Homelab mit Monitoring, Homelab-Updates je Geraet mit Alle aktualisieren, Protokoll, Einstellungsseite, neues Tab-Symbol - Startseite "Alles auf einen Blick": Gesamtlage, je Bereich Messwerte mit Verlauf, wer welche Updates hat (Kurzform, Klick fuehrt in den Bereich), Hinweise, als Naechstes und zuletzt eingespielt - Menue: Uebersicht, KI-Box (Cockpit, Updates, Modelle, Monitoring), Homelab (Cockpit, Updates, Monitoring, Protokoll), Einstellungen; Bereichsfarben Indigo und Violett; alte Adressen /updates und /modelle leiten weiter - Homelab-Updates je Geraet: App-Version laeuft -> neu (mit Grund, wenn sie wartet), Pakete, Rueckweg, Knoepfe; Alle aktualisieren mit Plan in der Rueckfrage und Fortschritt; Cockpit zeigt die Gesundheit der Geraete - Einstellungen als Seite: KI-Box, Homelab (Arcane-Schluessel, Docker echt/Probelauf, Ausfuehrer), Meldungen, Software-Stand - Diagramme: Verlauf mit Fadenkreuz, Legende und Tabellenansicht, Kennzahlen mit Linie; Farben geprueft - Tab-Symbol: favicon.ico/.svg, apple-touch-icon, Manifest-Icons Co-Authored-By: Claude Opus 5.5 --- frontend/index.html | 2 + frontend/public/apple-touch-icon.png | Bin 0 -> 16488 bytes frontend/public/favicon.ico | Bin 0 -> 5128 bytes frontend/public/favicon.svg | 24 +- frontend/public/icon-192.png | Bin 0 -> 18733 bytes frontend/public/icon-512.png | Bin 0 -> 117929 bytes frontend/public/manifest.webmanifest | 22 +- frontend/src/app/MenueSchublade.tsx | 5 +- frontend/src/app/Seitenleiste.tsx | 100 +++--- frontend/src/app/Shell.tsx | 24 +- frontend/src/app/router.tsx | 75 +++- .../src/components/diagramme/Kennzahl.tsx | 42 +++ .../src/components/diagramme/Sparkline.tsx | 40 +++ frontend/src/components/diagramme/Verlauf.tsx | 187 ++++++++++ .../src/components/homelab/GeraeteListe.tsx | 149 -------- .../src/components/homelab/Gesundheit.tsx | 78 +++++ .../src/components/homelab/Sammellauf.tsx | 110 ++++++ .../components/homelab/UpdateListe.test.tsx | 89 ----- .../src/components/homelab/UpdateListe.tsx | 110 ------ .../components/homelab/UpdateTabelle.test.tsx | 75 ++++ .../src/components/homelab/UpdateTabelle.tsx | 156 +++++++++ frontend/src/index.css | 22 ++ frontend/src/lib/abfragen.ts | 94 ++++- frontend/src/lib/diagramm.ts | 39 +++ frontend/src/lib/homelab.ts | 10 + frontend/src/lib/messung.ts | 74 ++++ frontend/src/lib/navigation.ts | 42 ++- frontend/src/lib/typen.ts | 84 +++++ frontend/src/lib/uebersicht.test.ts | 80 +++++ frontend/src/lib/uebersicht.ts | 82 +++++ frontend/src/views/Einstellungen.test.tsx | 10 +- frontend/src/views/Einstellungen.tsx | 214 ++++++++++-- frontend/src/views/Homelab.tsx | 58 +++- frontend/src/views/HomelabProtokoll.tsx | 151 ++++++++ frontend/src/views/Monitoring.tsx | 163 +++++++++ frontend/src/views/Start.tsx | 2 +- frontend/src/views/Uebersicht.tsx | 324 ++++++++++++++++++ 37 files changed, 2244 insertions(+), 493 deletions(-) create mode 100644 frontend/public/apple-touch-icon.png create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/icon-192.png create mode 100644 frontend/public/icon-512.png create mode 100644 frontend/src/components/diagramme/Kennzahl.tsx create mode 100644 frontend/src/components/diagramme/Sparkline.tsx create mode 100644 frontend/src/components/diagramme/Verlauf.tsx delete mode 100644 frontend/src/components/homelab/GeraeteListe.tsx create mode 100644 frontend/src/components/homelab/Gesundheit.tsx create mode 100644 frontend/src/components/homelab/Sammellauf.tsx delete mode 100644 frontend/src/components/homelab/UpdateListe.test.tsx delete mode 100644 frontend/src/components/homelab/UpdateListe.tsx create mode 100644 frontend/src/components/homelab/UpdateTabelle.test.tsx create mode 100644 frontend/src/components/homelab/UpdateTabelle.tsx create mode 100644 frontend/src/lib/diagramm.ts create mode 100644 frontend/src/lib/messung.ts create mode 100644 frontend/src/lib/uebersicht.test.ts create mode 100644 frontend/src/lib/uebersicht.ts create mode 100644 frontend/src/views/HomelabProtokoll.tsx create mode 100644 frontend/src/views/Monitoring.tsx create mode 100644 frontend/src/views/Uebersicht.tsx diff --git a/frontend/index.html b/frontend/index.html index 7ba38d1..c02cdce 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,7 +5,9 @@ + + Homelab Orchestrator diff --git a/frontend/public/apple-touch-icon.png b/frontend/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4d13829ea28ed60f8a50d9e1dad93397fd7e1fee GIT binary patch literal 16488 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 literal 0 HcmV?d00001 diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f0782d1856fb3122960eef27c7e427f0eb1d365 GIT binary patch literal 5128 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 + + - - - - + + + + + + + - - - - - + + + + diff --git a/frontend/public/icon-192.png b/frontend/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..be88dab842b5999ba4d363d18d7301c9440fa685 GIT binary patch literal 18733 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 dNklY002ovPDHLkV1g2CigW+~ literal 0 HcmV?d00001 diff --git a/frontend/public/icon-512.png b/frontend/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..a62fe64fbc53c745c02d90f6fc32e387ee9c45a4 GIT binary patch literal 117929 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

&Jd#2V1Lt|C%A#UNLG6&Bw0`$>oS%+!SJ;vV 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 W``#_?*GJ$#kb_>nzmR_pP5Up~2bQ1! literal 0 HcmV?d00001 diff --git a/frontend/public/manifest.webmanifest b/frontend/public/manifest.webmanifest index 04f3bff..7f32b1b 100644 --- a/frontend/public/manifest.webmanifest +++ b/frontend/public/manifest.webmanifest @@ -3,7 +3,23 @@ "short_name": "Orchestrator", "start_url": "/", "display": "standalone", - "background_color": "#0d1117", - "theme_color": "#0d1117", - "icons": [] + "background_color": "#0e1012", + "theme_color": "#0e1012", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] } diff --git a/frontend/src/app/MenueSchublade.tsx b/frontend/src/app/MenueSchublade.tsx index e5a609d..409568a 100644 --- a/frontend/src/app/MenueSchublade.tsx +++ b/frontend/src/app/MenueSchublade.tsx @@ -5,9 +5,8 @@ import { Marke, Seitenleiste } from "./Seitenleiste" * Handy und Tablet: dieselbe Menüführung wie links am PC, als Schublade hinter dem Menü-Knopf. * Eigene Datei, damit sie (mit der Dialog-Kette von Radix) erst beim Öffnen lädt. */ -export function MenueSchublade({ onDienste, onEinstellungen, onSchliessen }: { +export function MenueSchublade({ onDienste, onSchliessen }: { onDienste: () => void - onEinstellungen: () => void onSchliessen: () => void }) { return ( @@ -24,7 +23,7 @@ export function MenueSchublade({ onDienste, onEinstellungen, onSchliessen }: { Bereiche und Seiten - + ) diff --git a/frontend/src/app/Seitenleiste.tsx b/frontend/src/app/Seitenleiste.tsx index cd94f14..c4136d8 100644 --- a/frontend/src/app/Seitenleiste.tsx +++ b/frontend/src/app/Seitenleiste.tsx @@ -1,8 +1,8 @@ import { Link } from "@tanstack/react-router" -import { ExternalLink, ScrollText, Settings } from "lucide-react" +import { ExternalLink, ScrollText } from "lucide-react" import { cn } from "cn" import { useInstanz } from "@/lib/abfragen" -import { BEREICHE, SEITEN, useBereichStand, type Verbindung } from "@/lib/navigation" +import { BEREICHE, type Ort, SEITEN, type Seite, useBereichStand, type Verbindung } from "@/lib/navigation" const VERBINDUNG: Record = { online: { punkt: "bg-gruen shadow-[0_0_8px_rgba(61,220,138,0.7)]", text: "online" }, @@ -11,17 +11,28 @@ const VERBINDUNG: Record = { fehlt: { punkt: "bg-text-3", text: "nicht eingerichtet" }, } +// Jeder Ort hat seine Farbe: die aktive Seite trägt sie als Balken links, Grund und Symbol. +const FARBE: Record = { + uebersicht: { aktiv: "!border-bernstein bg-bernstein-grund", symbol: "group-data-[status=active]:text-bernstein" }, + box: { aktiv: "!border-box bg-box-grund", symbol: "group-data-[status=active]:text-box" }, + homelab: { aktiv: "!border-lab bg-lab-grund", symbol: "group-data-[status=active]:text-lab" }, + system: { aktiv: "!border-text-2 bg-erhaben", symbol: "group-data-[status=active]:text-foreground" }, +} + +const KOPF: Record<"box" | "homelab", { text: string; balken: string }> = { + box: { text: "text-box-text", balken: "bg-box" }, + homelab: { text: "text-lab-text", balken: "bg-lab" }, +} + const EINTRAG = - "flex h-11 items-center gap-3 rounded-lg border-l-2 border-transparent pr-3 pl-3 text-[15px] text-text-2 hover:bg-erhaben hover:text-foreground" + "group flex h-11 items-center gap-3 rounded-lg border-l-2 border-transparent pr-3 pl-3 text-[15px] text-text-2 " + + "hover:bg-erhaben hover:text-foreground" /** Homelab Orchestrator (vormals MC2, Name seit 24.09.2026). */ export function Marke() { return (

- - - - + Homelab Orchestrator @@ -30,63 +41,71 @@ export function Marke() { ) } +function SeitenLink({ s, zahl, onGewaehlt }: { s: Seite; zahl?: number; onGewaehlt?: () => void }) { + const f = FARBE[s.ort] + return ( + + + {s.label} + {!!zahl && ( + + {zahl} + {zahl === 1 ? "Update" : "Updates"} bereit + + )} + + ) +} + /** - * Die Menüführung: je Bereich ein Block mit Verbindungsanzeige und seinen Seiten. Am PC steht sie fest - * links, am Handy in der Schublade hinter dem Menü-Knopf. + * Die Menüführung: oben die Übersicht, dann je Bereich ein farbiger Block mit Verbindungsanzeige und seinen + * Seiten, unten die Einstellungen. Am PC steht sie fest links, am Handy in der Schublade hinter dem Menü-Knopf. */ -export function Seitenleiste({ onDienste, onEinstellungen, onGewaehlt }: { +export function Seitenleiste({ onDienste, onGewaehlt }: { onDienste: () => void - onEinstellungen: () => void /** Nach jedem Klick — die Handy-Schublade schließt sich dann. */ onGewaehlt?: () => void }) { const stand = useBereichStand() // Das Hermes-Dashboard liefert nur die KI-Box selbst aus. const hermes = useInstanz().data?.rolle !== "homelab" + const seite = (to: Seite["to"]) => SEITEN.find((s) => s.to === to)! return ( -
diff --git a/frontend/src/app/router.tsx b/frontend/src/app/router.tsx index 9906e5f..fab85fb 100644 --- a/frontend/src/app/router.tsx +++ b/frontend/src/app/router.tsx @@ -1,42 +1,81 @@ -import { createRootRoute, createRoute, createRouter, lazyRouteComponent, Navigate } from "@tanstack/react-router" +import { + createRootRoute, createRoute, createRouter, lazyRouteComponent, Navigate, redirect, +} from "@tanstack/react-router" import { Absturz } from "./Absturz" import { Shell } from "./Shell" -import { StartSeite } from "@/views/Start" +import { UebersichtSeite } from "@/views/Uebersicht" const wurzel = createRootRoute({ component: Shell, - // Alte Lesezeichen (/ideen, /wissen, /cockpit …) führen einfach zum Start. + // Alte Lesezeichen (/ideen, /wissen, /cockpit …) führen einfach zur Übersicht. notFoundComponent: () => , }) -// Der Start ist der Normalfall und kommt im ersten Bündel mit; Updates und Modelle -// werden erst beim ersten Besuch nachgeladen (Budget der CI-Ampel: 140 KB gzip). -const start = createRoute({ getParentRoute: () => wurzel, path: "/", component: StartSeite }) -const updates = createRoute({ - getParentRoute: () => wurzel, - path: "/updates", +// Die Übersicht ist der Normalfall und kommt im ersten Bündel mit; alle anderen Seiten werden erst beim +// ersten Besuch nachgeladen (Budget: 140 KB gzip für den Einstieg). +const uebersicht = createRoute({ getParentRoute: () => wurzel, path: "/", component: UebersichtSeite }) + +// Bereich KI-Box +const box = createRoute({ + getParentRoute: () => wurzel, path: "/box", component: lazyRouteComponent(() => import("@/views/Start"), "StartSeite"), +}) +const boxUpdates = createRoute({ + getParentRoute: () => wurzel, path: "/box/updates", component: lazyRouteComponent(() => import("@/views/Updates"), "UpdatesSeite"), }) -const modelle = createRoute({ - getParentRoute: () => wurzel, - path: "/modelle", +const boxModelle = createRoute({ + getParentRoute: () => wurzel, path: "/box/modelle", component: lazyRouteComponent(() => import("@/views/Modelle"), "ModelleSeite"), }) +const boxMonitoring = createRoute({ + getParentRoute: () => wurzel, path: "/box/monitoring", + component: lazyRouteComponent(() => import("@/views/Monitoring"), "BoxMonitoringSeite"), +}) -// Der Bereich Homelab (Proxmox-PC) — getrennt von der KI-Box, mit eigenem Cockpit und eigenen Updates. +// Bereich Homelab const homelab = createRoute({ - getParentRoute: () => wurzel, - path: "/homelab", + getParentRoute: () => wurzel, path: "/homelab", component: lazyRouteComponent(() => import("@/views/Homelab"), "HomelabSeite"), }) const homelabUpdates = createRoute({ - getParentRoute: () => wurzel, - path: "/homelab/updates", + getParentRoute: () => wurzel, path: "/homelab/updates", component: lazyRouteComponent(() => import("@/views/Homelab"), "HomelabUpdatesSeite"), }) +const homelabMonitoring = createRoute({ + getParentRoute: () => wurzel, path: "/homelab/monitoring", + component: lazyRouteComponent(() => import("@/views/Monitoring"), "HomelabMonitoringSeite"), +}) +const homelabProtokoll = createRoute({ + getParentRoute: () => wurzel, path: "/homelab/protokoll", + component: lazyRouteComponent(() => import("@/views/HomelabProtokoll"), "HomelabProtokollSeite"), +}) + +const einstellungen = createRoute({ + getParentRoute: () => wurzel, path: "/einstellungen", + component: lazyRouteComponent(() => import("@/views/Einstellungen"), "EinstellungenSeite"), +}) + +// Frühere Adressen der KI-Box (bis 24.09.2026) leiten weiter — Lesezeichen und Links bleiben gültig. +const altUpdates = createRoute({ + getParentRoute: () => wurzel, + path: "/updates", + beforeLoad: () => { + throw redirect({ to: "/box/updates" }) + }, +}) +const altModelle = createRoute({ + getParentRoute: () => wurzel, + path: "/modelle", + beforeLoad: ({ location }) => { + throw redirect({ to: "/box/modelle", hash: location.hash || undefined }) + }, +}) export const router = createRouter({ - routeTree: wurzel.addChildren([start, updates, modelle, homelab, homelabUpdates]), + routeTree: wurzel.addChildren([ + uebersicht, box, boxUpdates, boxModelle, boxMonitoring, homelab, homelabUpdates, homelabMonitoring, + homelabProtokoll, einstellungen, altUpdates, altModelle, + ]), defaultPreload: "intent", defaultErrorComponent: Absturz, }) diff --git a/frontend/src/components/diagramme/Kennzahl.tsx b/frontend/src/components/diagramme/Kennzahl.tsx new file mode 100644 index 0000000..428dc1a --- /dev/null +++ b/frontend/src/components/diagramme/Kennzahl.tsx @@ -0,0 +1,42 @@ +import { cn } from "cn" +import { type Einheit, wertText } from "@/lib/messung" +import type { Messpunkt } from "@/lib/typen" +import { Sparkline } from "./Sparkline" + +const ZUSTAND = { + ruhig: { wert: "text-foreground", marke: null }, + gelb: { wert: "text-bernstein", marke: "hoch" }, + rot: { wert: "text-rot-text", marke: "kritisch" }, +} as const + +/** + * Eine Kennzahl mit Verlauf: Beschriftung, der aktuelle Wert groß, darunter die Linie des Zeitraums. + * Überschreitet der Wert eine Grenze, färbt er sich und sagt es auch in Worten („hoch“, „kritisch“). + */ +export function Kennzahl({ label, wert, einheit, punkte, max, zustand = "ruhig", farbe, zeitraumText }: { + label: string + wert: number | null | undefined + einheit: Einheit + punkte?: Messpunkt[] + max: number + zustand?: keyof typeof ZUSTAND + farbe?: string + /** z. B. „letzte Stunde“ — für die Beschreibung der Linie. */ + zeitraumText: string +}) { + const z = ZUSTAND[zustand] + return ( +
+ {label} + + {wertText(wert, einheit)} + {z.marke && {z.marke}} + + {punkte && punkte.length > 1 && ( + + )} + {/* Kommt der Verlauf noch (erster Abruf), bleibt die Höhe gleich — kein Springen der Kacheln. */} + {punkte && punkte.length <= 1 && } +
+ ) +} diff --git a/frontend/src/components/diagramme/Sparkline.tsx b/frontend/src/components/diagramme/Sparkline.tsx new file mode 100644 index 0000000..0aab07b --- /dev/null +++ b/frontend/src/components/diagramme/Sparkline.tsx @@ -0,0 +1,40 @@ +import { flaechenPfad, linienPfad } from "@/lib/diagramm" +import type { Messpunkt } from "@/lib/typen" + +/** + * Kleine Verlaufslinie für Kennzahl-Kacheln: keine Achsen, nur der Verlauf des Zeitraums in der Reihenfarbe + * mit zarter Fläche. Die Zahl daneben trägt den Wert; die Linie zeigt nur die Richtung. + */ +export function Sparkline({ punkte, max, farbe = "var(--reihe-1)", hoehe = 36, beschriftung }: { + punkte: Messpunkt[] + max: number + farbe?: string + hoehe?: number + /** Kurzbeschreibung für Screenreader, z. B. „CPU der letzten Stunde“. */ + beschriftung: string +}) { + const t0 = punkte[0]?.[0] ?? 0 + const t1 = punkte[punkte.length - 1]?.[0] ?? 1 + const breite = 100 + const x = (t: number) => (t1 > t0 ? ((t - t0) / (t1 - t0)) * breite : 0) + const y = (v: number) => hoehe - 2 - (Math.min(v, max) / (max || 1)) * (hoehe - 4) + const hatWerte = punkte.some(([, v]) => v !== null) + return ( + + {hatWerte && ( + <> + + + + )} + + ) +} diff --git a/frontend/src/components/diagramme/Verlauf.tsx b/frontend/src/components/diagramme/Verlauf.tsx new file mode 100644 index 0000000..9411524 --- /dev/null +++ b/frontend/src/components/diagramme/Verlauf.tsx @@ -0,0 +1,187 @@ +import { type KeyboardEvent, type PointerEvent, useEffect, useRef, useState } from "react" +import { flaechenPfad, linienPfad } from "@/lib/diagramm" +import { type Einheit, obergrenze, wertText, zeitText } from "@/lib/messung" +import type { Messpunkt } from "@/lib/typen" + +export interface Reihe { + name: string + punkte: Messpunkt[] + /** CSS-Farbe der Linie, z. B. var(--reihe-1). Text trägt sie nie — nur die Marke daneben. */ + farbe: string +} + +const RAND = { rechts: 12, oben: 10, unten: 24 } + +/** Breite des Behälters; ohne ResizeObserver (Tests) bleibt es beim Standard. */ +function useBreite(standard = 640) { + const ref = useRef(null) + const [breite, setBreite] = useState(standard) + useEffect(() => { + const el = ref.current + if (!el || typeof ResizeObserver === "undefined") return + const beobachter = new ResizeObserver(([e]) => setBreite(Math.max(240, Math.round(e.contentRect.width)))) + beobachter.observe(el) + return () => beobachter.disconnect() + }, []) + return [ref, breite] as const +} + +/** + * Zeitverlauf einer oder zweier Messreihen: dünne Linien, bei einer Reihe eine zarte Fläche, haarfeines Raster. + * Ein Fadenkreuz folgt Zeiger oder Pfeiltasten und zeigt alle Reihen am selben Zeitpunkt. Darunter liegen die + * Werte zum Aufklappen als Tabelle — der Tooltip ist nie der einzige Weg zu einer Zahl. + */ +export function Verlauf({ reihen, einheit, langerZeitraum, hoehe = 180, titel }: { + reihen: Reihe[] + einheit: Einheit + /** 7 Tage: Zeitachse mit Wochentag. */ + langerZeitraum: boolean + hoehe?: number + titel: string +}) { + const [ref, breite] = useBreite() + const [index, setIndex] = useState(null) + const basis = reihen[0]?.punkte ?? [] + const n = basis.length + const max = obergrenze(reihen.map((r) => r.punkte), einheit) + const ticksY = einheit === "prozent" ? [0, 25, 50, 75, 100] : [0, max / 2, max] + // Der linke Rand richtet sich nach der längsten Beschriftung („500 KB/s“ braucht mehr als „75 %“). + const links = Math.max(40, Math.max(...ticksY.map((v) => wertText(v, einheit).length)) * 7 + 12) + const RAND_L = { ...RAND, links } + const innenB = breite - RAND_L.links - RAND.rechts + const innenH = hoehe - RAND.oben - RAND.unten + const t0 = basis[0]?.[0] ?? 0 + const t1 = basis[n - 1]?.[0] ?? 1 + const x = (t: number) => RAND_L.links + (t1 > t0 ? ((t - t0) / (t1 - t0)) * innenB : 0) + const y = (v: number) => RAND.oben + innenH - (Math.min(v, max) / (max || 1)) * innenH + const grund = RAND.oben + innenH + const ticksX = n > 1 ? [0, 0.25, 0.5, 0.75, 1].map((f) => basis[Math.round(f * (n - 1))][0]) : [] + const hatWerte = reihen.some((r) => r.punkte.some(([, v]) => v !== null)) + + function zeigen(e: PointerEvent) { + if (n === 0) return + const r = e.currentTarget.getBoundingClientRect() + const anteil = Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)) + setIndex(Math.round(anteil * (n - 1))) + } + function taste(e: KeyboardEvent) { + if (n === 0) return + if (e.key === "ArrowLeft") setIndex((i) => Math.max(0, (i ?? n - 1) - 1)) + else if (e.key === "ArrowRight") setIndex((i) => Math.min(n - 1, (i ?? n - 1) + 1)) + else return + e.preventDefault() + } + + const aktiv = index !== null && basis[index] ? basis[index][0] : null + const tipLinks = aktiv !== null ? x(aktiv) : 0 + const letzte = basis.slice(-12) + + return ( +
+ {reihen.length > 1 && ( +
    + {reihen.map((r) => ( +
  • + + {r.name} +
  • + ))} +
+ )} +
+ setIndex((i) => i ?? (n ? n - 1 : null))} + onBlur={() => setIndex(null)} + className="block max-w-full outline-none focus-visible:rounded-md focus-visible:ring-2 focus-visible:ring-cyan/60" + > + {ticksY.map((v) => ( + + + + {wertText(v, einheit)} + + + ))} + {ticksX.map((t, i) => ( + + {zeitText(t, langerZeitraum)} + + ))} + {hatWerte ? ( + reihen.map((r) => ( + + {reihen.length === 1 && } + + + )) + ) : ( + + Noch keine Messwerte in diesem Zeitraum + + )} + {aktiv !== null && ( + + + {reihen.map((r) => { + const v = r.punkte[index!]?.[1] + return v === null || v === undefined ? null : ( + + ) + })} + + )} + {/* Trefferfläche: die ganze Zeichenfläche — niemand muss eine 2-px-Linie treffen. */} + setIndex(null)} /> + + {aktiv !== null && ( +
breite / 2 ? { right: breite - tipLinks + 10 } : { left: tipLinks + 10 }} + > + {zeitText(aktiv, true)} + {reihen.map((r) => ( + + + {wertText(r.punkte[index!]?.[1], einheit)} + {reihen.length > 1 && {r.name}} + + ))} +
+ )} +
+
+ Werte als Tabelle + + + + + {reihen.map((r) => )} + + + + {letzte.map(([t], i) => ( + + + {reihen.map((r) => ( + + ))} + + ))} + +
Zeit{r.name}
{zeitText(t, langerZeitraum)} + {wertText(r.punkte[n - letzte.length + i]?.[1], einheit)} +
+
+
+ ) +} diff --git a/frontend/src/components/homelab/GeraeteListe.tsx b/frontend/src/components/homelab/GeraeteListe.tsx deleted file mode 100644 index e2d6469..0000000 --- a/frontend/src/components/homelab/GeraeteListe.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { useState } from "react" -import { RefreshCw } from "lucide-react" -import { cn } from "cn" -import { Bestaetigen } from "@/components/cockpit/Bestaetigen" -import { Button } from "@/components/ui/button" -import { - appBaustein, artText, listenAlter, PLATTE_GELB, PLATTE_ROT, plattenAnteil, systemBaustein, versionText, -} from "@/lib/homelab" -import type { BausteinStand, ZielAktion, ZielStand } from "@/lib/typen" - -type Frage = { ziel: ZielStand; aktion: ZielAktion } | null - -/** Die App: nur die Version, solange sie aktuell ist — Farbe und Pfeil erst, wenn etwas ansteht. */ -function AppZelle({ ziel, b }: { ziel: ZielStand; b: BausteinStand | null }) { - if (!b) { - // Der Host hat keine App; dort steht die Proxmox-Version. - const pve = ziel.art === "proxmox-host" ? systemBaustein(ziel)?.installiert : null - return pve ? PVE {pve} : – - } - return ( - - {b.installiert && {versionText(b.installiert)}} - {b.zustand === "neu" && b.verfuegbar && → {b.verfuegbar}} - {b.zustand === "unbekannt" && unklar} - - ) -} - -/** Pakete bzw. Docker-Images des Geräts, in wenigen Worten. */ -function SystemText({ b }: { b: BausteinStand }) { - const docker = b.id === "docker" - if (b.zustand === "neu") return {b.kurz ? `${b.kurz} offen` : "Update bereit"} - if (b.zustand === "aktuell") return {docker ? "Images aktuell" : "aktuell"} - // Unklar heißt nur: die Paketlisten im Gast sind alt. Der Homelab-Teil erneuert sie wöchentlich selbst, - // darum bleibt das ruhig grau statt bernsteinfarben. - const datum = listenAlter(b.grund) - return ( - - {docker ? "Images unklar" : "unklar"} - {datum && · Listen vom {datum}} - - ) -} - -/** Belegung der Platte: ruhig bis 80 %, dann bernstein, ab 90 % rot. */ -function PlatteZelle({ ziel }: { ziel: ZielStand }) { - const anteil = plattenAnteil(ziel) - if (anteil === null) return – - const farbe = anteil >= PLATTE_ROT ? "text-rot-text" : anteil >= PLATTE_GELB ? "text-bernstein" : "text-text-2" - const gb = (n: number) => (n / 1e9).toFixed(1).replace(".", ",") - return ( - - {Math.min(100, Math.round(anteil * 100))} %{anteil >= PLATTE_ROT ? " voll" : ""} - - ) -} - -function Zeile({ ziel, laeuft, gesperrt, onFrage }: { - ziel: ZielStand - laeuft: string | null - gesperrt: boolean - onFrage: (f: Frage) => void -}) { - const app = appBaustein(ziel) - const sys = systemBaustein(ziel) - // Das Update selbst steht oben in „Updates“; hier bleibt nur das Suchen, wenn der Stand unklar ist. - const suchen = sys?.zustand === "unbekannt" ? sys.aktion : null - return ( -
-
- {ziel.name} - {artText(ziel) !== ziel.name && {artText(ziel)}} - {laeuft && Update läuft: {laeuft}} -
- - - - - {sys ? : –} - {suchen && ( - - )} - - - Platte - - - - {ziel.erreichbar !== null && ( - - )} - {ziel.erreichbar === null ? "–" : ziel.erreichbar ? "erreichbar" : "antwortet nicht"} - -
- ) -} - -/** Was wo läuft: ein Gerät je Zeile, mit App-Version, Paketstand und Erreichbarkeit. */ -export function GeraeteListe({ ziele, laufend, gesperrt, onAktion }: { - ziele: ZielStand[] - laufend: Map - gesperrt: boolean - onAktion: (a: ZielAktion) => void -}) { - const [frage, setFrage] = useState(null) - return ( - <> -
-
- Gerät - App - Pakete - Platte - Netz -
- {ziele.map((z) => ( - - ))} -
- { - if (frage) onAktion(frage.aktion) - setFrage(null) - }} - onNein={() => setFrage(null)} - /> - - ) -} diff --git a/frontend/src/components/homelab/Gesundheit.tsx b/frontend/src/components/homelab/Gesundheit.tsx new file mode 100644 index 0000000..83dc215 --- /dev/null +++ b/frontend/src/components/homelab/Gesundheit.tsx @@ -0,0 +1,78 @@ +import { Link } from "@tanstack/react-router" +import { cn } from "cn" +import { artText, plattenAnteil } from "@/lib/homelab" +import { laufzeit } from "@/lib/messung" +import type { GeraetMesswerte, ZielStand } from "@/lib/typen" + +const SPALTEN = "md:grid-cols-[minmax(0,1.3fr)_110px_minmax(0,1fr)_minmax(0,1.2fr)_minmax(0,1fr)_90px]" + +const gb = (b: number) => (b / 1e9).toFixed(1).replace(".", ",") + +/** Balken mit Wert: ruhig in Blau, ab gelb bernstein, ab rot rot — der Text sagt es zusätzlich in Zahlen. */ +function Balken({ wert, gelb, rot, zusatz }: { wert: number | null | undefined; gelb: number; rot: number; zusatz?: string }) { + if (wert === null || wert === undefined) return – + const farbe = wert >= rot ? "var(--rot)" : wert >= gelb ? "var(--bernstein)" : "var(--reihe-1)" + const text = wert >= rot ? "text-rot-text" : wert >= gelb ? "text-bernstein" : "text-text-2" + return ( + + + {Math.round(wert)} %{zusatz && · {zusatz}} + + + + + + ) +} + +/** Probleme zuerst (stumm, Platte voll), dann der Host, dann die Gäste in ihrer Reihenfolge. */ +function rang(z: ZielStand): number { + if (z.erreichbar === false) return 0 + const p = plattenAnteil(z) + if (p !== null && p >= 0.8) return 1 + return z.art === "proxmox-host" ? 2 : 3 +} + +/** + * Wie es den Geräten geht: Netz, CPU, Speicher, Platte und Laufzeit je Gerät. Die Zahlen kommen vom Monitoring + * (jede Minute), die Erreichbarkeit und die Platte der Container aus dem Bericht des Ausführers. + */ +export function Gesundheit({ ziele, messwerte }: { ziele: ZielStand[]; messwerte: GeraetMesswerte[] | undefined }) { + const mess = new Map((messwerte ?? []).map((g) => [g.id, g.aktuell])) + const sortiert = [...ziele].sort((a, b) => rang(a) - rang(b)) + return ( +
+ + {sortiert.map((z) => { + const a = mess.get(z.id) + const platte = a?.platte ?? (plattenAnteil(z) !== null ? plattenAnteil(z)! * 100 : null) + const ram = a?.ram_used && a.ram_total ? `${gb(a.ram_used)} / ${gb(a.ram_total)} GB` : undefined + return ( +
+
+ + {z.name} + + {artText(z) !== z.name && {artText(z)}} +
+ + {z.erreichbar !== null && } + {z.erreichbar === null ? "–" : z.erreichbar ? "erreichbar" : "antwortet nicht"} + + CPU + Speicher + Platte + Läuft seit {laufzeit(a?.uptime_s)} +
+ ) + })} +
+ ) +} diff --git a/frontend/src/components/homelab/Sammellauf.tsx b/frontend/src/components/homelab/Sammellauf.tsx new file mode 100644 index 0000000..c193979 --- /dev/null +++ b/frontend/src/components/homelab/Sammellauf.tsx @@ -0,0 +1,110 @@ +import { useState } from "react" +import { CircleCheck, CircleDashed, CircleX, LoaderCircle, SkipForward, Undo2 } from "lucide-react" +import { cn } from "cn" +import { Bestaetigen } from "@/components/cockpit/Bestaetigen" +import { Chip, type ChipArt } from "@/components/cockpit/Chip" +import { Button } from "@/components/ui/button" +import { useAlleAktualisieren, useSammelPlan } from "@/lib/abfragen" +import type { SammelSchrittStatus, Sammellauf } from "@/lib/typen" +import { flugplanZeit } from "@/lib/zeit" + +const RUECKWEG: Record = { snapshot: "Snapshot", sicherung: "Sicherung", keiner: "kein Rückweg" } + +/** „Alle aktualisieren“: fragt mit dem Plan des Homelab-Teils (Reihenfolge, Rückweg je Schritt) und startet. */ +export function AlleKnopf({ anzahl, gesperrt }: { anzahl: number; gesperrt: boolean }) { + const [offen, setOffen] = useState(false) + const plan = useSammelPlan(offen) + const starten = useAlleAktualisieren() + const schritte = plan.data?.schritte ?? [] + return ( + <> + + setOffen(false)} + onJa={() => { + if (!offen) return + starten.mutate(undefined) + setOffen(false) + }} + > + {plan.isPending ? ( +

Die Reihenfolge wird gelesen …

+ ) : plan.error ? ( +

Der Plan lässt sich nicht lesen: {plan.error.message}

+ ) : ( +
+
    + {schritte.map((s, i) => ( +
  1. + {i + 1}. + {s.name} · {s.was} + {RUECKWEG[s.rueckweg_art]} +
  2. + ))} +
+ {plan.data?.hinweis &&

{plan.data.hinweis}

} +
+ )} +
+ + ) +} + +const SCHRITT: Record = { + wartet: { Symbol: CircleDashed, farbe: "text-text-3", text: "wartet" }, + laeuft: { Symbol: LoaderCircle, farbe: "text-cyan", text: "läuft" }, + eingespielt: { Symbol: CircleCheck, farbe: "text-gruen", text: "eingespielt" }, + zurueckgerollt: { Symbol: Undo2, farbe: "text-bernstein", text: "zurückgerollt" }, + fehler: { Symbol: CircleX, farbe: "text-rot", text: "Fehler" }, + uebersprungen: { Symbol: SkipForward, farbe: "text-text-3", text: "übersprungen" }, +} + +const LAUF: Record = { + laeuft: { art: "cyan", text: "läuft" }, + fertig: { art: "gruen", text: "fertig" }, + abgebrochen: { art: "bernstein", text: "abgebrochen" }, +} + +/** Fortschritt des Sammellaufs: jeder Schritt mit Zustand; fertige Läufe bleiben bis zum nächsten stehen. */ +export function SammellaufFortschritt({ lauf }: { lauf: Sammellauf }) { + const fertig = lauf.schritte.filter((s) => !["wartet", "laeuft"].includes(s.status)).length + const l = LAUF[lauf.status] + return ( +
+
+ Alle aktualisieren · {fertig} von {lauf.schritte.length} + + {flugplanZeit(new Date(lauf.start * 1000).toISOString())} + {l.text} + +
+
+
+
+
    + {lauf.schritte.map((s) => { + const st = SCHRITT[s.status] + return ( +
  1. + + + {s.name} · {s.was} · {st.text} + {s.text && s.status !== "eingespielt" && {s.text}} + +
  2. + ) + })} +
+ {lauf.text &&

{lauf.text}

} +
+ ) +} diff --git a/frontend/src/components/homelab/UpdateListe.test.tsx b/frontend/src/components/homelab/UpdateListe.test.tsx deleted file mode 100644 index e80a113..0000000 --- a/frontend/src/components/homelab/UpdateListe.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { fireEvent, render, screen, within } from "@testing-library/react" -import type { OffenesUpdate } from "@/lib/homelab" -import type { ZielStand } from "@/lib/typen" -import { GeraeteListe } from "./GeraeteListe" -import { UpdateListe } from "./UpdateListe" - -const aktion = { - methode: "POST" as const, pfad: "/api/homelab/ziele/ct-102/update", label: "Jetzt updaten", - frage: "NetBird jetzt aktualisieren? Vorher wird ein Snapshot angelegt.", -} -const suchen = { - methode: "POST" as const, pfad: "/api/homelab/ziele/ct-102/suchen", label: "Nach Updates suchen", - frage: "Die Paketlisten im Gast jetzt erneuern?", -} - -const netbird: ZielStand = { - id: "ct-102", name: "NetBird", art: "container", instanz: "homelab", erreichbar: true, geprueft: 1_790_000_000, - rueckweg: "Snapshot vor jedem Update", zaehlung: { neu: 1, unbekannt: 1 }, offen: 1, - bausteine: [ - { id: "os", name: "Pakete", zustand: "unbekannt", installiert: null, verfuegbar: null, kurz: "", - grund: "Die Paketlisten sind vom 22.08.2026; was fehlt, weiß erst eine neue Suche.", aktion: suchen }, - { id: "app", name: "NetBird", zustand: "neu", installiert: "0.77.1", verfuegbar: "0.79.0", kurz: "0.77.1 → 0.79.0", - grund: null, aktion }, - ], -} -const update: OffenesUpdate = { ziel: netbird, baustein: netbird.bausteine[1] } - -describe("UpdateListe", () => { - it("zeigt Gerät, Versionen und Rückweg in einer Zeile", () => { - render( {}} />) - const zeile = screen.getByRole("row", { name: /NetBird/ }) - expect(within(zeile).getByText("Container 102 · App")).toBeInTheDocument() - expect(within(zeile).getAllByText("0.79.0").length).toBeGreaterThan(0) - expect(within(zeile).getByText("Snapshot vorher, bei Rot zurück")).toBeInTheDocument() - }) - - it("fragt mit dem Text des Geräts und startet erst nach dem Ja", () => { - const start = vi.fn() - render() - fireEvent.click(screen.getByRole("button", { name: "Jetzt updaten" })) - const dialog = screen.getByRole("dialog") - expect(within(dialog).getByText(/Vorher wird ein Snapshot angelegt/)).toBeInTheDocument() - expect(start).not.toHaveBeenCalled() - fireEvent.click(within(dialog).getByRole("button", { name: "Jetzt updaten" })) - expect(start).toHaveBeenCalledWith(aktion) - }) - - it("zeigt statt des Knopfs den Schritt, solange das Update läuft", () => { - render( {}} />) - expect(screen.getByRole("status")).toHaveTextContent("Läuft: Snapshot …") - expect(screen.queryByRole("button", { name: "Jetzt updaten" })).not.toBeInTheDocument() - }) - - it("nennt ohne Knopf den Grund", () => { - const ohne: OffenesUpdate = { - ziel: netbird, - baustein: { ...netbird.bausteine[1], aktion: null, grund: "Das Update-Skript wurde am 24.09. geändert; zur Sicherheit erst ab 26.09. 20:15." }, - } - render( {}} />) - expect(screen.getByText(/zur Sicherheit erst ab 26.09. 20:15/)).toBeInTheDocument() - expect(screen.queryByRole("button")).not.toBeInTheDocument() - }) - - it("sagt es, wenn nichts offen ist", () => { - render( {}} />) - expect(screen.getByText("Im Homelab ist alles aktuell.")).toBeInTheDocument() - }) -}) - -describe("GeraeteListe", () => { - it("zeigt je Gerät App, Paketstand und Netz — und bietet bei unklarem Stand das Suchen an", () => { - const start = vi.fn() - render() - const zeile = screen.getByRole("row", { name: /NetBird/ }) - expect(within(zeile).getByText("→ 0.79.0")).toBeInTheDocument() - expect(within(zeile).getByText("unklar")).toBeInTheDocument() - expect(within(zeile).getByText(/Listen vom 22.08.2026/)).toBeInTheDocument() - expect(within(zeile).getByText("erreichbar")).toBeInTheDocument() - fireEvent.click(within(zeile).getByRole("button", { name: "Nach Updates suchen" })) - fireEvent.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Nach Updates suchen" })) - expect(start).toHaveBeenCalledWith(suchen) - }) - - it("zeigt die Belegung der Platte und sagt „voll“ ab 90 %", () => { - render( {}} />) - expect(screen.getByText("98 % voll")).toHaveAttribute("title", "2,0 von 2,0 GB belegt") - }) -}) diff --git a/frontend/src/components/homelab/UpdateListe.tsx b/frontend/src/components/homelab/UpdateListe.tsx deleted file mode 100644 index 50a8ca2..0000000 --- a/frontend/src/components/homelab/UpdateListe.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState } from "react" -import { Bestaetigen } from "@/components/cockpit/Bestaetigen" -import { Button } from "@/components/ui/button" -import { neuText, type OffenesUpdate, rueckwegText, worum } from "@/lib/homelab" -import type { ZielAktion, ZielStand } from "@/lib/typen" - -type Frage = { ziel: ZielStand; aktion: ZielAktion } | null - -function Zeile({ u, laeuft, gesperrt, onFrage }: { - u: OffenesUpdate - laeuft: string | null - gesperrt: boolean - onFrage: (f: Frage) => void -}) { - const { ziel, baustein: b } = u - const app = b.id === "app" || b.id === "arcane" - const alt = b.installiert ?? "–" - const neu = neuText(b) - // Ohne Knopf sagt der Grund, warum (eigener Updater der App, Wartezeit nach einer Skriptänderung). - const mitte = b.aktion ? rueckwegText(ziel) : b.grund ?? "Wird nicht über den Orchestrator eingespielt." - - let rechts - if (laeuft) { - rechts = Läuft: {laeuft} - } else if (b.aktion) { - const aktion = b.aktion - rechts = ( - - ) - } - - // Am PC eine Tabellenzeile, am Handy ein Block: Gerät, Versionen, Rückweg, Knopf. - return ( -
-
- {ziel.name} - {worum(ziel, b)} -
- - {app && b.installiert ? <>{alt} → {neu} : {neu}} - - {alt} - {neu} - {mitte} - {rechts} -
- ) -} - -/** - * Was im Homelab zu tun ist — nur die offenen Updates, eine Zeile je Update. Jeder Knopf fragt vorher - * mit dem Text, den das Gerät selbst mitliefert. - */ -export function UpdateListe({ updates, laufend, gesperrt, onAktion }: { - updates: OffenesUpdate[] - /** Letzter Schritt laufender Updates je Gerät. */ - laufend: Map - gesperrt: boolean - onAktion: (a: ZielAktion) => void -}) { - const [frage, setFrage] = useState(null) - return ( - <> - {updates.length === 0 ? ( -

Im Homelab ist alles aktuell.

- ) : ( -
-
- Gerät - Läuft - Neu - Rückweg - Aktion -
- {updates.map((u) => ( - - ))} -
- )} - - { - if (frage) onAktion(frage.aktion) - setFrage(null) - }} - onNein={() => setFrage(null)} - /> - - ) -} diff --git a/frontend/src/components/homelab/UpdateTabelle.test.tsx b/frontend/src/components/homelab/UpdateTabelle.test.tsx new file mode 100644 index 0000000..65882a5 --- /dev/null +++ b/frontend/src/components/homelab/UpdateTabelle.test.tsx @@ -0,0 +1,75 @@ +import { fireEvent, render, screen, within } from "@testing-library/react" +import type { Sammellauf, ZielStand } from "@/lib/typen" +import { SammellaufFortschritt } from "./Sammellauf" +import { UpdateTabelle } from "./UpdateTabelle" + +const pakete = { + methode: "POST", pfad: "/api/homelab/ziele/ct-102/os-update", label: "Jetzt updaten", + frage: "48 Pakete im Gast jetzt einspielen? Vorher wird ein Snapshot angelegt.", +} + +const netbird: ZielStand = { + id: "ct-102", name: "NetBird", art: "container", instanz: "homelab", erreichbar: true, geprueft: null, + rueckweg: "Snapshot vor jedem Update", rueckweg_art: "snapshot", zaehlung: {}, offen: 2, + bausteine: [ + { id: "app", name: "NetBird", zustand: "neu", installiert: "0.77.1", verfuegbar: "0.79.0", kurz: "", + grund: "Das Update-Skript wurde am 24.09. geändert; zur Sicherheit erst ab 26.09. 08:06.", aktion: null }, + { id: "os", name: "Pakete", zustand: "neu", installiert: null, verfuegbar: null, kurz: "48 Pakete", grund: null, aktion: pakete }, + ], +} +const gitea: ZielStand = { + ...netbird, id: "ct-104", name: "Gitea", offen: 0, + bausteine: [ + { id: "app", name: "Gitea", zustand: "aktuell", installiert: "1.27.3", verfuegbar: "1.27.3", kurz: "", grund: null, aktion: null }, + { id: "os", name: "Pakete", zustand: "aktuell", installiert: null, verfuegbar: null, kurz: "", grund: null, aktion: null }, + ], +} + +describe("UpdateTabelle", () => { + it("zeigt je Gerät die App-Version, den Paketstand und warum ein Knopf fehlt", () => { + render( {}} />) + const zeilen = screen.getAllByRole("row").slice(1) + // Was sich einspielen lässt, steht oben — auch wenn es später geliefert wurde. + expect(within(zeilen[0]).getByRole("rowheader")).toHaveTextContent("NetBird") + expect(within(zeilen[0]).getByText("0.77.1")).toBeInTheDocument() + expect(within(zeilen[0]).getByText("→ 0.79.0")).toBeInTheDocument() + expect(within(zeilen[0]).getByText(/zur Sicherheit erst ab 26.09. 08:06/)).toBeInTheDocument() + expect(within(zeilen[0]).getByText("48 Pakete")).toBeInTheDocument() + expect(within(zeilen[0]).queryByRole("button", { name: "App updaten" })).not.toBeInTheDocument() + expect(within(zeilen[1]).getByText("1.27.3")).toBeInTheDocument() + expect(within(zeilen[1]).getAllByText("aktuell").length).toBe(2) + }) + + it("fragt vor dem Einspielen mit dem Text des Geräts", () => { + const start = vi.fn() + render() + fireEvent.click(screen.getByRole("button", { name: "Pakete einspielen" })) + const dialog = screen.getByRole("dialog") + expect(within(dialog).getByText(/Vorher wird ein Snapshot angelegt/)).toBeInTheDocument() + fireEvent.click(within(dialog).getByRole("button", { name: "Jetzt updaten" })) + expect(start).toHaveBeenCalledWith(pakete) + }) + + it("zeigt den laufenden Schritt statt der Knöpfe", () => { + render( {}} />) + expect(screen.getByRole("status")).toHaveTextContent("Läuft: Snapshot …") + expect(screen.queryByRole("button", { name: "Pakete einspielen" })).not.toBeInTheDocument() + }) +}) + +describe("SammellaufFortschritt", () => { + it("zählt die fertigen Schritte und nennt jeden Zustand", () => { + const lauf: Sammellauf = { + id: "s1", status: "laeuft", start: 1_790_000_000, ende: null, aktuell: 1, text: null, + schritte: [ + { ziel: "ct-104", name: "Gitea", baustein: "os", was: "73 Pakete", status: "eingespielt", lauf: "l1", text: null }, + { ziel: "ct-102", name: "NetBird", baustein: "os", was: "48 Pakete", status: "laeuft", lauf: "l2", text: null }, + { ziel: "pve", name: "Proxmox-Host", baustein: "pakete", was: "118 Pakete", status: "wartet", lauf: null, text: null }, + ], + } + render() + expect(screen.getByText("Alle aktualisieren · 1 von 3")).toBeInTheDocument() + expect(screen.getByText(/48 Pakete · läuft/)).toBeInTheDocument() + expect(screen.getByText(/118 Pakete · wartet/)).toBeInTheDocument() + }) +}) diff --git a/frontend/src/components/homelab/UpdateTabelle.tsx b/frontend/src/components/homelab/UpdateTabelle.tsx new file mode 100644 index 0000000..abc2184 --- /dev/null +++ b/frontend/src/components/homelab/UpdateTabelle.tsx @@ -0,0 +1,156 @@ +import { useState } from "react" +import { Search } from "lucide-react" +import { cn } from "cn" +import { Bestaetigen } from "@/components/cockpit/Bestaetigen" +import { Chip } from "@/components/cockpit/Chip" +import { Button } from "@/components/ui/button" +import { + appBaustein, artText, listenAlter, rueckwegText, systemBaustein, updateRang, versionText, +} from "@/lib/homelab" +import type { BausteinStand, ZielAktion, ZielStand } from "@/lib/typen" + +type Frage = { ziel: ZielStand; aktion: ZielAktion } | null + +// Feste Breite für die Knöpfe — sonst verschiebt eine Zeile ohne Knopf alle Spalten. +const SPALTEN = "md:grid-cols-[minmax(0,1.1fr)_minmax(0,1.3fr)_minmax(0,1fr)_minmax(0,0.9fr)_11.5rem]" + +/** Die App eines Geräts: was läuft, was neu ist — und warum es keinen Knopf gibt, wenn es keinen gibt. */ +function AppZelle({ ziel, b }: { ziel: ZielStand; b: BausteinStand | null }) { + if (!b) { + const pve = ziel.art === "proxmox-host" ? systemBaustein(ziel)?.installiert : null + return pve ? PVE {pve} : – + } + const laeuft = b.installiert ? versionText(b.installiert) : "–" + return ( + + + {laeuft} + {b.zustand === "neu" && b.verfuegbar && → {b.verfuegbar}} + {b.zustand === "aktuell" && aktuell} + {b.zustand === "unbekannt" && unklar} + + {b.zustand === "neu" && !b.aktion && b.grund && {b.grund}} + + ) +} + +/** Pakete (bzw. Docker-Images): Zahl offener Pakete, „aktuell“ oder „unklar“ mit dem Alter der Listen. */ +function PaketZelle({ b, onSuchen, gesperrt }: { b: BausteinStand | null; onSuchen: (a: ZielAktion) => void; gesperrt: boolean }) { + if (!b) return – + const docker = b.id === "docker" + let text + if (b.zustand === "neu") text = {b.kurz || "Update bereit"} + else if (b.zustand === "aktuell") text = {docker ? "Images aktuell" : "aktuell"} + else { + const datum = listenAlter(b.grund) + text = ( + + {docker ? "Images unklar" : "unklar"} + {datum && · Listen vom {datum}} + + ) + } + const suchen = b.zustand === "unbekannt" ? b.aktion : null + return ( + + {text} + {suchen && ( + + )} + + ) +} + +const KNOPF_TEXT: Record = { app: "App updaten", os: "Pakete einspielen", pakete: "Pakete einspielen" } + +function Zeile({ ziel, laeuft, gesperrt, onFrage }: { + ziel: ZielStand + laeuft: string | null + gesperrt: boolean + onFrage: (f: Frage) => void +}) { + const app = appBaustein(ziel) + const sys = systemBaustein(ziel) + const knoepfe = ziel.bausteine.filter((b) => b.zustand === "neu" && b.aktion) + const ruhig = updateRang(ziel) >= 2 + return ( +
+
+ {ziel.name} + {artText(ziel) !== ziel.name && {artText(ziel)}} +
+ App + + Pakete + onFrage({ ziel, aktion })} /> + + {knoepfe.length ? rueckwegText(ziel) : ""} + + {laeuft ? ( + Läuft: {laeuft} + ) : ( + knoepfe.map((b) => ( + + )) + )} + +
+ ) +} + +/** + * Die Update-Tabelle des Homelabs: eine Zeile je Gerät mit App-Version (läuft → neu), Paketstand, Rückweg und + * den Knöpfen. Oben, was sich jetzt einspielen lässt; darunter, was wartet; zuletzt, was aktuell ist. + */ +export function UpdateTabelle({ ziele, laufend, gesperrt, onAktion }: { + ziele: ZielStand[] + laufend: Map + gesperrt: boolean + onAktion: (a: ZielAktion) => void +}) { + const [frage, setFrage] = useState(null) + const sortiert = [...ziele].sort((a, b) => updateRang(a) - updateRang(b)) + const aktuell = ziele.filter((z) => updateRang(z) === 3).length + return ( + <> +
+ + {sortiert.map((z) => ( + + ))} +
+ {aktuell > 0 && aktuell === ziele.length && Alles aktuell} + { + if (frage) onAktion(frage.aktion) + setFrage(null) + }} + onNein={() => setFrage(null)} + /> + + ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css index 51ff9be..5a3fd4c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -67,6 +67,14 @@ --color-rot-text: var(--rot-text); --color-rot-grund: var(--rot-grund); --color-rot-rand: var(--rot-rand); + --color-box: var(--box); + --color-box-text: var(--box-text); + --color-box-grund: var(--box-grund); + --color-box-rand: var(--box-rand); + --color-lab: var(--lab); + --color-lab-text: var(--lab-text); + --color-lab-grund: var(--lab-grund); + --color-lab-rand: var(--lab-rand); --color-aus-grund: var(--aus-grund); --color-aus-rand: var(--aus-rand); --color-aus-text: var(--aus-text); @@ -124,6 +132,20 @@ --rot-text: #ff9a92; --rot-grund: #2e0f0d; --rot-rand: #a33a33; + /* Farben der beiden Bereiche — nur für Zugehörigkeit (Menü, Kopf, Karten), nie für Zustände: + Zustände bleiben grün/bernstein/rot/cyan. KI-Box = Indigo, Homelab = Violett. */ + --box: #8b95ff; + --box-text: #b4bbff; + --box-grund: #151a36; + --box-rand: #3f4aa8; + --lab: #c98bff; + --lab-text: #deb8ff; + --lab-grund: #22163a; + --lab-rand: #6d44a6; + /* Messreihen im Monitoring (geprüft auf dunklem Grund #15181b, auch für Farbfehlsichtigkeit): + erste Reihe Blau, zweite Magenta. Zustandsfarben bleiben für Zustände. */ + --reihe-1: #3987e5; + --reihe-2: #d55181; --aus-grund: #171b1f; --aus-rand: #2e343a; --aus-text: #9aa3ad; diff --git a/frontend/src/lib/abfragen.ts b/frontend/src/lib/abfragen.ts index 6ae279b..23ddb76 100644 --- a/frontend/src/lib/abfragen.ts +++ b/frontend/src/lib/abfragen.ts @@ -9,9 +9,10 @@ import { fehlerText, laeuftNoch } from "./anzeige" import { melden } from "./meldungen" import { stromVerbunden } from "./strom" import type { - AktionsErgebnis, Altrest, AufraeumKandidat, EinstellungenStand, Hinweis, HomelabLauf, HomelabStand, Instanz, - InstanzMitStand, Job, Modell, Nutzung, PartnerStatus, Radar, Sicherung, Start, TelegramTest, UpdateDetails, - ZielAktion, ZielStand, + AktionsErgebnis, Altrest, AufraeumKandidat, BoxMesswerte, EinstellungenStand, Hinweis, HomelabEinstellungen, + HomelabLauf, HomelabMesswerte, HomelabStand, Instanz, InstanzMitStand, Job, Modell, Nutzung, PartnerStatus, + ProtokollEintrag, Radar, SammelPlanSchritt, Sammellauf, Sicherung, Start, TelegramTest, UpdateDetails, ZielAktion, + ZielStand, Zeitraum, } from "./typen" import { gbText } from "./zeit" @@ -361,3 +362,90 @@ export const useAltrestLoeschen = () => erfolg: (_, e) => (e as AktionsErgebnis).text ?? "Gelöscht.", neuLaden: ["altreste", "start"], }) + +// --- Monitoring ------------------------------------------------------------------------------------------------ + +/** Takt der Messwerte: je feiner der Zeitraum, desto öfter. Fehlt die Schnittstelle (ältere Box), bleibt es still. */ +const MESS_TAKT: Record = { "1h": 60_000, "24h": 300_000, "7d": 900_000 } + +export const useBoxMesswerte = (zeitraum: Zeitraum, aktiv = true) => + useQuery({ + queryKey: ["messwerte", "box", zeitraum], + queryFn: () => api(`/api/messwerte?zeitraum=${zeitraum}`), + enabled: aktiv, + refetchInterval: MESS_TAKT[zeitraum], + placeholderData: (vorher) => vorher, + retry: false, + }) + +export const useHomelabMesswerte = (zeitraum: Zeitraum, aktiv: boolean) => + useQuery({ + queryKey: ["messwerte", "homelab", zeitraum], + queryFn: () => api(`/api/homelab/messwerte?zeitraum=${zeitraum}`), + enabled: aktiv, + refetchInterval: MESS_TAKT[zeitraum], + placeholderData: (vorher) => vorher, + retry: false, + }) + +// --- Homelab: „Alle aktualisieren“, Protokoll, Einstellungen ---------------------------------------------- + +export const useSammelPlan = (aktiv: boolean) => + useQuery({ + queryKey: ["homelab-sammelplan"], + queryFn: () => api<{ schritte: SammelPlanSchritt[]; hinweis: string | null }>("/api/homelab/alle/plan"), + enabled: aktiv, + retry: false, + }) + +export const useSammellauf = (aktiv: boolean) => + useQuery({ + queryKey: ["homelab-sammellauf"], + queryFn: () => api<{ sammellauf: Sammellauf | null }>("/api/homelab/sammellauf"), + enabled: aktiv, + // Läuft einer, alle 5 s nachsehen — sonst reicht der normale Takt. + refetchInterval: (q) => (q.state.data?.sammellauf?.status === "laeuft" ? 5_000 : 60_000), + retry: false, + }) + +export const useAlleAktualisieren = () => + useAktion(() => post("/api/homelab/alle"), { + erfolg: () => "Alle aktualisieren: gestartet. Die Schritte laufen nacheinander; das Ergebnis kommt als Meldung.", + neuLaden: ["homelab-sammellauf", "homelab-laeufe", "homelab"], + }) + +export const useHomelabProtokoll = (aktiv: boolean, berichte: boolean) => + useQuery({ + queryKey: ["homelab-protokoll", berichte], + queryFn: () => api<{ eintraege: ProtokollEintrag[] }>(`/api/homelab/protokoll?grenze=300&berichte=${berichte ? 1 : 0}`), + enabled: aktiv, + refetchInterval: takt(30_000), + retry: false, + }) + +export const useHomelabEinstellungen = (aktiv: boolean) => + useQuery({ + queryKey: ["homelab-einstellungen"], + queryFn: () => api("/api/homelab/einstellungen"), + enabled: aktiv, + retry: false, + }) + +export const useArcaneSchluessel = () => + useAktion( + (schluessel: string | null) => + schluessel === null + ? api("/api/homelab/einstellungen/arcane-schluessel", { method: "DELETE" }) + : post("/api/homelab/einstellungen/arcane-schluessel", { schluessel }), + { + erfolg: (v, e) => (e as AktionsErgebnis).text ?? (v === null ? "Arcane-Schlüssel entfernt." : "Arcane-Schlüssel gespeichert."), + neuLaden: ["homelab-einstellungen", "homelab"], + }, + ) + +export const useArcaneEcht = () => + useAktion((an: boolean) => post("/api/homelab/einstellungen/arcane-echt", { an }), { + erfolg: (an) => (an ? "Docker-Updates über Arcane laufen jetzt echt." : "Docker-Updates über Arcane sind wieder nur ein Probelauf."), + neuLaden: ["homelab-einstellungen", "homelab"], + }) + diff --git a/frontend/src/lib/diagramm.ts b/frontend/src/lib/diagramm.ts new file mode 100644 index 0000000..757d97c --- /dev/null +++ b/frontend/src/lib/diagramm.ts @@ -0,0 +1,39 @@ +import type { Messpunkt } from "@/lib/typen" + +// Geometrie der Diagramme (Pfade aus Messreihen) — ohne DOM, damit sie sich testen lässt. + +/** Pfad einer Reihe; Lücken (null) unterbrechen die Linie, statt sie zu überbrücken. */ +export function linienPfad(punkte: Messpunkt[], x: (t: number) => number, y: (v: number) => number): string { + let pfad = "" + let offen = false + for (const [t, v] of punkte) { + if (v === null) { + offen = false + continue + } + pfad += `${offen ? "L" : "M"}${x(t).toFixed(1)},${y(v).toFixed(1)}` + offen = true + } + return pfad +} + +/** Fläche unter einer Reihe bis zur Grundlinie — je zusammenhängendem Stück eine eigene Fläche. */ +export function flaechenPfad( + punkte: Messpunkt[], x: (t: number) => number, y: (v: number) => number, grund: number, +): string { + let pfad = "" + let stueck: [number, number][] = [] + const schliessen = () => { + if (stueck.length > 1) { + pfad += `M${stueck[0][0].toFixed(1)},${grund}` + stueck.map(([a, b]) => `L${a.toFixed(1)},${b.toFixed(1)}`).join("") + + `L${stueck[stueck.length - 1][0].toFixed(1)},${grund}Z` + } + stueck = [] + } + for (const [t, v] of punkte) { + if (v === null) schliessen() + else stueck.push([x(t), y(v)]) + } + schliessen() + return pfad +} diff --git a/frontend/src/lib/homelab.ts b/frontend/src/lib/homelab.ts index ec8f5d3..c0e9af6 100644 --- a/frontend/src/lib/homelab.ts +++ b/frontend/src/lib/homelab.ts @@ -48,6 +48,16 @@ export function offeneUpdates(ziele: ZielStand[]): OffenesUpdate[] { 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) diff --git a/frontend/src/lib/messung.ts b/frontend/src/lib/messung.ts new file mode 100644 index 0000000..441d247 --- /dev/null +++ b/frontend/src/lib/messung.ts @@ -0,0 +1,74 @@ +import type { Messpunkt } from "@/lib/typen" + +// Formate und kleine Rechnungen für das Monitoring — ohne DOM, damit sie sich testen lassen. + +export type Einheit = "prozent" | "grad" | "bytes_s" | "anzahl" + +const zahl = new Intl.NumberFormat("de-DE", { maximumFractionDigits: 1 }) +const ganz = new Intl.NumberFormat("de-DE", { maximumFractionDigits: 0 }) + +/** Bytes/s lesbar: 812 B/s, 45,3 KB/s, 12,1 MB/s. */ +export function bytesProSekunde(v: number): string { + if (v < 1000) return `${ganz.format(v)} B/s` + if (v < 1_000_000) return `${zahl.format(v / 1000)} KB/s` + if (v < 1_000_000_000) return `${zahl.format(v / 1_000_000)} MB/s` + return `${zahl.format(v / 1_000_000_000)} GB/s` +} + +export function wertText(v: number | null | undefined, einheit: Einheit): string { + if (v === null || v === undefined || Number.isNaN(v)) return "–" + if (einheit === "prozent") return `${ganz.format(v)} %` + if (einheit === "grad") return `${ganz.format(v)} °C` + if (einheit === "bytes_s") return bytesProSekunde(v) + return ganz.format(v) +} + +/** Der letzte Wert einer Reihe, der keine Lücke ist. */ +export function letzterWert(reihe: Messpunkt[] | undefined): number | null { + if (!reihe) return null + for (let i = reihe.length - 1; i >= 0; i--) { + const v = reihe[i][1] + if (v !== null) return v + } + return null +} + +/** Obergrenze der y-Achse: Prozent immer 100; sonst ein runder Wert über dem Maximum (mindestens 1). */ +export function obergrenze(reihen: (Messpunkt[] | undefined)[], einheit: Einheit): number { + if (einheit === "prozent") return 100 + let max = 0 + for (const r of reihen) for (const [, v] of r ?? []) if (v !== null && v > max) max = v + if (einheit === "grad") return Math.max(60, Math.ceil((max + 5) / 10) * 10) + if (max <= 0) return 1 + const stufe = 10 ** Math.floor(Math.log10(max)) + for (const f of [1, 2, 2.5, 5, 10]) if (f * stufe >= max) return f * stufe + return 10 * stufe +} + +/** Bei Grenzwerten die Farbe des Zustands: ruhig, bernstein ab gelb, rot ab rot. */ +export function zustandBei(v: number | null, gelb: number, rot: number): "ruhig" | "gelb" | "rot" { + if (v === null) return "ruhig" + return v >= rot ? "rot" : v >= gelb ? "gelb" : "ruhig" +} + +const uhr = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin" }) +const tagUhr = new Intl.DateTimeFormat("de-DE", { + weekday: "short", hour: "2-digit", minute: "2-digit", timeZone: "Europe/Berlin", +}) + +/** Zeit an der x-Achse: bis 24 h die Uhrzeit, darüber mit Wochentag. */ +export function zeitText(t: number, langerZeitraum: boolean): string { + const d = new Date(t * 1000) + return (langerZeitraum ? tagUhr : uhr).format(d).replace(",", "") +} + +/** Laufzeit kurz: „3 T 4 h“, „5 h 12 min“. */ +export function laufzeit(s: number | null | undefined): string { + if (!s || s < 0) return "–" + const t = Math.floor(s / 86400) + const h = Math.floor((s % 86400) / 3600) + const m = Math.floor((s % 3600) / 60) + if (t > 0) return `${t} T ${h} h` + if (h > 0) return `${h} h ${m} min` + return `${m} min` +} diff --git a/frontend/src/lib/navigation.ts b/frontend/src/lib/navigation.ts index 4383f5a..9f18b92 100644 --- a/frontend/src/lib/navigation.ts +++ b/frontend/src/lib/navigation.ts @@ -1,41 +1,61 @@ import { useRouterState } from "@tanstack/react-router" -import { Cpu, LayoutDashboard, type LucideIcon, RefreshCw } from "lucide-react" +import { + Activity, Cpu, Gauge, LayoutGrid, type LucideIcon, RefreshCw, ScrollText, Settings, +} from "lucide-react" import { useBoxZiele, useHomelab, useInstanz, usePartner } from "@/lib/abfragen" import { offeneUpdates } from "@/lib/homelab" import { useStromLage } from "@/lib/strom" -// Zwei Bereiche, sauber getrennt: die KI-Box (Box-Wart) und das Homelab (Proxmox-PC). Jeder hat seine -// eigenen Seiten und seine eigene Verbindungsanzeige; keine Seite zeigt etwas aus dem anderen Bereich. +// Oben die Übersicht über alles, darunter zwei sauber getrennte Bereiche — die KI-Box (Box-Wart) und das +// Homelab (Proxmox-PC) —, jeder mit eigenen Seiten und eigener Verbindungsanzeige, unten die Einstellungen. export type Bereich = "box" | "homelab" +export type Ort = Bereich | "uebersicht" | "system" export const BEREICHE: { id: Bereich; name: string }[] = [ { id: "box", name: "KI-Box" }, { id: "homelab", name: "Homelab" }, ] +export type SeitenPfad = + | "/" | "/box" | "/box/updates" | "/box/modelle" | "/box/monitoring" + | "/homelab" | "/homelab/updates" | "/homelab/monitoring" | "/homelab/protokoll" | "/einstellungen" + export interface Seite { - to: "/" | "/updates" | "/modelle" | "/homelab" | "/homelab/updates" + to: SeitenPfad label: string Icon: LucideIcon - bereich: Bereich + ort: Ort /** Zeigt die Zahl offener Updates des Bereichs. */ zaehlt?: boolean } export const SEITEN: Seite[] = [ - { to: "/", label: "Cockpit", Icon: LayoutDashboard, bereich: "box" }, - { to: "/updates", label: "Updates", Icon: RefreshCw, bereich: "box", zaehlt: true }, - { to: "/modelle", label: "Modelle", Icon: Cpu, bereich: "box" }, - { to: "/homelab", label: "Cockpit", Icon: LayoutDashboard, bereich: "homelab" }, - { to: "/homelab/updates", label: "Updates", Icon: RefreshCw, bereich: "homelab", zaehlt: true }, + { to: "/", label: "Übersicht", Icon: LayoutGrid, ort: "uebersicht" }, + { to: "/box", label: "Cockpit", Icon: Gauge, ort: "box" }, + { to: "/box/updates", label: "Updates", Icon: RefreshCw, ort: "box", zaehlt: true }, + { to: "/box/modelle", label: "Modelle", Icon: Cpu, ort: "box" }, + { to: "/box/monitoring", label: "Monitoring", Icon: Activity, ort: "box" }, + { to: "/homelab", label: "Cockpit", Icon: Gauge, ort: "homelab" }, + { to: "/homelab/updates", label: "Updates", Icon: RefreshCw, ort: "homelab", zaehlt: true }, + { to: "/homelab/monitoring", label: "Monitoring", Icon: Activity, ort: "homelab" }, + { to: "/homelab/protokoll", label: "Protokoll", Icon: ScrollText, ort: "homelab" }, + { to: "/einstellungen", label: "Einstellungen", Icon: Settings, ort: "system" }, ] +/** Was über dem Seitentitel steht. */ +export const ORT_NAME: Record = { + uebersicht: "Alles auf einen Blick", + box: "KI-Box", + homelab: "Homelab", + system: "Beide Instanzen", +} + export const bereichName = (b: Bereich) => BEREICHE.find((x) => x.id === b)?.name ?? b /** Welche Seite gerade offen ist — für die Überschrift oben. */ export function useAktuelleSeite(): Seite { - const pfad = useRouterState({ select: (s) => s.location.pathname }) + const pfad = useRouterState({ select: (s) => s.location.pathname }).replace(/\/+$/, "") || "/" return SEITEN.find((s) => s.to === pfad) ?? SEITEN[0] } diff --git a/frontend/src/lib/typen.ts b/frontend/src/lib/typen.ts index 880d7fe..e5619fd 100644 --- a/frontend/src/lib/typen.ts +++ b/frontend/src/lib/typen.ts @@ -377,3 +377,87 @@ export interface Altrest { groesse: string hinweis: string } + +// --- Monitoring (GET /api/messwerte, GET /api/homelab/messwerte, seit 24.09.2026) -------------------------- + +export type Zeitraum = "1h" | "24h" | "7d" + +/** Ein Punkt einer Messreihe: [Unix-Sekunden des Schrittbeginns, Wert oder null bei einer Lücke]. */ +export type Messpunkt = [number, number | null] + +/** KI-Box: cpu, ram, gpu, platte in %, temp_cpu/temp_gpu in °C, netz_rx/netz_tx in Bytes/s, tokens je Minute. */ +export interface BoxMesswerte { + zeitraum: Zeitraum + schritt_s: number + reihen: Partial> + aktuell: Partial> +} + +export interface GeraetMesswerte { + id: string + name: string + art: "proxmox-host" | "container" | "vm" | string + reihen: Partial> + aktuell: { + cpu?: number | null + ram?: number | null + ram_used?: number | null + ram_total?: number | null + platte?: number | null + netz_rx?: number | null + netz_tx?: number | null + temp_cpu?: number | null + load1?: number | null + uptime_s?: number | null + } +} + +export interface HomelabMesswerte { + zeitraum: Zeitraum + schritt_s: number + geraete: GeraetMesswerte[] +} + +// --- Homelab: „Alle aktualisieren“, Protokoll, Einstellungen (seit 24.09.2026) --------------------------- + +export interface SammelPlanSchritt { + ziel: string + name: string + baustein: string + was: string + rueckweg_art: "snapshot" | "sicherung" | "keiner" +} + +export type SammelSchrittStatus = "wartet" | "laeuft" | "eingespielt" | "zurueckgerollt" | "fehler" | "uebersprungen" + +export interface Sammellauf { + id: string + status: "laeuft" | "fertig" | "abgebrochen" + start: number + ende: number | null + aktuell: number | null + schritte: (Omit & { status: SammelSchrittStatus; lauf: string | null; text: string | null })[] + text: string | null +} + +export interface ProtokollEintrag { + zeit: number + art: "auftrag" | "lauf" | "sammellauf" | "hinweis" | "meldung" | "pflege" + titel: string + text: string | null + stufe: "ok" | "info" | "warn" | "fehler" + ziel: string | null + details: string | null +} + +export interface HomelabEinstellungen { + arcane: { + url: string + schluessel: "hinterlegt" | "fehlt" | "umgebung" + echt: boolean + echt_quelle: "einstellung" | "umgebung" | "standard" + } + karenz_h: number + ausfuehrer: { verbunden: boolean; zuletzt: number | null; nur_lesen: boolean; sicherung_speicher: string | null } +} + diff --git a/frontend/src/lib/uebersicht.test.ts b/frontend/src/lib/uebersicht.test.ts new file mode 100644 index 0000000..846c96f --- /dev/null +++ b/frontend/src/lib/uebersicht.test.ts @@ -0,0 +1,80 @@ +import { flaechenPfad, linienPfad } from "@/lib/diagramm" +import { bytesProSekunde, laufzeit, letzterWert, obergrenze, wertText, zustandBei } from "@/lib/messung" +import type { BausteinStand, ZielStand } from "@/lib/typen" +import { gesamtlage, kurzform, kurzformen } from "@/lib/uebersicht" + +const b = (x: Partial & Pick): BausteinStand => ({ + name: x.id, installiert: null, verfuegbar: null, kurz: "", grund: null, aktion: null, ...x, +}) +const knopf = { methode: "POST", pfad: "/x", frage: "?", label: "Jetzt updaten" } +const ziel = (id: string, name: string, bausteine: BausteinStand[], art = "container"): ZielStand => ({ + id, name, art, instanz: "homelab", erreichbar: true, geprueft: null, rueckweg: null, zaehlung: {}, offen: 0, bausteine, +}) + +describe("kurzform", () => { + it("fasst App und Pakete eines Geräts in eine Zeile, samt Grund ohne Knopf", () => { + const netbird = ziel("ct-102", "NetBird", [ + b({ id: "app", zustand: "neu", verfuegbar: "0.79.0", + grund: "Das Update-Skript wurde am 24.09. geändert; zur Sicherheit erst ab 26.09. 08:06." }), + b({ id: "os", zustand: "neu", kurz: "48 Pakete", aktion: knopf }), + ]) + expect(kurzform(netbird)).toEqual({ id: "ct-102", name: "NetBird", teile: ["App → 0.79.0 (ab 26.09. 08:06)", "48 Pakete"], knopf: true }) + }) + + it("nennt Bausteine der KI-Box mit ihrem Namen", () => { + const box = ziel("ki-box", "KI-Box", [ + b({ id: "os", name: "Betriebssystem", zustand: "neu", kurz: "10 Pakete", aktion: knopf }), + b({ id: "hermes", name: "Hermes", zustand: "neu", kurz: "926 Änderungen", aktion: knopf }), + b({ id: "engine", name: "Motor", zustand: "aktuell" }), + ], "ki-box") + expect(kurzform(box)?.teile).toEqual(["10 Pakete", "Hermes · 926 Änderungen"]) + }) + + it("lässt aktuelle Geräte weg und stellt Knopf-Updates nach vorn", () => { + const arcane = ziel("vm-106", "Arcane", [b({ id: "arcane", zustand: "neu", verfuegbar: "2.13.1", grund: "über die eigene Oberfläche" })]) + const gitea = ziel("ct-104", "Gitea", [b({ id: "os", zustand: "neu", kurz: "73 Pakete", aktion: knopf })]) + const ruhig = ziel("ct-103", "PVE Scripts", [b({ id: "app", zustand: "aktuell" })]) + expect(kurzformen([arcane, ruhig, gitea]).map((z) => z.id)).toEqual(["ct-104", "vm-106"]) + expect(kurzform(arcane)?.teile).toEqual(["App → 2.13.1 (über die App)"]) + }) +}) + +describe("gesamtlage", () => { + const leer = { getrennt: [], stumm: [], hinweiseRot: 0, hinweise: 0, updates: 0 } + it("stellt Verbindung vor Störung vor Hinweis vor Updates", () => { + expect(gesamtlage({ ...leer, getrennt: ["Der Homelab-Teil"], hinweise: 2 }).stufe).toBe("rot") + expect(gesamtlage({ ...leer, stumm: ["AdGuard"] })).toMatchObject({ stufe: "rot", titel: "Störung" }) + expect(gesamtlage({ ...leer, hinweise: 1, updates: 3 })).toMatchObject({ stufe: "gelb", titel: "1 Hinweis" }) + expect(gesamtlage({ ...leer, updates: 3 })).toMatchObject({ stufe: "info", text: "3 Updates stehen bereit." }) + expect(gesamtlage(leer).stufe).toBe("ok") + }) +}) + +describe("Messwerte", () => { + it("formatiert Raten, Laufzeiten und Einheiten", () => { + expect(bytesProSekunde(812)).toBe("812 B/s") + expect(bytesProSekunde(45_300)).toBe("45,3 KB/s") + expect(bytesProSekunde(12_100_000)).toBe("12,1 MB/s") + expect(laufzeit(3 * 86400 + 4 * 3600)).toBe("3 T 4 h") + expect(laufzeit(5 * 3600 + 12 * 60)).toBe("5 h 12 min") + expect(wertText(null, "prozent")).toBe("–") + expect(wertText(42.4, "grad")).toBe("42 °C") + }) + + it("findet runde Obergrenzen und den letzten Wert vor einer Lücke", () => { + expect(obergrenze([[[0, 12]]], "prozent")).toBe(100) + expect(obergrenze([[[0, 3_700_000]]], "bytes_s")).toBe(5_000_000) + expect(obergrenze([[[0, 71]]], "grad")).toBe(80) + expect(letzterWert([[0, 5], [60, 7], [120, null]])).toBe(7) + expect(zustandBei(92, 80, 90)).toBe("rot") + expect(zustandBei(null, 80, 90)).toBe("ruhig") + }) + + it("unterbricht Linien und Flächen an Lücken", () => { + const x = (t: number) => t + const y = (v: number) => 100 - v + expect(linienPfad([[0, 10], [1, 20], [2, null], [3, 30]], x, y)).toBe("M0.0,90.0L1.0,80.0M3.0,70.0") + const flaeche = flaechenPfad([[0, 10], [1, 20], [2, null], [3, 30], [4, 40]], x, y, 100) + expect(flaeche.match(/Z/g)?.length).toBe(2) + }) +}) diff --git a/frontend/src/lib/uebersicht.ts b/frontend/src/lib/uebersicht.ts new file mode 100644 index 0000000..86f1562 --- /dev/null +++ b/frontend/src/lib/uebersicht.ts @@ -0,0 +1,82 @@ +import { systemBaustein, appBaustein } from "@/lib/homelab" +import type { BausteinStand, ZielStand } from "@/lib/typen" + +// Die Kurzform für die Übersicht: wer hat wo welche Updates — in einer Zeile je Gerät. + +export interface KurzZeile { + id: string + name: string + teile: string[] + /** Etwas davon lässt sich per Knopf einspielen. */ + knopf: boolean +} + +/** Warum ein Update keinen Knopf hat, in zwei, drei Worten. */ +function wartetText(b: BausteinStand): string | null { + if (b.aktion) return null + const g = b.grund ?? "" + const ab = g.match(/erst ab (\d{2}\.\d{2}\.(?:\d{4})? ?\d{2}:\d{2})/)?.[1] + if (ab) return `ab ${ab}` + if (/eigene Oberfläche|eigenen Oberfläche|eigenes Update/i.test(g)) return "über die App" + return null +} + +/** „App → 0.79.0 (ab 26.09. 08:06)“, „48 Pakete“, „Images“ — was an einem Gerät ansteht. */ +export function kurzform(ziel: ZielStand): KurzZeile | null { + const neu = ziel.bausteine.filter((b) => b.zustand === "neu") + if (neu.length === 0) return null + const teile: string[] = [] + const app = appBaustein(ziel) + if (app?.zustand === "neu") { + const w = wartetText(app) + teile.push(`App → ${app.verfuegbar ?? "neu"}${w ? ` (${w})` : ""}`) + } + const sys = systemBaustein(ziel) + if (sys?.zustand === "neu") teile.push(sys.id === "docker" ? "Images" : sys.kurz || "Pakete") + // KI-Box: Bausteine heißen selbst (Betriebssystem, Hermes, Motor …). + for (const b of neu) { + if (b === app || b === sys) continue + teile.push(`${b.name}${b.kurz ? ` · ${b.kurz}` : ""}`) + } + return { id: ziel.id, name: ziel.name, teile, knopf: neu.some((b) => !!b.aktion) } +} + +/** Die KI-Box ist ein Gerät mit mehreren Bausteinen — dort steht je Baustein eine Zeile. */ +function bausteinZeilen(ziel: ZielStand): KurzZeile[] { + return ziel.bausteine + .filter((b) => b.zustand === "neu") + .map((b) => ({ id: `${ziel.id}-${b.id}`, name: b.name, teile: [b.kurz || b.verfuegbar || "neu"], knopf: !!b.aktion })) +} + +/** Alle Kurzzeilen eines Bereichs: erst, was per Knopf geht, dann der Rest. */ +export function kurzformen(ziele: ZielStand[]): KurzZeile[] { + const zeilen = ziele.flatMap((z) => (z.art === "ki-box" ? bausteinZeilen(z) : [kurzform(z)])) + .filter((z): z is KurzZeile => z !== null) + return [...zeilen.filter((z) => z.knopf), ...zeilen.filter((z) => !z.knopf)] +} + +export type Lagestufe = "ok" | "info" | "gelb" | "rot" + +/** Die Gesamtlage über beide Bereiche — Störung vor Hinweis vor offenen Updates. */ +export function gesamtlage(e: { + getrennt: string[] + stumm: string[] + hinweiseRot: number + hinweise: number + updates: number +}): { stufe: Lagestufe; titel: string; text: string } { + if (e.getrennt.length > 0) { + return { stufe: "rot", titel: "Verbindung weg", text: `${e.getrennt.join(" und ")} antwortet nicht.` } + } + if (e.stumm.length > 0 || e.hinweiseRot > 0) { + const was = e.stumm.length > 0 ? `${e.stumm.join(", ")} antwortet nicht` : "Ein roter Hinweis wartet" + return { stufe: "rot", titel: "Störung", text: `${was}.` } + } + if (e.hinweise > 0) { + return { stufe: "gelb", titel: e.hinweise === 1 ? "1 Hinweis" : `${e.hinweise} Hinweise`, text: "Bei Gelegenheit ansehen." } + } + if (e.updates > 0) { + return { stufe: "info", titel: "Alles läuft", text: `${e.updates} ${e.updates === 1 ? "Update steht" : "Updates stehen"} bereit.` } + } + return { stufe: "ok", titel: "Alles läuft", text: "Keine Hinweise, keine offenen Updates." } +} diff --git a/frontend/src/views/Einstellungen.test.tsx b/frontend/src/views/Einstellungen.test.tsx index 127c593..117785a 100644 --- a/frontend/src/views/Einstellungen.test.tsx +++ b/frontend/src/views/Einstellungen.test.tsx @@ -2,7 +2,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import { fireEvent, render, screen, waitFor, within } from "@testing-library/react" import { api, post } from "@/lib/api" import type { EinstellungenStand, InstanzMitStand } from "@/lib/typen" -import { Einstellungen } from "./Einstellungen" +import { EinstellungenSeite } from "./Einstellungen" vi.mock("@/lib/api", async (original) => ({ ...(await original()), @@ -36,6 +36,12 @@ function antworten(partnerCommit = "40d2840") { "/api/einstellungen": BOX, "/api/instanz": instanz("box", "40d2840"), "/api/partner/instanz": instanz("homelab", partnerCommit), + "/api/partner": { eingerichtet: true, name: "Homelab", erreichbar: true }, + "/api/homelab/einstellungen": { + arcane: { url: "http://192.168.178.28:3552", schluessel: "fehlt", echt: false, echt_quelle: "standard" }, + karenz_h: 48, + ausfuehrer: { verbunden: true, zuletzt: 1_790_000_000, nur_lesen: false, sicherung_speicher: "local" }, + }, } vi.mocked(api).mockImplementation((async (pfad: string) => { if (pfad in daten) return daten[pfad] @@ -47,7 +53,7 @@ function zeigen() { const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) render( - {}} /> + , ) } diff --git a/frontend/src/views/Einstellungen.tsx b/frontend/src/views/Einstellungen.tsx index 7b8df5f..ce70458 100644 --- a/frontend/src/views/Einstellungen.tsx +++ b/frontend/src/views/Einstellungen.tsx @@ -1,17 +1,19 @@ import { type ReactNode, useState } from "react" import { useQuery, useQueryClient } from "@tanstack/react-query" import { cn } from "cn" -import { ExternalLink } from "lucide-react" +import { ExternalLink, KeyRound } from "lucide-react" import { Bestaetigen } from "@/components/cockpit/Bestaetigen" +import { Chip } from "@/components/cockpit/Chip" import { Button } from "@/components/ui/button" -import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { - useEinstellungenStand, useInstanz, useRadarSchalten, useSoftwareStand, useTelegramTest, useUpdateFensterSetzen, + useArcaneEcht, useArcaneSchluessel, useEinstellungenStand, useHomelabEinstellungen, useInstanz, usePartner, + useRadarSchalten, useSoftwareStand, useTelegramTest, useUpdateFensterSetzen, } from "@/lib/abfragen" import { api, post } from "@/lib/api" import type { Rolle } from "@/lib/instanz" import { melden } from "@/lib/meldungen" -import type { EinstellungenStand, SoftwareStand } from "@/lib/typen" +import type { EinstellungenStand, HomelabEinstellungen, SoftwareStand } from "@/lib/typen" +import { flugplanZeit } from "@/lib/zeit" interface GeheimnisStand { hf_token_gesetzt: boolean @@ -49,10 +51,31 @@ function neustartFenster(tag: number, uhrzeit: string): string { return tagEnde === tag ? `${TAGE[tag - 1]} ${von}–${bis}` : `${TAGE[tag - 1]} ${von} – ${TAGE[tagEnde - 1]} ${bis}` } -function Abschnitt({ titel, children, zuletzt }: { titel: string; children: ReactNode; zuletzt?: boolean }) { +function Abschnitt({ titel, children }: { titel: string; children: ReactNode }) { return ( -
-

{titel}

+
+

{titel}

+ {children} +
+ ) +} + +/** Eine Karte der Einstellungsseite; KI-Box und Homelab tragen ihre Bereichsfarbe als Streifen oben. */ +function Karte({ titel, text, farbe, children }: { + titel: string + text: string + farbe?: "box" | "homelab" + children: ReactNode +}) { + const streifen = farbe === "box" ? "bg-box" : farbe === "homelab" ? "bg-lab" : "bg-linie-stark" + const kopf = farbe === "box" ? "text-box-text" : farbe === "homelab" ? "text-lab-text" : "text-foreground" + return ( +
+ +
+

{titel}

+

{text}

+
{children}
) @@ -190,7 +213,7 @@ function Meldungen() { if (i?.partner.eingerichtet) ziele.push({ ziel: andere, name: i.partner.name }) ziele.sort((a, b) => Number(a.ziel !== "box") - Number(b.ziel !== "box")) return ( - +

Dringende Hinweise gehen zusätzlich per Telegram raus, alles andere bleibt hier; was nachts anfällt, kommt um 07:00 als eine Morgenmeldung. Einfache Probleme wie einen abgestürzten Dienst behebt der Wächter selbst und @@ -363,7 +386,7 @@ function Stand({ offen }: { offen: boolean }) { const [hier, partner] = useSoftwareStand(offen, partnerDa) const gleich = gleicherStand(hier.data?.stand?.commit, partner.data?.stand?.commit) return ( - + {partnerDa && ( @@ -379,30 +402,173 @@ function Stand({ offen }: { offen: boolean }) { ) } -// --- Schublade ------------------------------------------------------------------------------------------ +// --- Homelab: Arcane und Betrieb ------------------------------------------------------------------------ -export function Einstellungen({ offen, onSchliessen }: { offen: boolean; onSchliessen: () => void }) { - const box = useEinstellungenStand(offen) +function ArcaneSchluessel({ h }: { h: HomelabEinstellungen }) { + const speichern = useArcaneSchluessel() + const echt = useArcaneEcht() + const [wert, setWert] = useState("") + const [loeschFrage, setLoeschFrage] = useState(false) + const [echtFrage, setEchtFrage] = useState(null) + const a = h.arcane + const stand: Record = { + hinterlegt: hinterlegt, + umgebung: aus der Dienst-Einstellung, + fehlt: fehlt, + } + return ( + <> + +

+ API-Schlüssel {stand[a.schluessel]} + + Arcane öffnen + +
+

+ Mit Schlüssel sieht der Orchestrator, welche Docker-Images neuer sind. Anlegen in Arcane unter Einstellungen → + API-Schlüssel; Lesen reicht, solange Docker-Updates nur als Probelauf laufen. +

+ {a.schluessel !== "umgebung" && ( +
{ + e.preventDefault() + if (wert.trim()) speichern.mutate(wert.trim(), { onSuccess: () => setWert("") }) + }} + > + + + + setWert(e.target.value)} + placeholder={a.schluessel === "hinterlegt" ? "Neuen Schlüssel eintragen …" : "Schlüssel aus Arcane einfügen …"} + className="h-11 w-full rounded-lg border border-linie-stark bg-erhaben pr-3 pl-9 text-[15px] outline-none focus:border-lab" + /> + + +
+ )} +

+ Der Schlüssel wird vor dem Speichern an Arcane geprüft und nie wieder angezeigt. Er liegt nur im Homelab-Teil. +

+ {a.schluessel === "hinterlegt" && ( + loeschFrage ? ( + + + + + ) : ( + + ) + )} +
+ +

+ {a.echt + ? "Echt: „Jetzt updaten“ bei Arcane zieht neue Images und startet die Container neu." + : "Probelauf: Arcane zeigt nur, was es tun würde — kein Container wird angefasst."} + {a.echt_quelle === "umgebung" ? " Festgelegt in der Dienst-Einstellung (MC_ARCANE_ECHT)." : ""} +

+ + {a.schluessel === "fehlt" &&

Erst den Schlüssel eintragen.

} + setEchtFrage(null)} + onJa={() => { + if (echtFrage === null) return + echt.mutate(echtFrage) + setEchtFrage(null) + }} + /> +
+ + ) +} + +function HomelabBetrieb({ h }: { h: HomelabEinstellungen }) { + const f = h.ausfuehrer + return ( + +
+
Ausführer
+
+ {f.verbunden ? "verbunden" : "meldet sich nicht"} + {f.zuletzt ? ` · zuletzt ${flugplanZeit(new Date(f.zuletzt * 1000).toISOString()).toLowerCase()}` : ""} +
+
Modus
+
{f.nur_lesen ? "nur lesen — Updates sind gesperrt" : "voll: Snapshots, Sicherungen und Updates"}
+
Sicherungen
+
{f.sicherung_speicher ? `auf „${f.sicherung_speicher}“ (wo kein Snapshot geht)` : "kein Speicher gefunden"}
+
Wartezeit
+
+ {h.karenz_h > 0 ? `${h.karenz_h} Stunden nach einer Änderung am Update-Skript einer App` : "aus"} +
+
+
+ ) +} + +function HomelabKarte() { + const instanz = useInstanz().data + const partner = usePartner().data + const aktiv = instanz?.rolle === "homelab" || !!(partner?.eingerichtet && partner.erreichbar) + const h = useHomelabEinstellungen(aktiv) + let inhalt: ReactNode + if (!aktiv) inhalt =

Der Homelab-Teil antwortet gerade nicht.

+ else if (!h.data) inhalt = + else inhalt = <> + return ( + + {inhalt} + + ) +} + +// --- Seite ---------------------------------------------------------------------------------------------- + +export function EinstellungenSeite() { + const box = useEinstellungenStand(true) const boxProps = { e: box.data, laedt: box.isPending, fehler: box.error?.message } return ( - !o && onSchliessen()}> - - - Einstellungen - Was die Box sich merkt und wann sie was tut. - - - +
+ - - + + Hermes-Dashboard öffnen

Chat, Sitzungen und Cron-Jobs verwaltet Hermes selbst. Es fragt nach seiner eigenen Anmeldung.

- - +
+ + + + + + + +
) } diff --git a/frontend/src/views/Homelab.tsx b/frontend/src/views/Homelab.tsx index f7f10ff..afdf094 100644 --- a/frontend/src/views/Homelab.tsx +++ b/frontend/src/views/Homelab.tsx @@ -1,18 +1,22 @@ -import { useNavigate } from "@tanstack/react-router" +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 { GeraeteListe } from "@/components/homelab/GeraeteListe" +import { Gesundheit } from "@/components/homelab/Gesundheit" import { Lagebild } from "@/components/homelab/Lagebild" -import { UpdateListe } from "@/components/homelab/UpdateListe" +import { AlleKnopf, SammellaufFortschritt } from "@/components/homelab/Sammellauf" +import { UpdateTabelle } from "@/components/homelab/UpdateTabelle" import { - useHomelab, useHomelabHinweise, useHomelabLaeufe, useInstanz, usePartner, useZielAktion, + useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useInstanz, usePartner, useSammellauf, + useZielAktion, } from "@/lib/abfragen" import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab" import type { Hinweis, HomelabLauf, HomelabStand, PartnerStatus } from "@/lib/typen" import { flugplanZeit } from "@/lib/zeit" -// Der Bereich Homelab hat zwei Seiten wie die KI-Box: das Cockpit (Lage, Hinweise, Geräte) und die -// Updates (was ansteht, was gelaufen ist). Aus der KI-Box steht hier nichts — die hat ihren eigenen Bereich. +// 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. 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"] @@ -178,10 +182,11 @@ function useHomelabBereich() { } } -/** Homelab · Cockpit: die Lage auf einen Blick, Hinweise und alle Geräte. */ +/** 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, hinweise, aktion } = useHomelabBereich() + const { ersatz, stand, laufend, hinweise } = useHomelabBereich() + const mess = useHomelabMesswerte("1h", !!stand) if (ersatz || !stand) return ersatz const berichtZeit = stand.bericht ? zeitAus(stand.bericht.empfangen) : null @@ -201,31 +206,50 @@ export function HomelabSeite() { Stand {berichtZeit} : undefined} + rechts={ + + {berichtZeit && Stand {berichtZeit}} + + Monitoring + + + } > - aktion.mutate(a)} /> + + {mess.isError && ( +

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

+ )}
) } -/** Homelab · Updates: was ansteht — ein Knopf je Update — und was gelaufen ist. */ +/** 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) 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 ( <> - + } + >

- Du startest jedes Update selbst. Vorher wird gesichert, danach geprüft; ist die Prüfung rot, geht es von - selbst zurück. + 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.

- } + aktion.mutate(a)} />
diff --git a/frontend/src/views/HomelabProtokoll.tsx b/frontend/src/views/HomelabProtokoll.tsx new file mode 100644 index 0000000..4ff68df --- /dev/null +++ b/frontend/src/views/HomelabProtokoll.tsx @@ -0,0 +1,151 @@ +import { useState } from "react" +import { Bell, ChevronRight, Cog, RefreshCw, Search, TriangleAlert, type LucideIcon } from "lucide-react" +import { cn } from "cn" +import { Zustandsfeld } from "@/components/cockpit/Chip" +import { Panel } from "@/components/cockpit/Panel" +import { useHomelab, useHomelabProtokoll, useInstanz, usePartner } from "@/lib/abfragen" +import type { ProtokollEintrag } from "@/lib/typen" + +// Was der Homelab-Teil getan hat: Aufträge an den Ausführer (Snapshot, Update, Suchen …), Update-Läufe, +// Hinweise des Wächters, gesendete Meldungen und die nächtliche Pflege — neueste zuerst, nach Tagen. + +type Art = ProtokollEintrag["art"] + +const ARTEN: { id: Art | "alle"; name: string }[] = [ + { id: "alle", name: "Alles" }, + { id: "lauf", name: "Updates" }, + { id: "auftrag", name: "Ausführer" }, + { id: "hinweis", name: "Hinweise" }, + { id: "meldung", name: "Meldungen" }, + { id: "pflege", name: "Pflege" }, +] + +const SYMBOL: Record = { + auftrag: Cog, lauf: RefreshCw, sammellauf: RefreshCw, hinweis: TriangleAlert, meldung: Bell, pflege: Search, +} + +const STUFE: Record = { + ok: { punkt: "bg-gruen", text: "text-gruen-text", wort: "ok" }, + info: { punkt: "bg-cyan", text: "text-cyan-text", wort: "Info" }, + warn: { punkt: "bg-bernstein", text: "text-bernstein", wort: "Warnung" }, + fehler: { punkt: "bg-rot", text: "text-rot-text", wort: "Fehler" }, +} + +const tagName = new Intl.DateTimeFormat("de-DE", { weekday: "long", day: "2-digit", month: "2-digit", timeZone: "Europe/Berlin" }) +const uhr = new Intl.DateTimeFormat("de-DE", { hour: "2-digit", minute: "2-digit", second: "2-digit", timeZone: "Europe/Berlin" }) +const tagSchluessel = new Intl.DateTimeFormat("sv-SE", { timeZone: "Europe/Berlin" }) + +function tagUeberschrift(zeit: number): string { + const heute = tagSchluessel.format(new Date()) + const gestern = tagSchluessel.format(new Date(Date.now() - 86_400_000)) + const t = tagSchluessel.format(new Date(zeit * 1000)) + if (t === heute) return "Heute" + if (t === gestern) return "Gestern" + return tagName.format(new Date(zeit * 1000)) +} + +function Eintrag({ e, geraet }: { e: ProtokollEintrag; geraet: string | null }) { + const [offen, setOffen] = useState(false) + const s = STUFE[e.stufe] ?? STUFE.info + const Symbol = SYMBOL[e.art] ?? Cog + return ( +
  • + {uhr.format(new Date(e.zeit * 1000))} + + + +
    + + + {e.titel} + {geraet && {geraet}} + ({s.wort}) + + {e.text && {e.text}} + {e.details && ( + <> + + {offen && ( +
    +                {e.details}
    +              
    + )} + + )} +
    +
  • + ) +} + +export function HomelabProtokollSeite() { + const instanz = useInstanz() + const partner = usePartner() + const aktiv = instanz.data?.rolle === "homelab" || !!(partner.data?.eingerichtet && partner.data.erreichbar) + const [art, setArt] = useState("alle") + const [berichte, setBerichte] = useState(false) + const protokoll = useHomelabProtokoll(aktiv, berichte) + const namen = new Map((useHomelab(aktiv).data?.ziele ?? []).map((z) => [z.id, z.name])) + + if (instanz.isPending || partner.isPending) return + if (!aktiv) return + + const alle = protokoll.data?.eintraege ?? [] + const passend = alle.filter((e) => art === "alle" || e.art === art || (art === "lauf" && e.art === "sammellauf")) + // Nach Tagen gruppieren (die Liste kommt neueste zuerst). + const tage: { titel: string; eintraege: ProtokollEintrag[] }[] = [] + for (const e of passend) { + const titel = tagUeberschrift(e.zeit) + if (tage.at(-1)?.titel !== titel) tage.push({ titel, eintraege: [] }) + tage.at(-1)!.eintraege.push(e) + } + + return ( + +

    + Was der Homelab-Teil getan hat: Aufträge an den Ausführer auf dem Proxmox-Host, Update-Läufe, Hinweise, + gesendete Meldungen und die nächtliche Paketsuche. +

    +
    + {ARTEN.map((a) => ( + + ))} + +
    + {protokoll.isPending ? ( +

    Wird gelesen …

    + ) : protokoll.isError ? ( +

    Das Protokoll lässt sich nicht lesen: {protokoll.error.message}

    + ) : tage.length === 0 ? ( +

    Dazu gibt es noch keine Einträge.

    + ) : ( +
    + {tage.map((t) => ( +
    +

    {t.titel}

    +
      + {t.eintraege.map((e, i) => { + // Das Gerät als Schild — außer, der Titel nennt es schon. + const name = e.ziel ? namen.get(e.ziel) ?? e.ziel : null + return + })} +
    +
    + ))} +
    + )} +
    + ) +} diff --git a/frontend/src/views/Monitoring.tsx b/frontend/src/views/Monitoring.tsx new file mode 100644 index 0000000..48df6de --- /dev/null +++ b/frontend/src/views/Monitoring.tsx @@ -0,0 +1,163 @@ +import { type ReactNode, useEffect, useState } from "react" +import { useRouterState } from "@tanstack/react-router" +import { cn } from "cn" +import { Zustandsfeld } from "@/components/cockpit/Chip" +import { Panel } from "@/components/cockpit/Panel" +import { Kennzahl } from "@/components/diagramme/Kennzahl" +import { Verlauf } from "@/components/diagramme/Verlauf" +import { useBoxMesswerte, useHomelabMesswerte, useInstanz, usePartner } from "@/lib/abfragen" +import { laufzeit, letzterWert, zustandBei } from "@/lib/messung" +import { useLetzteMetrik } from "@/lib/strom" +import type { GeraetMesswerte, Zeitraum } from "@/lib/typen" + +// Monitoring je Bereich: oben der Zeitraum (gilt für alles darunter), dann die Kennzahlen, dann die Verläufe. +// Die Werte kommen jede Minute (KI-Box: der Steward; Homelab: der Ausführer auf dem Proxmox-Host). + +const ZEITRAEUME: { id: Zeitraum; name: string; text: string }[] = [ + { id: "1h", name: "1 Stunde", text: "letzte Stunde" }, + { id: "24h", name: "24 Stunden", text: "letzte 24 Stunden" }, + { id: "7d", name: "7 Tage", text: "letzte 7 Tage" }, +] + +function ZeitraumWahl({ wert, onWahl, farbe }: { wert: Zeitraum; onWahl: (z: Zeitraum) => void; farbe: "box" | "homelab" }) { + const aktiv = farbe === "box" ? "border-box-rand bg-box-grund text-box-text" : "border-lab-rand bg-lab-grund text-lab-text" + return ( +
    + {ZEITRAEUME.map((z) => ( + + ))} +
    + ) +} + +function Diagrammkarte({ titel, rechts, children }: { titel: string; rechts?: ReactNode; children: ReactNode }) { + return ( +
    +
    +

    {titel}

    + {rechts} +
    + {children} +
    + ) +} + +const zeitraumText = (z: Zeitraum) => ZEITRAEUME.find((x) => x.id === z)?.text ?? "" + +// --- KI-Box ------------------------------------------------------------------------------------------- + +export function BoxMonitoringSeite() { + const [zeitraum, setZeitraum] = useState("24h") + const mess = useBoxMesswerte(zeitraum) + const live = useLetzteMetrik() + const r = mess.data?.reihen ?? {} + const a = mess.data?.aktuell ?? {} + const lang = zeitraum === "7d" + const wert = (live: number | null | undefined, reihe: keyof typeof r) => live ?? a[reihe] ?? letzterWert(r[reihe]) + return ( + <> +
    + + Zahlen live, Verläufe im Minutentakt +
    +
    + + + + + + +
    + {mess.isError && !mess.data ? ( + + ) : ( +
    + + + + + + +
    + )} + + ) +} + +// --- Homelab ------------------------------------------------------------------------------------------ + +function GastKarte({ g, zeitraum }: { g: GeraetMesswerte; zeitraum: Zeitraum }) { + const a = g.aktuell + return ( +
    +
    +

    {g.name}

    + + CPU {a.cpu != null ? `${Math.round(a.cpu)} %` : "–"} · RAM {a.ram != null ? `${Math.round(a.ram)} %` : "–"} · läuft {laufzeit(a.uptime_s)} + +
    + +
    + ) +} + +export function HomelabMonitoringSeite() { + const [zeitraum, setZeitraum] = useState("24h") + const instanz = useInstanz() + const partner = usePartner() + const aktiv = instanz.data?.rolle === "homelab" || !!(partner.data?.eingerichtet && partner.data.erreichbar) + const mess = useHomelabMesswerte(zeitraum, aktiv) + const hash = useRouterState({ select: (s) => s.location.hash }) + // Aus dem Cockpit kommt man mit #ct-104 — dann zu diesem Gerät springen, sobald es gezeichnet ist. + useEffect(() => { + if (hash && mess.data) document.getElementById(hash)?.scrollIntoView({ block: "start" }) + }, [hash, mess.data]) + + if (!aktiv) return + if (mess.isPending) return + if (!mess.data) { + return + } + const host = mess.data.geraete.find((g) => g.art === "proxmox-host") + const gaeste = mess.data.geraete.filter((g) => g.art !== "proxmox-host") + const lang = zeitraum === "7d" + const h = host?.aktuell ?? {} + return ( + <> +
    + + Werte im Minutentakt vom Proxmox-Host +
    + {host && ( + läuft {laufzeit(h.uptime_s)}{h.load1 != null ? ` · Last ${h.load1.toFixed(2).replace(".", ",")}` : ""}}> +
    + + + + +
    +
    + + + {(host.reihen.temp_cpu?.some(([, v]) => v !== null) ?? false) && ( + + )} +
    +
    + )} +
    + {gaeste.map((g) => )} +
    + + ) +} diff --git a/frontend/src/views/Start.tsx b/frontend/src/views/Start.tsx index 320d3a9..b179a9b 100644 --- a/frontend/src/views/Start.tsx +++ b/frontend/src/views/Start.tsx @@ -81,7 +81,7 @@ function RadarKasten({ radar }: { radar: Radar | undefined }) { )} ) diff --git a/frontend/src/views/Uebersicht.tsx b/frontend/src/views/Uebersicht.tsx new file mode 100644 index 0000000..1a784b2 --- /dev/null +++ b/frontend/src/views/Uebersicht.tsx @@ -0,0 +1,324 @@ +import type { ReactNode } from "react" +import { Link } from "@tanstack/react-router" +import { ArrowRight, CircleAlert, CircleCheck, RefreshCw } from "lucide-react" +import { cn } from "cn" +import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip" +import { Flugplan } from "@/components/cockpit/Flugplan" +import { Panel } from "@/components/cockpit/Panel" +import { Kennzahl } from "@/components/diagramme/Kennzahl" +import { + useBoxMesswerte, useBoxZiele, useHomelab, useHomelabHinweise, useHomelabLaeufe, useHomelabMesswerte, useStart, + useUpdateVerlauf, +} from "@/lib/abfragen" +import { useBereichStand, type Verbindung } from "@/lib/navigation" +import { useLetzteMetrik } from "@/lib/strom" +import type { Hinweis, Messpunkt } from "@/lib/typen" +import { gesamtlage, type KurzZeile, kurzformen, type Lagestufe } from "@/lib/uebersicht" +import { zustandBei } from "@/lib/messung" +import { flugplanZeit } from "@/lib/zeit" + +// Die Startseite: alles auf einen Blick. Oben die Gesamtlage, darunter je Bereich eine Karte (Messwerte, +// wer welche Updates hat, Hinweise), unten, was als Nächstes läuft und was zuletzt passiert ist. Jeder Block +// führt mit einem Klick in seinen Bereich. + +const LAGE: Record = { + ok: { rahmen: "border-gruen-rand bg-gruen-grund", titel: "text-gruen", Symbol: CircleCheck }, + info: { rahmen: "border-cyan-rand bg-cyan-grund", titel: "text-cyan", Symbol: RefreshCw }, + gelb: { rahmen: "border-bernstein/70 bg-bernstein-grund", titel: "text-bernstein", Symbol: CircleAlert }, + rot: { rahmen: "border-rot bg-rot-grund", titel: "text-rot", Symbol: CircleAlert }, +} + +function Zahlkachel({ label, wert, zusatz, art }: { label: string; wert: string; zusatz?: string; art: ChipArt }) { + const farbe: Record = { + gruen: "text-gruen", bernstein: "text-bernstein", cyan: "text-cyan", rot: "text-rot-text", grau: "text-foreground", + } + return ( +
    + {label} + {wert} + {zusatz && {zusatz}} +
    + ) +} + +const PUNKT: Record = { + online: "bg-gruen shadow-[0_0_8px_rgba(61,220,138,0.7)]", + verbinde: "bg-bernstein", + getrennt: "bg-rot", + fehlt: "bg-text-3", +} +const VERBINDUNG_TEXT: Record = { + online: "online", verbinde: "verbinde", getrennt: "getrennt", fehlt: "nicht eingerichtet", +} + +/** Eine Bereichskarte: farbiger Kopf mit Weg ins Cockpit, darunter Messwerte, Updates und Hinweise. */ +function Bereichskarte({ bereich, name, verbindung, cockpit, children }: { + bereich: "box" | "homelab" + name: string + verbindung: Verbindung + cockpit: "/box" | "/homelab" + children: ReactNode +}) { + const farbe = bereich === "box" + ? { rand: "border-box-rand", kopf: "from-box-grund", text: "text-box-text", balken: "bg-box" } + : { rand: "border-lab-rand", kopf: "from-lab-grund", text: "text-lab-text", balken: "bg-lab" } + return ( +
    + + + + + {name} + + + {VERBINDUNG_TEXT[verbindung]} + + + + + Cockpit + + +
    {children}
    +
    + ) +} + +function Unterkopf({ titel, rechts }: { titel: string; rechts?: ReactNode }) { + return ( +
    +

    {titel}

    + {rechts} +
    + ) +} + +/** Wer hat welche Updates — eine Zeile je Gerät, Klick führt zur Updates-Seite des Bereichs. */ +function UpdateKurzliste({ zeilen, ziel, laedt, alleAktuell }: { + zeilen: KurzZeile[] + ziel: "/box/updates" | "/homelab/updates" + laedt: boolean + alleAktuell: string +}) { + const knopf = zeilen.filter((z) => z.knopf).length + return ( +
    + 0 ? {knopf} bereit : zeilen.length > 0 ? wartet : aktuell} + /> + {laedt ? ( +

    Wird gelesen …

    + ) : zeilen.length === 0 ? ( +

    {alleAktuell}

    + ) : ( +
      + {zeilen.map((z) => ( +
    • + + {z.name} + {z.teile.join(" · ")} + + +
    • + ))} +
    + )} +
    + ) +} + +function Hinweisliste({ hinweise, ziel }: { hinweise: Hinweis[]; ziel: "/box" | "/homelab" }) { + if (hinweise.length === 0) return null + const sortiert = [...hinweise].sort((a, b) => Number(b.stufe === "rot") - Number(a.stufe === "rot")) + return ( +
    + h.stufe === "rot") ? "rot" : "bernstein"}>{hinweise.length}} + /> +
      + {sortiert.slice(0, 3).map((h) => ( +
    • + + + {h.titel} + +
    • + ))} +
    +
    + ) +} + +/** Vier Messwerte mit der Linie der letzten Stunde. Ohne Verlauf (ältere Box) nur die Zahlen. */ +function Messkacheln({ werte }: { + werte: { label: string; wert: number | null | undefined; einheit: "prozent" | "grad"; punkte?: Messpunkt[]; gelb: number; rot: number }[] +}) { + return ( +
    + {werte.map((w) => ( + + ))} +
    + ) +} + +function KiBoxKarte({ verbindung }: { verbindung: Verbindung }) { + const start = useStart() + const ziele = useBoxZiele() + const mess = useBoxMesswerte("1h") + const live = useLetzteMetrik() + const r = mess.data?.reihen + const b = start.data?.box + const ramProzent = live?.ram ?? (b?.ram_used && b.ram_total ? (b.ram_used / b.ram_total) * 100 : null) + return ( + + + + + + ) +} + +function HomelabKarte({ verbindung }: { verbindung: Verbindung }) { + const aktiv = verbindung === "online" + const homelab = useHomelab(aktiv) + const hinweise = useHomelabHinweise(aktiv) + const mess = useHomelabMesswerte("1h", aktiv) + const host = mess.data?.geraete.find((g) => g.art === "proxmox-host") + const ziele = homelab.data?.ziele ?? [] + return ( + + {!aktiv ? ( +

    Der Homelab-Teil antwortet gerade nicht; die Karte zeigt nichts Veraltetes.

    + ) : ( + <> +
    + + +
    + + + + )} +
    + ) +} + +const ERGEBNIS: Record = { + eingespielt: "gruen", gruen: "gruen", zurueckgerollt: "bernstein", gelb: "bernstein", fehler: "rot", rot: "rot", + neustart: "cyan", info: "cyan", +} + +/** Was zuletzt eingespielt wurde, aus beiden Bereichen, neueste zuerst. */ +function Zuletzt({ homelabAktiv }: { homelabAktiv: boolean }) { + const box = useUpdateVerlauf() + const lab = useHomelabLaeufe(homelabAktiv) + const eintraege = [ + ...(box.data?.laeufe ?? []).slice(0, 6).map((l) => ({ + zeit: new Date(l.start).getTime(), bereich: "KI-Box", titel: l.anlass, chip: l.titel, art: ERGEBNIS[l.stufe] ?? "grau", + })), + ...(lab.data?.laeufe ?? []).filter((l) => l.status !== "laeuft").slice(0, 6).map((l) => ({ + zeit: l.start * 1000, bereich: "Homelab", titel: l.name, chip: l.ergebnis ?? "unterbrochen", + art: (l.ergebnis ? ERGEBNIS[l.ergebnis] : "grau") ?? "grau", + })), + ].sort((a, b) => b.zeit - a.zeit).slice(0, 6) + if (eintraege.length === 0) return

    Noch keine Updates aufgezeichnet.

    + return ( +
      + {eintraege.map((e) => ( +
    1. + {flugplanZeit(new Date(e.zeit).toISOString())} + + {e.bereich} + {e.titel} + + {e.chip} +
    2. + ))} +
    + ) +} + +export function UebersichtSeite() { + const stand = useBereichStand() + const start = useStart() + const homelabAktiv = stand.homelab.verbindung === "online" + const homelab = useHomelab(homelabAktiv) + const labHinweise = useHomelabHinweise(homelabAktiv) + + if (start.isPending && !start.data) return + + // Weg ist die KI-Box erst, wenn auch ihre Schnittstelle nicht antwortet — ein abgerissener Live-Strom allein + // (Neustart, Standby des Browsers) ist kein Alarm. + const getrennt = [ + ...(stand.box.verbindung === "getrennt" && start.isError ? ["Die KI-Box"] : []), + ...(stand.homelab.verbindung === "getrennt" ? ["Der Homelab-Teil"] : []), + ] + const stumm = (homelab.data?.ziele ?? []).filter((z) => z.erreichbar === false).map((z) => z.name) + const alleHinweise = [...(start.data?.hinweise ?? []), ...(labHinweise.data?.hinweise ?? [])] + const updates = stand.box.updates + stand.homelab.updates + const lage = gesamtlage({ + getrennt, stumm, hinweiseRot: alleHinweise.filter((h) => h.stufe === "rot").length, hinweise: alleHinweise.length, updates, + }) + const l = LAGE[lage.stufe] + const geraete = homelab.data?.ziele ?? [] + const online = geraete.filter((z) => z.erreichbar !== false).length + (stand.box.verbindung === "online" ? 1 : 0) + const gesamt = geraete.length + 1 + const naechster = start.data?.flugplan.geplant[0] + + return ( + <> +
    +
    + +
    + {lage.titel} + {lage.text} +
    +
    +
    + + h.stufe === "rot") ? "rot" : alleHinweise.length ? "bernstein" : "gruen"} /> + + +
    +
    + +
    + + +
    + +
    + + + + + + +
    + + ) +}