"use client"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { Karte } from "@/components/karte"; import { Knopf } from "@/components/knopf"; import { Status, type StatusArt } from "@/components/status"; import type { VerbindungsBericht } from "@/engine/proxmox/erkundung"; import type { Strategie } from "@/engine/proxmox/faehigkeiten"; import { apiAufruf, ApiFehler } from "@/lib/api-client"; import { bytes, prozent, zeitpunkt } from "@/lib/formate"; type TestErgebnis = { ok: true; bericht: VerbindungsBericht } | { ok: false; art: string; ursache: string; hinweis: string }; const WEG_STATUS: Record = { api: { art: "erledigt", text: "per API" }, ssh: { art: "laeuft", text: "per SSH" }, nicht_moeglich: { art: "fehler", text: "nicht möglich" }, }; const VORGAENGE: Array<[keyof VerbindungsBericht["faehigkeiten"], string]> = [ ["templateBau", "Template bauen"], ["vmProvisionierung", "VM provisionieren"], ["lxcProvisionierung", "LXC provisionieren"], ["snippets", "cloud-init-Snippets"], ["lxcFeaturesErweitert", "LXC-Features außer nesting"], ]; /** * Ergebnis des Verbindungstests: Version, Nodes, Storages, Bridges, fehlende Rechte und – am * wichtigsten – welcher Vorgang auf welchem Weg geht. Host-Keys lassen sich hier bestätigen. */ export function Verbindungsbericht({ name, bericht: anfangsBericht, geprueftAm, verwalten, }: { name: string; bericht: VerbindungsBericht | null; geprueftAm: string | null; verwalten: boolean; }) { const router = useRouter(); const [bericht, setBericht] = useState(anfangsBericht); const [fehler, setFehler] = useState<{ ursache: string; hinweis?: string } | null>(null); const [laeuft, setLaeuft] = useState(false); const [zeit, setZeit] = useState(geprueftAm); async function testen() { setLaeuft(true); setFehler(null); try { const ergebnis = await apiAufruf("POST", `/api/v1/server/${name}/test`); setZeit(new Date().toISOString()); if (ergebnis.ok) setBericht(ergebnis.bericht); else setFehler({ ursache: ergebnis.ursache, hinweis: ergebnis.hinweis }); router.refresh(); } catch (ursache) { setFehler({ ursache: ursache instanceof ApiFehler ? ursache.message : "Unerwarteter Fehler." }); } finally { setLaeuft(false); } } async function bestaetigen(knoten: string, hostSchluessel: string) { try { await apiAufruf("POST", `/api/v1/server/${name}/ssh/hostschluessel`, { knoten, hostSchluessel }); await testen(); } catch (ursache) { setFehler({ ursache: ursache instanceof ApiFehler ? ursache.message : "Unerwarteter Fehler.", hinweis: ursache instanceof ApiFehler ? ursache.hinweis : undefined, }); } } const pflichtFehlend = bericht?.fehlendeRechte.filter((r) => r.pflicht) ?? []; const optionalFehlend = bericht?.fehlendeRechte.filter((r) => !r.pflicht) ?? []; return (
{laeuft ? "Testet …" : "Verbindung testen"} } >
{laeuft ? ( Liest Version, Nodes, Storages, Netzwerk und Rechte … ) : null} {fehler ? (
{fehler.ursache} {fehler.hinweis ?

{fehler.hinweis}

: null}
) : null} {!bericht && !fehler && !laeuft ? Noch nicht getestet. : null} {bericht ? (
Proxmox VE
{bericht.version}
Cluster
{bericht.cluster ? ( <> {bericht.cluster.name} ·{" "} {bericht.cluster.quorate ? "Quorum vorhanden" : "kein Quorum"} ) : ( "Einzelhost" )}
Geprüft
{zeitpunkt(zeit)}
) : null} {bericht?.warnungen.map((warnung) => ( {warnung} ))}
{bericht ? ( <>
    {VORGAENGE.map(([schluessel, titel]) => { const strategie = bericht.faehigkeiten[schluessel]; const anzeige = WEG_STATUS[strategie.weg]; return (
  • {titel} {anzeige.text} {strategie.grund}
  • ); })}
{pflichtFehlend.length === 0 ? ( Alle nötigen Rechte vorhanden. ) : (
{pflichtFehlend.length === 1 ? "Ein Recht fehlt" : `${pflichtFehlend.length} Rechte fehlen`}{" "} – die Rolle „ProxDeploy“ steht vollständig in docs/proxmox-rechte.md.
)} {optionalFehlend.length > 0 ? (
{optionalFehlend.length} optionale Rechte fehlen
) : null}
{bericht.knoten.map((knoten) => ( {knoten.name} {knoten.ip ? · {knoten.ip} : null} } aktion={ knoten.online ? online : offline } > {knoten.online ? (
Version
{knoten.version ?? "–"}
CPU
{knoten.cpus ?? "–"} Kerne · {prozent(knoten.cpuAuslastung)}
RAM
{bytes(knoten.speicherBelegt)} / {bytes(knoten.speicherGesamt)}
{knoten.storages.map((s) => ( ))}
Storages
Name Typ Inhalte Frei
{s.storage} {s.typ} {s.inhalte.join(", ")} {bytes(s.frei)} / {bytes(s.gesamt)}

Bridges

    {knoten.bridges.map((b) => (
  • {b.name} {b.vlanBewusst ? " · VLAN-fähig" : ""} {b.cidr ? ` · ${b.cidr}` : ""} {b.kommentar ? ` · ${b.kommentar}` : ""}
  • ))}
{knoten.ssh ? (

SSH

{knoten.ssh.zustand === "ok" ? ( Verbunden mit {knoten.ssh.host} · Host-Key bestätigt ) : knoten.ssh.zustand === "nicht_bestaetigt" ? (
Host-Key noch nicht bestätigt – bitte mit dem Node vergleichen: {knoten.ssh.hostSchluessel}

Auf dem Node:{" "} ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

{verwalten && knoten.ssh.hostSchluessel ? ( bestaetigen(knoten.name, knoten.ssh!.hostSchluessel!)} > Host-Key von {knoten.name} bestätigen ) : null}
) : ( {knoten.ssh.meldung ?? "SSH nicht möglich."} )}
) : null}
) : (

Node ist offline – keine Details.

)}
))} {bericht.vnets && bericht.vnets.length > 0 ? (
    {bericht.vnets.map((v) => (
  • {v.name} {" "} · Zone {v.zone} {v.tag ? ` · VLAN ${v.tag}` : ""} {v.alias ? ` · ${v.alias}` : ""}
  • ))}
) : null} ) : null}
); } function RechteListe({ rechte }: { rechte: VerbindungsBericht["fehlendeRechte"] }) { return ( ); }