homelab: Seite neu geordnet — Lage auf einen Blick, Updates, Geraete, Verlauf getrennt
Vorher stand alles auf Geraetekarten durcheinander (KI-Box zwischen den Containern, Paketstand, App-Version, Knoepfe und Rueckweg je Karte). Jetzt: oben die Lage wie das Warnpanel der Startseite (grosse Leuchte + eine Lampe je Geraet), darunter nur die offenen Updates als Tabelle mit Rueckweg und Knopf, dann die Geraeteliste (App, Pakete, Netz) und der Verlauf. Die KI-Box steht nur noch als Hinweis auf ihre eigene Updates-Seite. Solange der Homelab-Teil laedt, sagt die Seite nicht mehr voreilig "aktuell". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
40d2840b16
commit
a32f55645c
+102
-60
@@ -1,10 +1,15 @@
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { ZielKarte } from "@/components/cockpit/ZielKarte"
|
||||
import { GeraeteListe } from "@/components/homelab/GeraeteListe"
|
||||
import { Lagebild } from "@/components/homelab/Lagebild"
|
||||
import { UpdateListe } from "@/components/homelab/UpdateListe"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
useBoxZiele, useHomelab, useHomelabHinweise, useHomelabLaeufe, useInstanz, usePartner, useZielAktion,
|
||||
} from "@/lib/abfragen"
|
||||
import type { Hinweis, HomelabLauf, PartnerStatus, ZielStand } from "@/lib/typen"
|
||||
import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab"
|
||||
import type { Hinweis, HomelabLauf, PartnerStatus } from "@/lib/typen"
|
||||
import { flugplanZeit } from "@/lib/zeit"
|
||||
|
||||
/** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026). */
|
||||
@@ -17,6 +22,8 @@ const ERGEBNIS: Record<string, { art: ChipArt; text: string }> = {
|
||||
neustart: { art: "cyan", text: "Neustart" },
|
||||
}
|
||||
|
||||
const zeitAus = (sekunden: number) => flugplanZeit(new Date(sekunden * 1000).toISOString())
|
||||
|
||||
function NochNichtEingerichtet() {
|
||||
return (
|
||||
<>
|
||||
@@ -62,14 +69,14 @@ function HomelabFehlt({ partner }: { partner: PartnerStatus }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Hinweise des Homelab-Wächters (die der KI-Box stehen auf dem Start). Rot zuerst. */
|
||||
function HomelabHinweise({ hinweise }: { hinweise: Hinweis[] }) {
|
||||
if (hinweise.length === 0) return null
|
||||
/** Hinweise des Homelab-Wächters (die der KI-Box stehen auf dem Start). */
|
||||
function Hinweise({ hinweise }: { hinweise: Hinweis[] }) {
|
||||
const rot = hinweise.some((h) => h.stufe === "rot")
|
||||
return (
|
||||
<Panel titel="Hinweise im Homelab" rechts={<Chip art={hinweise.some((h) => h.stufe === "rot") ? "rot" : "bernstein"}>{hinweise.length}</Chip>}>
|
||||
<ul className="flex flex-col gap-3">
|
||||
<Panel titel="Hinweise" id="hinweise" rechts={<Chip art={rot ? "rot" : "bernstein"}>{hinweise.length} offen</Chip>}>
|
||||
<ul className="m-0 flex list-none flex-col p-0">
|
||||
{hinweise.map((h) => (
|
||||
<li key={h.id} className="flex flex-col gap-1 border-t border-linie pt-3 first:border-t-0 first:pt-0">
|
||||
<li key={h.id} className="flex flex-col gap-1 border-t border-linie py-3 first:border-t-0 first:pt-0">
|
||||
<span className={h.stufe === "rot" ? "text-[15px] font-medium text-rot-text" : "text-[15px] font-medium text-bernstein"}>
|
||||
{h.titel}
|
||||
</span>
|
||||
@@ -81,29 +88,46 @@ function HomelabHinweise({ hinweise }: { hinweise: Hinweis[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
function LetzteLaeufe({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 6)
|
||||
if (fertig.length === 0) return null
|
||||
/** Die KI-Box hat ihre eigene Updates-Seite; hier steht nur, dass dort etwas wartet. */
|
||||
function BoxHinweis({ namen }: { namen: string[] }) {
|
||||
if (namen.length === 0) return null
|
||||
return (
|
||||
<Panel titel="Letzte Updates im Homelab">
|
||||
<ul className="flex flex-col gap-3">
|
||||
{fertig.map((l) => {
|
||||
const e = l.ergebnis ? ERGEBNIS[l.ergebnis] : { art: "grau" as ChipArt, text: "unterbrochen" }
|
||||
return (
|
||||
<li key={l.id} className="flex flex-col gap-1 border-t border-linie pt-3 first:border-t-0 first:pt-0">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<span className="text-[15px] font-medium">{l.name}</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="ziffern text-sm text-text-3">{flugplanZeit(new Date(l.start * 1000).toISOString())}</span>
|
||||
<Chip art={e.art}>{e.text}</Chip>
|
||||
</span>
|
||||
</div>
|
||||
{l.text && <p className="text-sm text-text-2">{l.text}</p>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</Panel>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-cyan-rand bg-cyan-grund px-4 py-3.5">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<span className="schild text-sm text-cyan">KI-Box · {namen.length} {namen.length === 1 ? "Update" : "Updates"}</span>
|
||||
<span className="text-[15px] text-text-2">{namen.join(", ")} — eingespielt wird auf der Updates-Seite der Box.</span>
|
||||
</div>
|
||||
<Button variant="info" size="sm" asChild>
|
||||
<Link to="/updates">Ansehen</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Die letzten Läufe — gebaut wie der Verlauf der Updates-Seite: Zeit, Gerät, Ergebnis, darunter der Satz. */
|
||||
function Verlauf({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 8)
|
||||
if (fertig.length === 0) return <p className="text-[15px] text-text-2">Noch kein Update im Homelab eingespielt.</p>
|
||||
return (
|
||||
<ol className="@container m-0 flex list-none flex-col p-0">
|
||||
{fertig.map((l) => {
|
||||
const e = l.ergebnis ? ERGEBNIS[l.ergebnis] : { art: "grau" as ChipArt, text: "unterbrochen" }
|
||||
return (
|
||||
<li key={l.id} className="flex flex-col gap-1.5 border-t border-linie py-3 first:border-t-0 first:pt-0">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_auto] items-center gap-x-3 gap-y-1 @sm:grid-cols-[112px_minmax(0,1fr)_auto]">
|
||||
<span className="ziffern col-start-1 row-start-1 text-sm text-text-3">{zeitAus(l.start)}</span>
|
||||
<span className="col-span-2 row-start-2 min-w-0 text-[15px] break-words @sm:col-span-1 @sm:col-start-2 @sm:row-start-1">
|
||||
{l.name}
|
||||
</span>
|
||||
<span className="col-start-2 row-start-1 justify-self-end @sm:col-start-3">
|
||||
<Chip art={e.art}>{e.text}</Chip>
|
||||
</span>
|
||||
</div>
|
||||
{l.text && <p className="m-0 text-sm text-text-2 @sm:pl-[124px]">{l.text}</p>}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -119,53 +143,71 @@ export function HomelabSeite() {
|
||||
const aktion = useZielAktion()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
if (!aktiv) {
|
||||
return partner.data
|
||||
? <HomelabFehlt partner={partner.data} />
|
||||
: <Zustandsfeld fehler text={`Der Partner lässt sich nicht abfragen: ${partner.error?.message ?? "keine Daten"}`} />
|
||||
}
|
||||
// Solange der erste Bericht unterwegs ist, weiß die Seite nichts — also auch nicht „aktuell“ sagen.
|
||||
if (homelab.isPending) return <Zustandsfeld text="Der Homelab-Teil meldet sich gleich …" />
|
||||
if (!homelab.data) return <Zustandsfeld fehler text={`Der Homelab-Teil antwortet nicht: ${homelab.error?.message ?? "keine Daten"}`} />
|
||||
|
||||
// Laufende Updates je Gerät: der letzte Schritt steht auf der Karte.
|
||||
const stand = homelab.data
|
||||
const laufend = new Map<string, string>()
|
||||
for (const l of laeufe.data?.laeufe ?? []) {
|
||||
if (l.status === "laeuft") laufend.set(l.ziel, l.schritte.at(-1) ?? "startet …")
|
||||
}
|
||||
const ziele: ZielStand[] = [...(box.data?.ziele ?? []), ...(homelab.data?.ziele ?? [])]
|
||||
const offen = ziele.reduce((n, z) => n + z.offen, 0)
|
||||
const bericht = homelab.data?.bericht
|
||||
const offeneHinweise = hinweise.data?.hinweise ?? []
|
||||
const berichtZeit = stand.bericht ? zeitAus(stand.bericht.empfangen) : null
|
||||
const l = lage(stand, offeneHinweise, laufend.size, berichtZeit)
|
||||
const boxOffen = (box.data?.ziele ?? []).flatMap((z) => z.bausteine.filter((b) => b.zustand === "neu").map((b) => b.name))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel
|
||||
titel="Alle Geräte"
|
||||
rechts={offen > 0 ? <Chip art="cyan">{offen} {offen === 1 ? "Update" : "Updates"} bereit</Chip> : <Chip art="gruen">aktuell</Chip>}
|
||||
>
|
||||
<p className="text-[15px] text-text-2">
|
||||
Die KI-Box und das Homelab an einem Ort. Updates startest du nur hier per Knopf; jedes fragt vorher und sagt,
|
||||
welcher Rückweg bereitsteht.
|
||||
<Lagebild
|
||||
lage={l}
|
||||
lampen={stand.ziele.map((z) => lampeFuer(z, laufend.has(z.id)))}
|
||||
onAnsehen={() => l.ziel && document.getElementById(l.ziel)?.scrollIntoView({ behavior: "smooth", block: "start" })}
|
||||
/>
|
||||
|
||||
{offeneHinweise.length > 0 && <Hinweise hinweise={offeneHinweise} />}
|
||||
|
||||
<Panel titel="Updates" id="updates">
|
||||
<p className="-mt-2 text-[15px] text-text-2">
|
||||
Du startest jedes Update selbst. Vorher wird gesichert, danach geprüft; ist die Prüfung rot, geht es von
|
||||
selbst zurück.
|
||||
</p>
|
||||
{aktiv && homelab.data && !homelab.data.ausfuehrer.verbunden && (
|
||||
{!stand.ausfuehrer.verbunden && (
|
||||
<p role="alert" className="text-[15px] text-bernstein">
|
||||
Der Ausführer auf dem Proxmox-Host meldet sich nicht
|
||||
{homelab.data.ausfuehrer.zuletzt ? ` (zuletzt ${flugplanZeit(new Date(homelab.data.ausfuehrer.zuletzt * 1000).toISOString())})` : ""}.
|
||||
Die Homelab-Geräte zeigen den letzten bekannten Stand.
|
||||
{stand.ausfuehrer.zuletzt ? ` (zuletzt ${zeitAus(stand.ausfuehrer.zuletzt)})` : ""}. Updates starten erst, wenn
|
||||
er wieder da ist.
|
||||
</p>
|
||||
)}
|
||||
{aktiv && homelab.data && !bericht && (
|
||||
<p className="text-[15px] text-text-2">Der Ausführer auf dem Proxmox-Host hat noch keinen Bericht geschickt.</p>
|
||||
{stand.bericht?.veraltet && (
|
||||
<p className="text-[15px] text-bernstein">Der letzte Bericht ist von {berichtZeit}; die Liste kann veraltet sein.</p>
|
||||
)}
|
||||
{box.isError && <p role="alert" className="text-sm text-rot-text">Die KI-Box antwortet gerade nicht: {box.error.message}</p>}
|
||||
{homelab.isError && <p role="alert" className="text-sm text-rot-text">Der Homelab-Teil antwortet gerade nicht: {homelab.error.message}</p>}
|
||||
{!stand.bericht && <p className="text-[15px] text-text-2">Der Ausführer hat noch keinen Bericht geschickt.</p>}
|
||||
<UpdateListe
|
||||
updates={offeneUpdates(stand.ziele)}
|
||||
laufend={laufend}
|
||||
gesperrt={aktion.isPending}
|
||||
onAktion={(a) => aktion.mutate(a)}
|
||||
/>
|
||||
<BoxHinweis namen={boxOffen} />
|
||||
</Panel>
|
||||
|
||||
{aktiv && <HomelabHinweise hinweise={hinweise.data?.hinweise ?? []} />}
|
||||
<Panel
|
||||
titel="Geräte"
|
||||
id="geraete"
|
||||
rechts={berichtZeit ? <span className="ziffern text-sm text-text-3">Stand {berichtZeit}</span> : undefined}
|
||||
>
|
||||
<GeraeteListe ziele={stand.ziele} laufend={laufend} gesperrt={aktion.isPending} onAktion={(a) => aktion.mutate(a)} />
|
||||
</Panel>
|
||||
|
||||
{ziele.length > 0 && (
|
||||
<div className="grid gap-5 md:gap-6 xl:grid-cols-2">
|
||||
{ziele.map((z) => (
|
||||
<ZielKarte key={z.id} ziel={z} laeuft={laufend.get(z.id) ?? null} gesperrt={aktion.isPending}
|
||||
onAktion={(a) => aktion.mutate(a)} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!aktiv && partner.data && <HomelabFehlt partner={partner.data} />}
|
||||
{aktiv && <LetzteLaeufe laeufe={laeufe.data?.laeufe ?? []} />}
|
||||
<Panel titel="Verlauf">
|
||||
<Verlauf laeufe={laeufe.data?.laeufe ?? []} />
|
||||
</Panel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user