Files
mission-control-v2/frontend/src/components/homelab/NeuEntdeckt.tsx
T
HitonabiandClaude Opus 5.5 6af42bd42c Welle 2 · Docker-Dienste unter Arcane einzeln: Stacks, Zustand, Gesundheit, Update-Stand; neue Stacks unter „Neu entdeckt“
Statt „Arcane (Docker) · 11 Images“ zeigt das Homelab-Cockpit je Stack jeden Container mit Zustand, Healthcheck und
Update-Stand („Update verfügbar“, „lokal gebaut“ für NerdQuiz und Rippy, „aktuell“). Der Wächter meldet gelb, wenn
ein Container ungesund ist oder ständig neu startet; beendete Container sind kein Befund. Neue Stacks erscheinen unter
„Neu entdeckt“ (eigenes Register, nur „Übernehmen“), Lucy meldet sie einmal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:31:05 +02:00

136 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState } from "react"
import { cn } from "cn"
import { Bestaetigen } from "@/components/cockpit/Bestaetigen"
import { Geraetesymbol } from "@/components/cockpit/Geraetesymbol"
import { Panel } from "@/components/cockpit/Panel"
import { Button } from "@/components/ui/button"
import { geraetebild } from "@/lib/geraetebild"
import type { NeuerStack, NeuesGeraet, NeuWahl } from "@/lib/typen"
import { flugplanZeit } from "@/lib/zeit"
// „Neu entdeckt“ (Ausbauplan Welle 2, Punkt 7, seit 25.09.2026): Geräte, die der Orchestrator noch nicht kennt, mit
// dem, was er über sie weiß, und drei Wegen, sie zu übernehmen. Freigeben setzt das Etikett watcher — erst danach darf
// er dort Snapshots anlegen und Updates einspielen; deshalb fragt die Seite vorher nach.
const WEG: Record<string, string> = {
skript: "Updates über das Community-Script",
"eigene-oberflaeche": "Updates über die eigene Oberfläche der App",
pakete: "Updates über die Pakete im Gast",
}
function Karte({ g, gesperrt, onWahl }: { g: NeuesGeraet; gesperrt: boolean; onWahl: (w: NeuWahl) => void }) {
const bild = geraetebild({ art: g.art, name: g.app?.kennung ?? g.hostname ?? g.name })
const fakten = [
g.app ? `${g.app.name}${g.app.quelle ? ` (GitHub ${g.app.quelle})` : ""}` : "App unbekannt",
g.app ? WEG[g.app.weg] ?? g.app.weg : null,
g.app?.adresse ? `Weboberfläche ${g.app.adresse} wird geprüft` : g.app ? "Weboberfläche nicht gefunden, keine Webprüfung" : null,
].filter(Boolean)
return (
<li className="flaeche-innen flex flex-col gap-3 rounded-2xl p-4">
<div className="flex items-start gap-3">
<Geraetesymbol bild={bild} groesse="md" punkt={g.status === "running" ? "ok" : "aus"} />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-[15px] font-semibold break-words">{g.name}</span>
<span className="text-[13px] text-text-3">
{g.art === "container" ? "Container" : "VM"} {g.vmid}
{g.hostname && g.hostname !== g.name ? ` · „${g.hostname}“` : ""}
{g.erstmals ? ` · entdeckt ${flugplanZeit(new Date(g.erstmals * 1000).toISOString()).toLowerCase()}` : ""}
</span>
</div>
</div>
<ul className="m-0 flex list-none flex-col gap-1 p-0 text-sm text-text-2">
{fakten.map((f) => <li key={f}>{f}</li>)}
{g.freigegeben && <li>Schon freigegeben (Etikett {g.community_script ? "community-script" : "watcher"}).</li>}
{g.ohne_sicherung && (
<li className="text-bernstein">In keinem Sicherungsauftrag — in Proxmox: Rechenzentrum → Backup → den Auftrag bearbeiten.</li>
)}
</ul>
<div className="flex flex-wrap gap-2">
{!g.freigegeben && !g.ausgeschlossen && (
<Button size="sm" disabled={gesperrt} onClick={() => onWahl("freigeben")}>Für Updates freigeben</Button>
)}
<Button variant={g.freigegeben ? "default" : "outline"} size="sm" disabled={gesperrt} onClick={() => onWahl("beobachten")}>
{g.freigegeben ? "Übernehmen" : "Nur beobachten"}
</Button>
{!g.ausgeschlossen && (
<Button variant="ghost" size="sm" disabled={gesperrt} onClick={() => onWahl("ausschliessen")}>Nicht anfassen</Button>
)}
</div>
</li>
)
}
const FRAGE: Record<NeuWahl, { titel: string; text: string; knopf: string }> = {
freigeben: {
titel: "Für Updates freigeben?",
text: "Der Orchestrator setzt in Proxmox das Etikett „watcher“. Danach darf er dort Snapshots anlegen und Updates " +
"einspielen — immer erst nach deinem Klick, mit Snapshot vorher und Rückweg bei Rot.",
knopf: "Freigeben",
},
beobachten: { titel: "Nur beobachten?", text: "", knopf: "Übernehmen" },
ausschliessen: {
titel: "Nicht anfassen?",
text: "Der Orchestrator setzt in Proxmox das Etikett „watcher-aus“ und lässt das Gerät dann auch bei „Alle " +
"aktualisieren“ aus.",
knopf: "Nicht anfassen",
},
}
/** Ein neuer Docker-Stack: nur „Übernehmen“ — Etiketten gibt es unter Arcane nicht. */
function StackKarte({ s, gesperrt, onWahl }: { s: NeuerStack; gesperrt: boolean; onWahl: (w: NeuWahl) => void }) {
return (
<li className="flaeche-innen flex flex-col gap-3 rounded-2xl p-4">
<div className="flex items-start gap-3">
<Geraetesymbol bild={geraetebild({ art: "container", name: s.name })} groesse="md" punkt="ok" />
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="text-[15px] font-semibold break-words">Docker-Stack {s.name}</span>
<span className="text-[13px] text-text-3">
unter Arcane{s.erstmals ? ` · entdeckt ${flugplanZeit(new Date(s.erstmals * 1000).toISOString()).toLowerCase()}` : ""}
</span>
</div>
</div>
<p className="m-0 text-sm break-words text-text-2">Container: {s.container.join(", ") || "–"}</p>
<Button size="sm" className="self-start" disabled={gesperrt} onClick={() => onWahl("beobachten")}>Übernehmen</Button>
</li>
)
}
export function NeuEntdeckt({ geraete, stacks = [], gesperrt, onWahl }: {
geraete: NeuesGeraet[]
/** Neue Docker-Stacks unter Arcane (seit 25.09.2026). */
stacks?: NeuerStack[]
gesperrt: boolean
onWahl: (id: string, wahl: NeuWahl) => void
}) {
const [frage, setFrage] = useState<{ g: NeuesGeraet; wahl: NeuWahl } | null>(null)
const anzahl = geraete.length + stacks.length
if (anzahl === 0) return null
return (
<Panel titel="Neu entdeckt" id="neu">
<p className={cn("-mt-2 text-[15px] text-text-2")}>
{anzahl === 1 ? "Eins kennt" : `${anzahl} kennt`} der Orchestrator noch nicht. Entscheide, ob er
{anzahl === 1 ? " es" : " sie"} nur beobachtet oder auch aktualisieren darf.
</p>
<ul className="m-0 grid list-none gap-3 p-0 md:grid-cols-2">
{geraete.map((g) => (
<Karte key={g.id} g={g} gesperrt={gesperrt}
onWahl={(wahl) => (wahl === "beobachten" ? onWahl(g.id, wahl) : setFrage({ g, wahl }))} />
))}
{stacks.map((s) => <StackKarte key={s.id} s={s} gesperrt={gesperrt} onWahl={(wahl) => onWahl(s.id, wahl)} />)}
</ul>
<Bestaetigen
offen={!!frage}
titel={frage ? `${frage.g.name}: ${FRAGE[frage.wahl].titel}` : ""}
text={frage ? FRAGE[frage.wahl].text : ""}
knopf={frage ? FRAGE[frage.wahl].knopf : ""}
gesperrt={gesperrt}
onNein={() => setFrage(null)}
onJa={() => {
if (frage) onWahl(frage.g.id, frage.wahl)
setFrage(null)
}}
/>
</Panel>
)
}