feat(260923-dhh): Proxmox-Modul Aufgabe 6 - Modulseite mit Auslastung

- ServerCard.tsx: einzige Stelle, die einen Messwert in Text verwandelt
  (formatMetric) — null wird ueberall "unbekannt", nie 0/NaN/leer;
  verzweigt ueber productType auf PVE/PBS/PMG; PBS ohne Sicherung zeigt
  "noch keine Sicherung" statt eines Fehlers; nicht erreichbarer Server
  zeigt Klartext-Ursache plus Zeitpunkt der letzten erfolgreichen Messung
- page.tsx: "Jetzt aktualisieren" fragt alle Server neu ab und laedt die
  Liste danach neu, waehrend des Laufs gesperrt; ruhiger Hinweis bei
  leerer Liste mit Weg zu den Einstellungen; Link zu den Einstellungen
  nur fuer Administratoren sichtbar (Anzeige, kein Zugriffsriegel)
- proxmox-api.ts: ProxmoxMetrics-Union (Pve/Pbs/Pmg) fuer typsichere
  Verzweigung im Frontend
- umlaut-dictionary.ts: zwei weitere korrekte Woerter auf die
  Positivliste (Messung, Prozessorlast)

Tore: web 708/708 (>=693), type-check 4/4, Biome apps/web 53 Warnungen
(unveraendert).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 14:45:27 +02:00
parent 723cf6814b
commit 06fcdc0157
7 changed files with 622 additions and 34 deletions
@@ -1,39 +1,75 @@
'use client';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useTranslations } from 'next-intl';
import { listServers, type ProxmoxServer } from '@/lib/proxmox-api';
import Link from 'next/link';
import { useAuthStore } from '@/lib/stores/auth-store';
import { listServers, pollServer, type ProxmoxServer } from '@/lib/proxmox-api';
import { ServerCard } from './components/ServerCard';
/**
* Modulseite (Aufgabe 1: duenne Liste — Name, Typ, Adresse; Aufgabe 6
* ergaenzt die produktabhaengige Auslastungsanzeige ueber `ServerCard`).
* Liest ausschliesslich aus dem Zwischenlager, das `GET servers` liefert —
* kein Live-Zugriff bei Proxmox von hier aus (D-05).
* Modulseite (Aufgabe 6) — liest ausschliesslich aus dem Zwischenlager, das
* `GET servers` liefert; kein Live-Zugriff bei Proxmox von hier aus (D-05).
* "Jetzt aktualisieren" loest je Server eine Abfrage aus und laedt die
* Liste danach neu — waehrend des Laufs ist der Knopf gesperrt.
*/
export default function ProxmoxPage() {
const t = useTranslations('proxmox');
const user = useAuthStore((s) => s.user);
const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';
const [servers, setServers] = useState<ProxmoxServer[] | null>(null);
const [error, setError] = useState<string | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
const reload = useCallback(() => {
listServers()
.then(setServers)
.catch(() => setError(t('loadError')));
}, [t]);
useEffect(() => {
let cancelled = false;
listServers()
.then((data) => {
if (!cancelled) setServers(data);
})
.catch(() => {
if (!cancelled) setError(t('loadError'));
});
return () => {
cancelled = true;
};
}, [t]);
reload();
}, [reload]);
const handleRefresh = async () => {
if (!servers || servers.length === 0) return;
setIsRefreshing(true);
try {
await Promise.all(servers.map((server) => pollServer(server.id).catch(() => undefined)));
reload();
} finally {
setIsRefreshing(false);
}
};
return (
<div className="mx-auto max-w-3xl space-y-6 p-6">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
<p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>
</div>
<div className="flex items-center gap-3">
{servers !== null && servers.length > 0 && (
<button
type="button"
onClick={handleRefresh}
disabled={isRefreshing}
className="rounded border border-border px-4 py-2 text-sm text-foreground hover:bg-muted disabled:cursor-not-allowed disabled:opacity-50"
>
{isRefreshing ? t('card.refreshing') : t('card.refresh')}
</button>
)}
{isAdmin && (
<Link
href="/modules/proxmox/settings"
className="text-sm text-primary hover:underline"
>
{t('card.settingsLink')}
</Link>
)}
</div>
</div>
{error && <p className="text-sm text-destructive">{error}</p>}
@@ -45,23 +81,22 @@ export default function ProxmoxPage() {
{!error && servers !== null && servers.length === 0 && (
<div className="rounded-lg border border-border bg-card p-6 text-sm text-muted-foreground shadow-sm">
{t('emptyState')}
{isAdmin && (
<>
{' '}
<Link href="/modules/proxmox/settings" className="text-primary hover:underline">
{t('card.settingsLink')}
</Link>
</>
)}
</div>
)}
{!error && servers !== null && servers.length > 0 && (
<ul className="space-y-3">
{servers.map((server) => (
<li
key={server.id}
className="rounded-lg border border-border bg-card p-4 shadow-sm"
>
<div className="flex items-center justify-between">
<span className="font-medium">{server.name}</span>
<span className="text-xs uppercase text-muted-foreground">
{server.productType}
</span>
</div>
<p className="mt-1 text-sm text-muted-foreground">{server.baseUrl}</p>
<li key={server.id}>
<ServerCard server={server} />
</li>
))}
</ul>