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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user