feat(server): Proxmox-Server verwalten mit Fingerprint-Pinning

Server lassen sich anlegen, prüfen, bearbeiten und löschen. Proxmox
liefert ab Werk ein selbst signiertes Zertifikat; der Admin sieht es
vor dem Speichern und bestätigt den SHA-256-Fingerprint.

Der Pin wird in einem eigenen undici-Connector geprüft, sodass eine
fremde Gegenstelle das Token nie zu sehen bekommt. Weder Prüfung noch
Pin ist im Schema, im Client und per CHECK ausgeschlossen.

Fehler von Proxmox werden zu Ursache und Hinweis; bei 403 nennt der
Hinweis das fehlende Recht samt Pfad. Getestet gegen einen HTTPS-Mock
und im gebauten Stand auf Desktop und 390 px.

Closes #002

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 09:35:10 +02:00
co-authored by Claude Opus 5.5
parent 871bb3a77c
commit 48e2a5cb8f
43 changed files with 2847 additions and 39 deletions
+104
View File
@@ -0,0 +1,104 @@
import { clsx } from "clsx";
/**
* Formular-Bausteine: echtes `<label>`, Fehlermeldung per `aria-describedby` verbunden,
* 44 px hoch auf dem Handy.
*/
export const EINGABE_KLASSE =
"bg-panel-hoch border-linie text-text h-11 w-full rounded-lg border px-3 placeholder:text-muted focus:border-akzent disabled:opacity-60 sm:h-10";
export function Feld({
id,
label,
hilfe,
fehler,
children,
className,
}: {
id: string;
label: string;
hilfe?: React.ReactNode;
fehler?: string;
children: React.ReactNode;
className?: string;
}) {
return (
<div className={clsx("flex flex-col gap-1.5", className)}>
<label htmlFor={id} className="text-sm font-medium">
{label}
</label>
{children}
{fehler ? (
<p id={`${id}-fehler`} className="text-fehler text-sm">
<span aria-hidden="true">✗ </span>
{fehler}
</p>
) : hilfe ? (
<p id={`${id}-hilfe`} className="text-muted text-sm">
{hilfe}
</p>
) : null}
</div>
);
}
/** Verknüpft ein Eingabefeld mit Hilfe- oder Fehlertext. */
export function beschreibung(id: string, fehler?: string, hilfe?: unknown): Record<string, string | boolean> {
if (fehler) return { "aria-describedby": `${id}-fehler`, "aria-invalid": true };
if (hilfe) return { "aria-describedby": `${id}-hilfe` };
return {};
}
export function Schalter({
id,
label,
hilfe,
checked,
onChange,
disabled,
}: {
id: string;
label: string;
hilfe?: React.ReactNode;
checked: boolean;
onChange: (wert: boolean) => void;
disabled?: boolean;
}) {
return (
<div className="flex items-start gap-3">
<input
id={id}
type="checkbox"
checked={checked}
disabled={disabled}
onChange={(ereignis) => onChange(ereignis.target.checked)}
className="accent-akzent mt-0.5 h-5 w-5 shrink-0"
{...(hilfe ? { "aria-describedby": `${id}-hilfe` } : {})}
/>
<div>
<label htmlFor={id} className="text-sm font-medium">
{label}
</label>
{hilfe ? (
<p id={`${id}-hilfe`} className="text-muted text-sm">
{hilfe}
</p>
) : null}
</div>
</div>
);
}
/** Text in JetBrains Mono mit Knopf zum Kopieren (Befehle, Fingerprints, Schlüssel). */
export function Code({ children, className }: { children: string; className?: string }) {
return (
<code
className={clsx(
"bg-grund border-linie block rounded-lg border px-3 py-2 font-mono text-xs break-all sm:text-sm",
className,
)}
>
{children}
</code>
);
}