feat(server): Verbindungstest mit Fähigkeitserkennung

Der Test liest Version, Cluster, Nodes, Storages, Bridges, VNets und die
effektiven Rechte des Tokens und leitet daraus ab, welcher Vorgang per
API, per SSH oder gar nicht geht – je PVE-Version: Template-Bau per API
erst ab 8.4 mit import-Storage, Snippets und LXC-Exec nur per SSH.

Fehlende Rechte erscheinen einzeln mit Pfad und Zweck; die Rollenliste
in docs/proxmox-rechte.md prüft ein Test gegen den Code. SSH-Host-Keys
werden wie Zertifikate erst nach Bestätigung genutzt, und bestätigt
werden kann nur ein Schlüssel, den der letzte Test gesehen hat.

Statussymbole in der GUI sind jetzt gezeichnet: ◐ und ○ fehlen im
Web-Subset von JetBrains Mono.

Refs #003

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:44:27 +02:00
co-authored by Claude Opus 5.5
parent 48e2a5cb8f
commit 86b650e20a
26 changed files with 2094 additions and 18 deletions
+37
View File
@@ -0,0 +1,37 @@
/** Formate für die Anzeige – deutsch, gleich in Web und (sinngemäß) CLI. */
export function bytes(wert: number | null | undefined): string {
if (wert === null || wert === undefined) return "–";
const einheiten = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
let zahl = wert;
let stufe = 0;
while (zahl >= 1024 && stufe < einheiten.length - 1) {
zahl /= 1024;
stufe += 1;
}
return `${zahl.toLocaleString("de-DE", { maximumFractionDigits: zahl < 10 && stufe > 0 ? 1 : 0 })} ${einheiten[stufe]}`;
}
export function prozent(anteil: number | null | undefined): string {
if (anteil === null || anteil === undefined) return "–";
return `${Math.round(anteil * 100)} %`;
}
/** „vor 3 min“, „vor 2 h“, sonst Datum mit Uhrzeit. */
export function zeitpunkt(iso: string | null | undefined, jetzt = new Date()): string {
if (!iso) return "nie";
const datum = new Date(iso);
const sekunden = Math.round((jetzt.getTime() - datum.getTime()) / 1000);
if (sekunden < 60) return "gerade eben";
if (sekunden < 3600) return `vor ${Math.round(sekunden / 60)} min`;
if (sekunden < 86_400) return `vor ${Math.round(sekunden / 3600)} h`;
return datum.toLocaleString("de-DE", { dateStyle: "medium", timeStyle: "short" });
}
/** Dauer wie in der CLI: „18s“, „1m 12s“, „1h 03m“. */
export function dauer(ms: number): string {
const s = Math.max(0, Math.round(ms / 1000));
if (s < 60) return `${s}s`;
if (s < 3600) return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, "0")}s`;
return `${Math.floor(s / 3600)}h ${String(Math.floor((s % 3600) / 60)).padStart(2, "0")}m`;
}
+12
View File
@@ -1,3 +1,4 @@
import type { VerbindungsBericht } from "@/engine/proxmox/erkundung";
import { z } from "@/lib/validierung";
/**
@@ -114,4 +115,15 @@ export type ServerSicht = {
erreichbar: boolean | null;
pveVersion: string | null;
geprueftAm: string | null;
letzterTest: VerbindungsBericht | null;
/** Gepinnte SSH-Host-Keys je Node – öffentlich, kein Geheimnis. */
sshHostSchluessel: Record<string, string>;
};
export const HostSchluesselBestaetigen = z.object({
knoten: z.string().trim().min(1),
hostSchluessel: z
.string()
.trim()
.regex(/^SHA256:[A-Za-z0-9+/]{43}$/, "Kein SHA256-Fingerprint eines Host-Keys."),
});