oberflaeche: Seitenleiste mit zwei getrennten Bereichen KI-Box und Homelab
Statt der flachen Leiste oben (Start, Updates, Modelle, Homelab) steht links die Menuefuehrung: je Bereich ein Block mit eigener Verbindungsanzeige und seinen Seiten, Updates mit Zahl. Das Homelab bekommt wie die KI-Box ein Cockpit (Lage, Hinweise, Geraete) und eine Updates-Seite (/homelab/updates, offene Updates und Verlauf); der Hinweis auf Box-Updates im Homelab faellt weg. Am Handy dieselbe Leiste als Schublade hinter dem Menue-Knopf, oben Bereich und Seitentitel; die Leiste unten und das Mehr-Menue entfallen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a32f55645c
commit
6afec70640
@@ -1,17 +1,19 @@
|
||||
import { Link } from "@tanstack/react-router"
|
||||
import { useNavigate } from "@tanstack/react-router"
|
||||
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
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,
|
||||
useHomelab, useHomelabHinweise, useHomelabLaeufe, useInstanz, usePartner, useZielAktion,
|
||||
} from "@/lib/abfragen"
|
||||
import { lage, lampeFuer, offeneUpdates } from "@/lib/homelab"
|
||||
import type { Hinweis, HomelabLauf, PartnerStatus } from "@/lib/typen"
|
||||
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.
|
||||
|
||||
/** 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"]
|
||||
|
||||
@@ -69,7 +71,7 @@ function HomelabFehlt({ partner }: { partner: PartnerStatus }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** Hinweise des Homelab-Wächters (die der KI-Box stehen auf dem Start). */
|
||||
/** Hinweise des Homelab-Wächters (die der KI-Box stehen in deren Cockpit). */
|
||||
function Hinweise({ hinweise }: { hinweise: Hinweis[] }) {
|
||||
const rot = hinweise.some((h) => h.stufe === "rot")
|
||||
return (
|
||||
@@ -88,25 +90,9 @@ function Hinweise({ hinweise }: { hinweise: Hinweis[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
/** 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 (
|
||||
<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. */
|
||||
/** Die letzten Läufe — gebaut wie der Verlauf der KI-Box: Zeit, Gerät, Ergebnis, der Satz darunter. */
|
||||
function Verlauf({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 8)
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 12)
|
||||
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">
|
||||
@@ -131,10 +117,32 @@ function Verlauf({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
)
|
||||
}
|
||||
|
||||
export function HomelabSeite() {
|
||||
/** Wenn der Ausführer schweigt oder sein Bericht alt ist — auf beiden Seiten gleich. */
|
||||
function AusfuehrerHinweis({ stand }: { stand: HomelabStand }) {
|
||||
if (!stand.ausfuehrer.verbunden) {
|
||||
return (
|
||||
<p role="alert" className="rounded-xl border border-bernstein/60 bg-bernstein-grund px-4 py-3 text-[15px] text-bernstein">
|
||||
Der Ausführer auf dem Proxmox-Host meldet sich nicht
|
||||
{stand.ausfuehrer.zuletzt ? ` (zuletzt ${zeitAus(stand.ausfuehrer.zuletzt)})` : ""}. Die Geräte zeigen den letzten
|
||||
bekannten Stand; Updates starten erst, wenn er wieder da ist.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
if (!stand.bericht) return <p className="text-[15px] text-text-2">Der Ausführer hat noch keinen Bericht geschickt.</p>
|
||||
if (stand.bericht.veraltet) {
|
||||
return (
|
||||
<p className="text-[15px] text-bernstein">
|
||||
Der letzte Bericht ist von {zeitAus(stand.bericht.empfangen)}; die Angaben können veraltet sein.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/** Gemeinsame Daten beider Homelab-Seiten — oder was stattdessen zu zeigen ist. */
|
||||
function useHomelabBereich() {
|
||||
const instanz = useInstanz()
|
||||
const partner = usePartner()
|
||||
const box = useBoxZiele()
|
||||
const homelabHier = instanz.data?.rolle === "homelab"
|
||||
const aktiv = homelabHier || !!(partner.data?.eingerichtet && partner.data.erreichbar)
|
||||
const homelab = useHomelab(aktiv)
|
||||
@@ -142,61 +150,54 @@ export function HomelabSeite() {
|
||||
const hinweise = useHomelabHinweise(aktiv)
|
||||
const aktion = useZielAktion()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
if (!aktiv) {
|
||||
return partner.data
|
||||
let ersatz = null
|
||||
if (instanz.isPending || partner.isPending) {
|
||||
ersatz = <Zustandsfeld text="Wird gelesen …" />
|
||||
} else if (!aktiv) {
|
||||
ersatz = partner.data
|
||||
? <HomelabFehlt partner={partner.data} />
|
||||
: <Zustandsfeld fehler text={`Der Partner lässt sich nicht abfragen: ${partner.error?.message ?? "keine Daten"}`} />
|
||||
} else if (homelab.isPending) {
|
||||
// Solange der erste Bericht unterwegs ist, weiß die Seite nichts — also auch nicht „aktuell“ sagen.
|
||||
ersatz = <Zustandsfeld text="Der Homelab-Teil meldet sich gleich …" />
|
||||
} else if (!homelab.data) {
|
||||
ersatz = <Zustandsfeld fehler text={`Der Homelab-Teil antwortet nicht: ${homelab.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"}`} />
|
||||
|
||||
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 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 {
|
||||
ersatz,
|
||||
stand: homelab.data,
|
||||
laufend,
|
||||
laeufe: laeufe.data?.laeufe ?? [],
|
||||
hinweise: hinweise.data?.hinweise ?? [],
|
||||
aktion,
|
||||
}
|
||||
}
|
||||
|
||||
/** Homelab · Cockpit: die Lage auf einen Blick, Hinweise und alle Geräte. */
|
||||
export function HomelabSeite() {
|
||||
const navigate = useNavigate()
|
||||
const { ersatz, stand, laufend, hinweise, aktion } = useHomelabBereich()
|
||||
if (ersatz || !stand) return ersatz
|
||||
|
||||
const berichtZeit = stand.bericht ? zeitAus(stand.bericht.empfangen) : null
|
||||
const l = lage(stand, hinweise, laufend.size, berichtZeit)
|
||||
return (
|
||||
<>
|
||||
<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" })}
|
||||
onAnsehen={() => {
|
||||
if (l.ziel === "updates") void navigate({ to: "/homelab/updates" })
|
||||
else if (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>
|
||||
{!stand.ausfuehrer.verbunden && (
|
||||
<p role="alert" className="text-[15px] text-bernstein">
|
||||
Der Ausführer auf dem Proxmox-Host meldet sich nicht
|
||||
{stand.ausfuehrer.zuletzt ? ` (zuletzt ${zeitAus(stand.ausfuehrer.zuletzt)})` : ""}. Updates starten erst, wenn
|
||||
er wieder da ist.
|
||||
</p>
|
||||
)}
|
||||
{stand.bericht?.veraltet && (
|
||||
<p className="text-[15px] text-bernstein">Der letzte Bericht ist von {berichtZeit}; die Liste kann veraltet sein.</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>
|
||||
|
||||
<AusfuehrerHinweis stand={stand} />
|
||||
{hinweise.length > 0 && <Hinweise hinweise={hinweise} />}
|
||||
<Panel
|
||||
titel="Geräte"
|
||||
id="geraete"
|
||||
@@ -204,9 +205,32 @@ export function HomelabSeite() {
|
||||
>
|
||||
<GeraeteListe ziele={stand.ziele} laufend={laufend} gesperrt={aktion.isPending} onAktion={(a) => aktion.mutate(a)} />
|
||||
</Panel>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/** Homelab · Updates: was ansteht — ein Knopf je Update — und was gelaufen ist. */
|
||||
export function HomelabUpdatesSeite() {
|
||||
const { ersatz, stand, laufend, laeufe, aktion } = useHomelabBereich()
|
||||
if (ersatz || !stand) return ersatz
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel titel="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>
|
||||
<AusfuehrerHinweis stand={stand} />
|
||||
<UpdateListe
|
||||
updates={offeneUpdates(stand.ziele)}
|
||||
laufend={laufend}
|
||||
gesperrt={aktion.isPending || !stand.ausfuehrer.verbunden}
|
||||
onAktion={(a) => aktion.mutate(a)}
|
||||
/>
|
||||
</Panel>
|
||||
<Panel titel="Verlauf">
|
||||
<Verlauf laeufe={laeufe.data?.laeufe ?? []} />
|
||||
<Verlauf laeufe={laeufe} />
|
||||
</Panel>
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user