--- quick_id: 260924-h7x type: quick wave: 1 autonomous: true files_modified: - apps/web/src/app/globals.css - apps/web/src/app/(portal)/modules/proxmox/page.tsx - apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx - apps/web/src/app/(portal)/modules/proxmox/components/proxmox-status.ts (new) - apps/web/src/app/(portal)/modules/proxmox/components/*.test.tsx - apps/web/src/components/layout/header.tsx - apps/web/src/components/dashboard/dashboard-tabs.tsx - apps/web/src/app/(portal)/page.tsx - apps/web/src/messages/de.json, en.json - CHANGELOG.md --- # Quick 260924-h7x — Proxmox-Seite: Status-Design mit Tiefe; Dashboard-Reiter in die Kopfzeile Nutzerauftrag (24.09.): „Kennzeichnen als offline & verwaist. Das Proxmox-Modul scheint zu funktionieren, aber es gefällt mir optisch gar nicht. Benutze den Design-Skill und überarbeite etwas — mehr Tiefe, Farben nach Status etc. Die Reiter sind da, aber die müssen woanders hin, die nehmen zu viel Platz ein. Auch mit Design.“ Die frühere Sperre „KEIN Umbau der Proxmox-Modulseite“ (23.09.) ist damit durch den Nutzer selbst aufgehoben. Design-Plan vom Orchestrator (frontend-design-Skill) — verbindlich, nicht neu erfinden. Keine neuen Pakete. App-Texte deutsch in Sie-Form, alle Schlüssel in de.json UND en.json. Keine Großbuchstaben-Etiketten (kein `uppercase`, kein `tracking-widest`), keine Mittelpunkt-Ketten „A · B · C“ in neuen Texten, kein „→“ an Knöpfen. ## Design-Tokens (in `globals.css`, `:root` und `.dark`) Statusfarben als OKLCH-Variablen plus Tailwind-Abbildung in `@theme inline` (`--color-status-ok` usw., damit `bg-status-ok`, `text-status-ok`, `border-status-ok/40` gehen): | Token | hell | dunkel | Bedeutung | |---|---|---|---| | `--status-ok` | `oklch(0.62 0.15 152)` | `oklch(0.72 0.15 152)` | in Ordnung | | `--status-warn` | `oklch(0.72 0.15 70)` | `oklch(0.80 0.14 75)` | Warnung | | `--status-down` | `oklch(0.58 0.21 27)` | `oklch(0.68 0.19 27)` | nicht erreichbar / Fehler | | `--status-idle` | `oklch(0.62 0.02 260)` | `oklch(0.60 0.02 260)` | noch nicht abgefragt | | `--status-orphan` | `oklch(0.55 0.01 260)` | `oklch(0.52 0.01 260)` | offline & verwaist | | `--well` | `oklch(0.975 0.003 260)` | `oklch(0.235 0.01 260)` | eingelassene Messfelder | Kontrast prüfen: Text in Statusfarbe auf `--card` muss ≥ 4.5:1 (Fließtext) bzw. ≥ 3:1 (große/fette Zahlen) erreichen — sonst Helligkeit anpassen und die gemessene Zahl in der SUMMARY nennen. ## Task 1 — Statuslogik als reine Funktionen (TDD) Neue Datei `components/proxmox-status.ts`: - `serverHealth(server): 'ok' | 'warn' | 'down' | 'idle' | 'orphan'` - `!server.isActive` → `orphan` (VORRANG vor allem anderen — „offline & verwaist“, auch wenn alte Messwerte im Zwischenlager liegen) - kein `status` oder `status.lastPolledAt === null` → `idle` - `!status.reachable` → `down` - erreichbar + irgendein Messwert über Warnschwelle → `warn` - sonst `ok` - Schwellen als EINE benannte Konstante `THRESHOLDS`: Last/Arbeitsspeicher/Datenspeicher warn ≥ 0.80, kritisch ≥ 0.92; PBS letzte Sicherung älter als 26 h → warn; `lastVerifyState` ungleich `ok` (und nicht null) → warn; PMG `virusCount > 0` → warn. - `meterLevel(fraction | null): 'ok' | 'warn' | 'crit' | 'unknown'` für die Balken. - `formatAge(epochSeconds|iso, now)` → „vor 3 Min.“, „vor 5 Std.“, „vor 2 Tagen“ über `Intl.RelativeTimeFormat('de')` bzw. next-intl — keine neue Abhängigkeit. - Tests für jede Verzweigung inkl. `null`-Werte (unbekannt ≠ 0, nie Warnung aus `null`). ## Task 2 — Proxmox-Seite und Karte neu gestalten **Seitenkopf:** Titel „Proxmox“ links, darunter die Beschreibung. Rechts „Jetzt aktualisieren“ (nur Admins, wie heute) als richtiger Knopf mit Kreispfeil-Symbol (dreht sich während des Laufs; `motion-reduce:animate-none`) und „Einstellungen“ als ruhiger Textlink. Breite `max-w-5xl`. **Gesundheitsbalken (das eine prägnante Element der Seite):** direkt unter dem Kopf ein waagrechter Balken, 8 px hoch, voll gerundet, anteilig in Segmente je Zustand geteilt (Reihenfolge down, warn, ok, idle, orphan; leere Zustände entfallen). Darunter eine Zeile mit Legende: farbiger Punkt + Zahl + Wort („2 in Ordnung“, „1 nicht erreichbar“, „1 offline & verwaist“ …, ICU-Plural). Segmente `role="img"` mit `aria-label`, das die Zusammenfassung vorliest. Keine Zahl-in-riesig-plus-Verlauf-Heldenkachel. **Kartenraster:** `grid gap-4 lg:grid-cols-2`. Sortierung: down, warn, ok, idle, orphan, darin `position`. (Die Einstellungsseite behält ihre Reihenfolge.) **Karte (Tiefe über Status, nicht Einheits-Schatten):** - Karte `rounded-xl bg-card` mit Rand `border-border`, links eine 4 px breite Statusleiste (absolut positioniert, volle Höhe, Farbe = Status). - Schatten zweischichtig und im Statuston getönt, z. B. `shadow-[0_1px_2px_oklch(0_0_0/0.06),0_12px_28px_-14px_var(--status-x)]` — bei `orphan` KEIN farbiger Schatten, stattdessen gestrichelter Rand (`border-dashed`), Inhalt `opacity-70 saturate-50`. - Kopfzeile: Produktsymbol (kleines inline-SVG je Typ: PVE = Server-Einschübe, PBS = Archivkiste/Datenträger, PMG = Briefumschlag) in einem 32 px Feld mit `bg-well`, daneben Name (fett) und darunter Produktname ausgeschrieben („Virtualisierung“, „Datensicherung“, „Mail-Gateway“) plus Adresse klein und gedämpft. Rechts eine Statuspille: Punkt + Wort („In Ordnung“, „Warnung“, „Nicht erreichbar“, „Noch nicht abgefragt“, „Offline & verwaist“), Hintergrund Statusfarbe /12, Text in Statusfarbe. Bei `ok` pulsiert der Punkt EINMAL beim Laden nicht — keine Dauer-Animation. - Fuß der Karte: „Letzte Abfrage vor 4 Min.“ (relativ, `title` mit exaktem Zeitpunkt). **Messbereich je Produkt, als eingelassene Felder** (`bg-well`, `rounded-lg`, `shadow-[inset_0_1px_2px_oklch(0_0_0/0.06)]`): - **PVE:** oben zwei Kennzahlen nebeneinander: laufende Gäste (groß, `tabular-nums`) und gestoppte Gäste (gedämpft), Knotenzahl klein. Darunter je Knoten ein „Einschub“: Knotenname links, rechts zwei schmale Balken „Prozessor“ und „Arbeitsspeicher“ mit Prozentzahl (`tabular-nums`) und bei Speicher „12,0 / 64,0 GB“ als `title`/kleine Zeile. Balkenfarbe nach `meterLevel`. Unbekannter Wert: Balken schraffiert/leer + Text „unbekannt“, NIE 0 %. Deutsche Zahlformate (Komma) über `Intl.NumberFormat('de-DE')`. - **PBS:** je Datenspeicher ein Einschub: Name, Füllstandsbalken mit „1,2 / 4,0 TB“, darunter „Letzte Sicherung vor 5 Std.“ (warn-Farbe, wenn > 26 h; „noch keine Sicherung“ gedämpft) und Prüfstatus als kleine Pille (ok grün, sonst warn, null „unbekannt“ grau). - **PMG:** vier Zahlfelder im 2×2-Raster: Eingehend, Ausgehend, Spam, Viren; Viren > 0 in down-Farbe, sonst normal. `null` → „unbekannt“. **Zustände ohne Messwerte:** - `down`: roter Hinweisblock im Well (`bg-status-down/8`, Rand links in Statusfarbe) mit der bekannten Fehlermeldung (`errors.*`) und darunter gedämpft „Zuletzt erreichbar: vor 2 Tagen“ bzw. nichts, wenn nie. `errorDetail` klein und gedämpft in eigener Zeile, nicht in Klammern an den Satz gehängt. - `idle`: ruhiger Text wie heute (Admin-/Nicht-Admin-Variante bleibt, 260923-le6). - `orphan`: Text „Dieser Server ist deaktiviert und wird nicht mehr abgefragt.“ plus für Admins der Hinweis, dass er in den Einstellungen wieder aktiviert werden kann (Link). Alte Messwerte werden bei `orphan` NICHT angezeigt. **Leer-, Lade-, Fehlerzustand der Seite:** Leerzustand als Well mit Serversymbol und Satz + Link (Admins); Laden als zwei Skelett-Karten (`animate-pulse`, `motion-reduce:animate-none`). Tests: bestehende ServerCard-/Seitentests anpassen (Texte/Strukturen), neue Tests für orphan (Vorrang, keine alten Werte), Sortierung, Gesundheitsbalken-Zusammenfassung, unbekannte Werte als „unbekannt“. Die Nur-Admin-Regeln aus 260923-le6 müssen weiter getestet sein. ## Task 3 — Dashboard-Reiter in die Kopfzeile Heute belegt `DashboardTabs` eine eigene Zeile über dem Raster (`app/(portal)/page.tsx`), die Kopfzeilenmitte (`components/layout/header.tsx`) zeigt nur den Text „Startseite“. - Kopfzeile bekommt in der Mitte einen Einhängepunkt `
`. Auf der Startseite (`pathname === '/'`) entfällt der Text „Startseite“; auf allen anderen Seiten bleibt alles wie heute. - `DashboardTabs` rendert per `createPortal` in diesen Einhängepunkt (sobald er im DOM ist — `useEffect` + State; Rückfall: solange kein Einhängepunkt da ist, nichts rendern). Die eigene Zeile über dem Raster entfällt ersatzlos. - Gestaltung als kompakter Umschalter mit Tiefe: eine eingelassene Spur (`bg-muted`, `rounded-lg`, `p-0.5`, `shadow-[inset_0_1px_2px_oklch(0_0_0/0.08)]`, Höhe 32 px), darin die Reiter als Textknöpfe (`text-sm`, `px-3`, `h-7`); der aktive Reiter liegt erhaben darauf (`bg-card`, `shadow-sm`, `font-medium`, Text `foreground`), inaktive gedämpft mit Hover. Keine Unterstreichung, kein Gelb-Flächen-Reiter. - Viele Reiter: Spur höchstens `max-w-[min(56vw,720px)]`, waagrecht scrollbar ohne sichtbare Leiste, an den Rändern weich ausgeblendet (`mask-image` nur wenn überläuft), aktiver Reiter wird ins Bild gescrollt. - Bearbeitungsmodus: Umbenennen/Löschen/Ziehen/Anlegen bleiben funktional gleich (alle bestehenden Tests grün halten bzw. nur Selektoren anpassen). „+“ als 28-px-Rundknopf am Ende der Spur (immer sichtbar, wie heute der Anlegen-Knopf sichtbar ist — Verhalten beibehalten). Löschen-Kreuz erscheint im Bearbeitungsmodus klein im Reiter. - Genau EIN Reiter: Umschalter trotzdem zeigen (sonst weiß niemand, dass es Reiter gibt), aber nur mit „+“ daneben. - Mobil (< md): die Spur darf die Kopfzeilenmitte füllen; Logo-Schriftzug bleibt, Aktionen rechts bleiben erreichbar — nichts darf die Kopfzeile sprengen (overflow prüfen). - Tastatur: Pfeiltasten links/rechts zwischen Reitern (`role="tablist"`/`tab`, `aria-selected`), sichtbarer Fokusring. - `navigation "Dashboard-Reiter"`-Beschriftung bleibt als `aria-label`. Tests: dashboard-tabs.test.tsx an Portal anpassen (Einhängepunkt im Test anlegen), neuer Test: Kopfzeile zeigt „Startseite“ nicht auf `/`, aber auf anderen Pfaden; Pfeiltasten. ## Tore - `pnpm --filter web exec vitest run` komplett grün - `pnpm turbo run type-check lint` grün, Biome-Warnungen web nicht mehr als 53 - CHANGELOG „Unveröffentlicht“: je ein Eintrag unter „Neu“/„Geändert“ in Nutzersprache - Browser-Nachweis macht der Orchestrator (hell UND dunkel, 1400 px und 390 px Breite)