phase3+4: Homelab-Teil (Ausfuehrer, Ziele, Jetzt updaten mit Rueckweg) und Seite Homelab fuer alle Geraete
- deploy/homelab/ausfuehrer.py: laeuft als root auf dem Proxmox-Host, holt Auftraege beim Homelab-Teil ab (Pull, kein offener Port), feste Aktionsliste, prueft Etiketten selbst; Bericht gegen den echten Host erprobt (nur lesend). Kein Proxmox-Schluessel im Container noetig. - services/homelab: Kanal mit gemeinsamem Geheimnis, App-Katalog, Inventar -> Ziele im gemeinsamen Modell (GitHub-Versionen, Webpruefung, alte Paketlisten = unklar), Jetzt updaten: Snapshot -> Update -> Pruefung -> bei Rot zurueck + dringende Meldung - Waechter in der Rolle homelab: Ausfuehrer schweigt, Gaeste antworten nicht - kern/github.py fuer beide Rollen (auch untagged Releases mit Version im Namen) - Oberflaeche: Seite Homelab zeigt alle Geraete als Karten (KI-Box ueber /api/ziele, Homelab ueber /api/homelab/ziele) mit Stand, Rueckweg und Knopf samt Rueckfrage - Einrichtung als Skripte (container-anlegen, ausrollen, ausfuehrer-einrichten, box-partner) — noch nicht ausgefuehrt, jeder Schritt braucht das User-OK - frontend-bauen-box.sh: Frontend-Pruefung und Build auf der Box, wenn der PC keinen Speicher hat Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a47489fa85
commit
5e9227c4a3
+110
-49
@@ -1,29 +1,18 @@
|
||||
import { Chip, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Chip, type ChipArt, Zustandsfeld } from "@/components/cockpit/Chip"
|
||||
import { Panel } from "@/components/cockpit/Panel"
|
||||
import { useInstanz, usePartner } from "@/lib/abfragen"
|
||||
import type { PartnerStatus } from "@/lib/typen"
|
||||
import { ZielKarte } from "@/components/cockpit/ZielKarte"
|
||||
import { useBoxZiele, useHomelab, useHomelabLaeufe, useInstanz, usePartner, useZielAktion } from "@/lib/abfragen"
|
||||
import type { HomelabLauf, PartnerStatus, ZielStand } from "@/lib/typen"
|
||||
import { flugplanZeit } from "@/lib/zeit"
|
||||
|
||||
/** Was der Homelab-Teil übernimmt (Bestandsaufnahme vom 24.09.2026; ab Phase 3 kommt die Liste live). */
|
||||
/** 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"]
|
||||
|
||||
function Verbindung({ partner }: { partner: PartnerStatus }) {
|
||||
if (partner.erreichbar) {
|
||||
return (
|
||||
<>
|
||||
<p className="text-[15px] text-text-2">
|
||||
{partner.instanz ?? partner.name} antwortet{partner.version ? ` (Version ${partner.version})` : ""}. Die
|
||||
Übersicht der Geräte mit ihren Updates folgt als Nächstes.
|
||||
</p>
|
||||
<p className="ziffern text-sm text-text-3">{partner.url}</p>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<p role="alert" className="text-[15px] text-rot-text">
|
||||
Der Homelab-Teil ({partner.url}) ist {partner.fehler ?? "nicht erreichbar"}. Hält das an, meldet der Wächter es
|
||||
nach fünf Minuten auf Telegram.
|
||||
</p>
|
||||
)
|
||||
const ERGEBNIS: Record<string, { art: ChipArt; text: string }> = {
|
||||
eingespielt: { art: "gruen", text: "eingespielt" },
|
||||
zurueckgerollt: { art: "bernstein", text: "zurückgerollt" },
|
||||
fehler: { art: "rot", text: "Fehler" },
|
||||
neustart: { art: "cyan", text: "Neustart" },
|
||||
}
|
||||
|
||||
function NochNichtEingerichtet() {
|
||||
@@ -34,8 +23,8 @@ function NochNichtEingerichtet() {
|
||||
auch die KI-Box von außen.
|
||||
</p>
|
||||
<p className="text-[15px] text-text-2">
|
||||
Steht er, zeigt diese Seite alle Geräte mit ihren offenen Updates. Ein Update startest du dann hier mit
|
||||
„Jetzt updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst
|
||||
Steht er, erscheinen hier alle Geräte mit ihren offenen Updates. Ein Update startest du mit „Jetzt
|
||||
updaten“: Vorher wird gesichert, danach die Erreichbarkeit geprüft, und ist sie rot, geht es von selbst
|
||||
zurück.
|
||||
</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
@@ -53,34 +42,106 @@ function NochNichtEingerichtet() {
|
||||
)
|
||||
}
|
||||
|
||||
export function HomelabSeite() {
|
||||
const instanz = useInstanz()
|
||||
const partner = usePartner()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
if (!partner.data) {
|
||||
return <Zustandsfeld fehler text={`Der Stand des Homelab-Teils lässt sich nicht lesen: ${partner.error?.message ?? "keine Daten"}`} />
|
||||
}
|
||||
|
||||
// Liefert der Homelab-Teil die Seite selbst aus, ist die Box sein Partner (ab Phase 3).
|
||||
if (instanz.data?.rolle === "homelab") {
|
||||
return (
|
||||
<Panel titel="Homelab" rechts={<Chip art="gruen">Diese Instanz</Chip>}>
|
||||
<p className="text-[15px] text-text-2">Diese Seite kommt vom Homelab-Teil auf dem Proxmox-PC.</p>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
const p = partner.data
|
||||
const chip = !p.eingerichtet
|
||||
function HomelabFehlt({ partner }: { partner: PartnerStatus }) {
|
||||
const chip = !partner.eingerichtet
|
||||
? <Chip>Noch nicht eingerichtet</Chip>
|
||||
: p.erreichbar
|
||||
? <Chip art="gruen">Verbunden</Chip>
|
||||
: <Chip art="rot">Antwortet nicht</Chip>
|
||||
|
||||
: <Chip art="rot">Antwortet nicht</Chip>
|
||||
return (
|
||||
<Panel titel="Homelab" rechts={chip}>
|
||||
{p.eingerichtet ? <Verbindung partner={p} /> : <NochNichtEingerichtet />}
|
||||
{partner.eingerichtet ? (
|
||||
<p role="alert" className="text-[15px] text-rot-text">
|
||||
Der Homelab-Teil ({partner.url}) ist {partner.fehler ?? "nicht erreichbar"}. Hält das an, meldet der
|
||||
Wächter es nach fünf Minuten auf Telegram.
|
||||
</p>
|
||||
) : (
|
||||
<NochNichtEingerichtet />
|
||||
)}
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
function LetzteLaeufe({ laeufe }: { laeufe: HomelabLauf[] }) {
|
||||
const fertig = laeufe.filter((l) => l.status !== "laeuft").slice(0, 6)
|
||||
if (fertig.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>
|
||||
)
|
||||
}
|
||||
|
||||
export function HomelabSeite() {
|
||||
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)
|
||||
const laeufe = useHomelabLaeufe(aktiv)
|
||||
const aktion = useZielAktion()
|
||||
|
||||
if (instanz.isPending || partner.isPending) return <Zustandsfeld text="Wird gelesen …" />
|
||||
|
||||
// Laufende Updates je Gerät: der letzte Schritt steht auf der Karte.
|
||||
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
|
||||
|
||||
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.
|
||||
</p>
|
||||
{aktiv && homelab.data && !homelab.data.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.
|
||||
</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>
|
||||
)}
|
||||
{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>}
|
||||
</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 ?? []} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user