Files
tessera-ctl/.planning/quick/260924-i8v-proxmox-kachel-fuers-dashboard/260924-i8v-PLAN.md
T
schalli dd09c08311
Tessera CI/CD / Lint & Type Check (push) Successful in 54s
Tessera CI/CD / Tests (push) Successful in 1m22s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 20s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m18s
docs(quick-260924-i8v): Proxmox-Kachel fuers Dashboard
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 13:38:17 +02:00

45 KiB
Raw Blame History

phase, plan, quick_id, type, wave, depends_on, autonomous, requirements, files_modified, estimate, must_haves
phase plan quick_id type wave depends_on autonomous requirements files_modified estimate must_haves
quick-260924-i8v 01 260924-i8v execute 1
true
QUICK-260924-i8v
packages/shared/src/index.ts
apps/api/src/dashboard/widget-module-map.spec.ts
apps/web/src/components/proxmox/proxmox-status.ts (git mv aus app/(portal)/modules/proxmox/components/)
apps/web/src/components/proxmox/proxmox-status.test.ts (git mv)
apps/web/src/components/proxmox/HealthBar.tsx (git mv)
apps/web/src/components/proxmox/status-styles.ts (git mv)
apps/web/src/components/proxmox/proxmox-server-picker.tsx (neu)
apps/web/src/components/proxmox/proxmox-server-picker.test.tsx (neu)
apps/web/src/app/(portal)/modules/proxmox/page.tsx
apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
apps/web/src/app/(portal)/page.tsx
apps/web/src/app/(portal)/page.test.tsx
apps/web/src/components/dashboard/widget-registry.tsx
apps/web/src/components/dashboard/widget-registry.test.tsx
apps/web/src/components/dashboard/widget-catalog-modal.test.tsx
apps/web/src/components/dashboard/widgets/proxmox-widget.tsx (neu)
apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx (neu)
apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts (neu)
apps/web/src/components/dashboard/widgets/proxmox-widget-model.test.ts (neu)
apps/web/src/components/settings/proxmox-widget-config-form.tsx (neu)
apps/web/src/components/settings/proxmox-widget-config-form.test.tsx (neu)
apps/web/src/components/settings/widget-settings-panel.tsx
apps/web/src/components/settings/widget-settings-panel.test.tsx
apps/web/src/messages/de.json
apps/web/src/messages/en.json
docs/anleitung-anwender.md
docs/anleitung-entwicklung.md
CHANGELOG.md
tokens raw_tokens tasks confidence
60000 60000 3 low
truths artifacts key_links
Wer das Proxmox-Modul nutzen darf, findet im Katalog „Widget hinzufügen“ die Kachel „Proxmox“ (als letzte, die übrigen neun in unveränderter Reihenfolge) und kann sie anlegen; die API akzeptiert den Typ proxmox
Wer das Modul nicht nutzen darf, sieht die Kachel weder im Katalog noch auf dem Dashboard (Katalogfilter als Komfort, verbindlich serverseitig in DashboardService.getWidgets, fail-closed)
Oben in der Kachel steht ein 6 px hoher Gesundheitsbalken und eine Zeile in Worten: „Alles in Ordnung“ in der Ok-Farbe, sonst z. B. „1 nicht erreichbar, 1 mit Warnung“ in der Farbe des schlimmsten Zustands
Darunter stehen die Server sortiert nach down, warn, ok, idle, orphan, je mit Statuspunkt, Name und genau einer rechtsbündigen Kennzahl; ein unbekannter Wert heißt „unbekannt“, nie 0
Ein Klick auf eine Serverzeile öffnet /modules/proxmox; im Bearbeitungsmodus führt keine Zeile irgendwohin und die ganze Kachel bleibt ziehbar
Die Kachel liest alle 60 s neu aus dem Zwischenlager (GET servers), pausiert bei verborgenem Browser-Tab und löst niemals eine Abfrage bei Proxmox aus
Titel und Serverauswahl lassen sich im Bearbeitungsmodus direkt an der Kachel und unter Einstellungen > Dashboard festlegen; keine Auswahl bedeutet alle Server
Schmale Kachel (unter 15rem): nur Punkte und Namen; sehr kleine Kachel (unter 7.5rem hoch oder unter 8rem breit): nur Balken und Zusammenfassung
path provides
apps/web/src/components/dashboard/widgets/proxmox-widget.tsx ProxmoxWidget (WidgetProps) — Balken, Zusammenfassung, Serverliste, Minutentakt, Bearbeitungsmodus
path provides
apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts reine Funktionen: resolveProxmoxWidgetConfig, selectServers, healthSummary, widgetKeyFigure
path provides
apps/web/src/components/proxmox/ gemeinsamer Ort für proxmox-status.ts, HealthBar.tsx (mit variant compact), status-styles.ts, proxmox-server-picker.tsx
path provides
apps/web/src/components/settings/proxmox-widget-config-form.tsx Einstellungsformular der Kachel (Titel + Serverauswahl) für Einstellungen > Dashboard
path provides
packages/shared/src/index.ts WIDGET_TYPES enthält 'proxmox', WIDGET_MODULE_SLUGS = { proxmox: 'proxmox' }
from to via pattern
packages/shared/src/index.ts WIDGET_MODULE_SLUGS apps/api/src/dashboard/dashboard.service.ts getWidgets (über widget-module-map.ts) getModuleSlugForWidgetType('proxmox') === 'proxmox' proxmox: 'proxmox'
from to via pattern
apps/web/src/app/(portal)/page.tsx proxmox-widget.tsx registerWidget('proxmox', ProxmoxWidget) registerWidget('proxmox'
from to via pattern
proxmox-widget.tsx apps/web/src/lib/proxmox-api.ts listServers einziger Import aus proxmox-api; Intervall 60 s + visibilitychange listServers
from to via pattern
proxmox-widget.tsx apps/web/src/components/proxmox/HealthBar.tsx <HealthBar variant="compact" counts=... /> variant="compact"
from to via pattern
apps/web/src/components/settings/widget-settings-panel.tsx proxmox-widget-config-form.tsx widget.widgetType === 'proxmox' ProxmoxWidgetConfigForm

Quick 260924-i8v — Proxmox-Kachel fürs Dashboard

Nutzerauftrag (24.09.): „Jetzt die Proxmox-Kachel fürs Dashboard bauen.“

Grundlagen, auf denen dieser Plan steht (nicht neu erfinden):

  • quick-260922-m1h hat den Weg „ein Modul bringt seine Kachel mit“ gebaut: WIDGET_TYPES + WIDGET_MODULE_SLUGS in packages/shared/src/index.ts (die API validiert per @IsIn gegen genau diese Liste), registerWidget() in (portal)/page.tsx, visibleWidgetTypes() als Katalogfilter. Eine Kachel mit moduleSlug verschwindet für Benutzer ohne Modulzugriff automatisch aus Katalog und Dashboard (serverseitig DashboardService.getWidgets, fail-closed). Für „gesperrt“ ist deshalb nichts zu bauen — die Kachel erscheint schlicht nicht; widgets.unavailable im Wrapper bleibt der Rückfall.
  • quick-260924-h7x hat die Statussprache der Proxmox-Seite festgelegt: serverHealth, THRESHOLDS, meterLevel, formatAge, sortServersByHealth, summarizeHealth, HEALTH_ORDER, HealthBar, HEALTH_STYLE/METER_TEXT/WELL, Tokens --status-* und --status-*-fg in globals.css. Diese Teile werden wiederverwendet, nicht kopiert — dafür ziehen sie an einen neutralen Ort apps/web/src/components/proxmox/.

Verbindliche Gestaltungsregeln (wie h7x): Zustand steuert die Optik; keine Großbuchstaben-Etiketten, keine Mittelpunkt-Ketten, kein Pfeilzeichen an Knöpfen oder in Texten; App-Texte deutsch in Sie-Form, jeder neue Schlüssel in de.json und en.json. Keine neuen Pakete. Schriftfarbe in Statusfarbe immer über die -fg-Variante (HEALTH_STYLE[h].text), Flächen über fill — so bleibt der in h7x gemessene Kontrast von mindestens 4,5:1 erhalten.

Rasterrechnung (aus dashboard-grid.tsx: 24 Spalten, rowHeight 20, margin 8): Höhe h Zeilen = 20h + 8(h−1) px, also 4 Zeilen = 104 px, 8 Zeilen = 216 px. Breite bei rund 1400 px Inhalt: eine Spalte ≈ 50 px, 3 Spalten ≈ 166 px, 8 Spalten ≈ 456 px.

Die erste echte Modul-Kachel: „Proxmox“ zeigt auf dem Dashboard den Zustand der Proxmox-Server in der Statussprache der Modulseite — kompakter Gesundheitsbalken, Zusammenfassung in Worten, darunter die Server nach Dringlichkeit mit je einer Kennzahl, Klick führt zur Modulseite. Sie liest nur das Zwischenlager, frischt sich minütlich auf, lässt sich auf Titel und Serverauswahl einstellen und passt sich per Container-Query an kleine Kachelgrößen an.

Purpose: Der Nutzer sieht den Zustand seiner Proxmox-Umgebung, ohne die Modulseite zu öffnen. Output: Kachel-Komponente samt reiner Modell-Funktionen, gemeinsamer Proxmox-Ordner, Einstellungsformular, Registry-/Katalog-/API-Tests angepasst, Doku und Changelog.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/STATE.md @./CLAUDE.md @.planning/quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/260922-m1h-SUMMARY.md @.planning/quick/260924-h7x-proxmox-seite-status-design-und-dashboar/260924-h7x-SUMMARY.md @apps/web/src/components/dashboard/widget-registry.tsx @apps/web/src/components/dashboard/widgets/widget-wrapper.tsx @apps/web/src/components/dashboard/widgets/favorites-widget.tsx @apps/web/src/app/(portal)/modules/proxmox/components/proxmox-status.ts @apps/web/src/app/(portal)/modules/proxmox/components/HealthBar.tsx @apps/web/src/app/(portal)/modules/proxmox/components/status-styles.ts @apps/web/src/app/(portal)/modules/proxmox/page.tsx @apps/web/src/lib/proxmox-api.ts @apps/web/src/components/settings/widget-settings-panel.tsx

Schnittstellen, die der Executor braucht (aus dem Code gelesen, Stand 8bfa4fc):

  • WidgetProps = { instanceId: string; config: Record<string, unknown>; isEditMode: boolean }
  • WIDGET_CONSTRAINTS: Record<WidgetType, { minW; minH; defaultW; defaultH }>
  • registerWidget(type: WidgetType, component: ComponentType<WidgetProps>)
  • visibleWidgetTypes(registry, accessibleModuleSlugs: readonly string[] | null)
  • listServers(): Promise<ProxmoxServer[]> — GET /modules/proxmox/servers, @UseModule('proxmox'), für alle Rollen mit Modulzugriff lesbar (nur Schreib-/Abfrage-Endpunkte sind Admin-only).
  • ProxmoxServer: id, name, productType ('pve'|'pbs'|'pmg'), isActive, position, status: ProxmoxServerStatus | null; status.metrics: pve { guestsRunning, guestsStopped, nodes[{cpu, mem, maxmem}], storages[{disk, maxdisk}] }, pbs { datastores[{ used, total, lastBackupAt (Unix-Sekunden), lastVerifyState }] }, pmg { countIn, countOut, spamCount, virusCount } — alle Messwerte number | null.
  • serverHealth(server, now) → 'ok'|'warn'|'down'|'idle'|'orphan', HEALTH_ORDER = ['down','warn','ok','idle','orphan'], summarizeHealth(servers, now) → Record<ServerHealth, number>, sortServersByHealth(servers, now), ratio(used, total), meterLevel(fraction), isBackupStale(lastBackupAt, now), toEpochMs, formatAge(value, now, locale) → 'vor 5 Std.' | null.
  • HEALTH_STYLE[h] = { fill, pill, text, shadow }, METER_TEXT[level].
  • updateWidgetConfig(instanceId, partialConfig) aus @/lib/dashboard-api (Muster Favoriten-Titel).
  • Admin-Erkennung wie auf der Modulseite: useAuthStore((s) => s.user), Rolle ADMIN oder SUPER_ADMIN.
  • Wiederverwendbare Übersetzungen (Namensraum proxmox): legend.<h> (ICU-Plural, „nicht erreichbar“, „mit Warnung“ …), health.<h> („In Ordnung“, „Warnung“ …), loadError, loading, card.unknownValue („unbekannt“), card.pbs.noBackupYet, card.settingsLink, card.product.<typ>.
  • Tailwind 4.3.1, geprüft per Probelauf in der Planung: @max-[15rem]:hidden erzeugt @container (width < 15rem), [@container(max-height:7.5rem)]:hidden erzeugt @container (max-height:7.5rem). Der Container ist der Kachelrumpf im Wrapper (@container-size).
Aufgabe 1: Durchstich — Kacheltyp „proxmox“ von der Typliste über API-Whitelist, Registry und Katalog bis zur Anzeige von Balken und Zusammenfassung apps/web/src/components/proxmox/proxmox-status.ts, apps/web/src/components/proxmox/proxmox-status.test.ts, apps/web/src/components/proxmox/HealthBar.tsx, apps/web/src/components/proxmox/status-styles.ts, apps/web/src/app/(portal)/modules/proxmox/page.tsx, apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx, packages/shared/src/index.ts, apps/api/src/dashboard/widget-module-map.spec.ts, apps/web/src/components/dashboard/widget-registry.tsx, apps/web/src/components/dashboard/widget-registry.test.tsx, apps/web/src/components/dashboard/widget-catalog-modal.test.tsx, apps/web/src/components/dashboard/widgets/proxmox-widget.tsx, apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts, apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx, apps/web/src/app/(portal)/page.tsx, apps/web/src/app/(portal)/page.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json **1. Gemeinsame Teile an neutralen Ort ziehen (Git-Historie erhalten).** Mit `git mv` die vier Dateien `proxmox-status.ts`, `proxmox-status.test.ts`, `HealthBar.tsx`, `status-styles.ts` aus `apps/web/src/app/(portal)/modules/proxmox/components/` nach `apps/web/src/components/proxmox/` verschieben. Ihre gegenseitigen relativen Importe bleiben gültig. In `modules/proxmox/page.tsx` und `components/ServerCard.tsx` (und in Tests, die direkt importieren) auf `@/components/proxmox/...` umstellen. `ServerCard.tsx` bleibt am Modulort, nur die Seite braucht sie. Keine Logikänderung; Kopfkommentare um einen Satz ergänzen („seit 260924-i8v gemeinsam für Modulseite und Dashboard-Kachel“). Grund: eine Kachel unter `components/` soll nicht in einen Routenordner unter `app/` greifen.

2. HealthBar bekommt eine kompakte Variante. Optionale Prop variant: 'full' | 'compact', Standard full (Modulseite unverändert). compact: Balken h-1.5 (6 px) statt h-2, ohne Legende; der Balken ist dann aria-hidden, weil die Kachel darunter die Zusammenfassung als sichtbaren Text zeigt (sonst würde ein Vorleser sie doppelt vorlesen). data-testid="health-bar" bleibt; zusätzlich data-variant für Tests.

3. Typ und Modulbindung eintragen in packages/shared/src/index.ts: 'proxmox' ans Ende von WIDGET_TYPES (die neun bisherigen behalten Reihenfolge, der Katalog zeigt Proxmox zuletzt); WIDGET_MODULE_SLUGS = { proxmox: 'proxmox' } — der Slug ist derselbe wie @UseModule('proxmox') im Controller und slug: 'proxmox' in proxmox.seed.ts. Den Kommentar „heute bewusst leer“ richtigstellen. Nur löschbare TypeScript-Syntax (Warnung in der Datei beachten, m1h).

4. Registry (widget-registry.tsx): WIDGET_CONSTRAINTS.proxmox = { minW: 3, minH: 4, defaultW: 8, defaultH: 8 } mit Kommentar im Stil der Nachbarn: 4 Zeilen = 104 px reichen genau für Balken und Zusammenfassung (die Liste blendet sich darunter per Container-Query aus); 8×8 ≈ 456×216 px bei 1400 px Breite zeigt rund sechs Serverzeilen; 3 Spalten ≈ 166 px = Punkte und Namen. Inline-SVG ProxmoxIcon im Projektmuster (Server-Einschübe wie das Leersymbol der Modulseite: zwei abgerundete Rechtecke mit je einem Punkt). Registry-Eintrag proxmox mit nameKey: 'proxmox.name', descriptionKey: 'proxmox.description' (Namensraum widgets), moduleSlug: WIDGET_MODULE_SLUGS.proxmox, component: PlaceholderWidget.

5. Modell-Datei anlegen widgets/proxmox-widget-model.ts (rein, ohne React), zunächst mit healthSummary(counts) => { allOk: boolean; entries: Array<{ health; count }>; worst: ServerHealth | null }: entries = alle Zustände außer ok mit Anzahl > 0, in HEALTH_ORDER; allOk = es gibt Server und entries ist leer; worst = erster Zustand in HEALTH_ORDER mit Anzahl > 0. Aufgabe 2 erweitert die Datei.

6. Kachel, erster Schnitt widgets/proxmox-widget.tsx, 'use client', exportiert ProxmoxWidget({ instanceId, config, isEditMode }: WidgetProps). Aus @/lib/proxmox-api wird ausschließlich listServers (und Typen) importiert — die Funktion für die manuelle Abfrage, die POST …/poll auslöst, darf in keiner Kachel-Datei vorkommen (T-I8V-02). Beim Einhängen einmal laden (Abbruch-Flag gegen setState nach dem Aushängen); Zustand servers: ProxmoxServer[] | null, loadFailed: boolean (Fehler als Flag, nicht als Text — t gehört nicht in Effekt-Abhängigkeiten, Befund 14 aus favorites-widget), now beim erfolgreichen Laden setzen. Darstellung, Wurzel flex h-full flex-col overflow-hidden, Innenabstand p-2.5:

  • Laden: 6 px hohe Leiste bg-muted, pulsierend mit motion-reduce:animate-none, dazu sr-only proxmox.loading.
  • Laden fehlgeschlagen und noch nie eine Liste: Satz proxmox.loadError, gedämpft, zentriert.
  • Leere Liste: Satz widgets.proxmox.empty; für Admins darunter proxmox.card.settingsLink als Link auf /modules/proxmox/settings — im Bearbeitungsmodus nur als Text, ohne Link.
  • Sonst: HealthBar variant="compact" mit summarizeHealth(servers, now) und darunter die Zusammenfassung als p (text-sm font-medium truncate, title = voller Text): bei allOk widgets.proxmox.allOk in HEALTH_STYLE.ok.text; sonst die Einträge als „Anzahl + Wort aus proxmox.legend.<h> (mit count)“, mit Komma und Leerzeichen verbunden, in HEALTH_STYLE[worst].text. Die Anzahl ok wird nicht genannt.

7. Anmelden: in (portal)/page.tsx Import und registerWidget('proxmox', ProxmoxWidget) nach xframe; in (portal)/page.test.tsx ein vi.mock für proxmox-widget wie für die übrigen Kacheln.

8. Texte (de/en) unter widgets.proxmox: name „Proxmox“/„Proxmox“, description „Zustand Ihrer Proxmox-Server auf einen Blick“/„Health of your Proxmox servers at a glance“, allOk „Alles in Ordnung“/„All good“, empty „Noch kein Proxmox-Server eingetragen.“/„No Proxmox server added yet.“

9. Bestehende Tests nachziehen — sie benutzen „proxmox“ bisher als Beispiel für einen unbekannten Typ, das stimmt jetzt nicht mehr:

  • widget-registry.test.tsx: ALL_WIDGET_TYPES um 'proxmox' am Ende ergänzen, „neun“ in den Testnamen zu „zehn“; der Test „keine Kachel trägt einen moduleSlug“ wird zu „nur proxmox trägt moduleSlug 'proxmox', alle anderen keinen“; die beiden Unbekannt-Typ-Tests nehmen 'gibt-es-nicht'; visibleWidgetTypes(WIDGET_REGISTRY, []) erwartet alle Typen außer proxmox, neu dazu ['proxmox'] => alle zehn; Constraints proxmox = 3/4/8/8.
  • widget-catalog-modal.test.tsx: Reihenfolgetest mit accessibleModuleSlugs={['proxmox']} => alle zehn in WIDGET_TYPES-Reihenfolge; mit [] => neun ohne Proxmox. Die beiden Tests, die bisher vorübergehend clock zur Modul-Kachel gemacht haben, prüfen jetzt die echte Kachel „Proxmox“ (fehlt bei [], erscheint bei ['proxmox'], fehlt bei null während „Notizen“ bleibt).
  • apps/api/src/dashboard/widget-module-map.spec.ts: „die neun Kacheln sind Plattform-Kacheln“ wird zu „alle außer proxmox ohne Modul, getModuleSlugForWidgetType('proxmox') === 'proxmox'“; die DTO-Whitelist deckt 'proxmox' über it.each([...WIDGET_TYPES]) von selbst ab.

10. Neuer Test widgets/proxmox-widget.test.tsx mit echtem NextIntlClientProvider + de.json (Muster proxmox-page-roles.test.tsx), vi.mock('@/lib/proxmox-api') mit listServers und einem Spion für die manuelle Abfragefunktion, Attrappe für @/lib/stores/auth-store und next/link, Baukasten makeServer(overrides) mit Zwischenlager je Zustand. Fälle: alle ok => „Alles in Ordnung“ mit Klasse text-status-ok-fg; je ein down, warn, ok => „1 nicht erreichbar, 1 mit Warnung“ mit text-status-down-fg; kompakter Balken vorhanden (data-variant="compact", h-1.5), keine Legende; leere Liste => Satz, Admin sieht Link auf /modules/proxmox/settings, Rolle USER nicht; listServers lehnt ab => „Die Serverliste konnte nicht geladen werden.“; der Abfrage-Spion wird nie aufgerufen. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard src/components/proxmox "src/app/(portal)/page.test.tsx" "src/app/(portal)/modules/proxmox" && pnpm --filter @tessera/api exec vitest run src/dashboard && pnpm --filter @tessera/web exec tsc --noEmit && pnpm --filter @tessera/api exec tsc --noEmit && test ! -e "apps/web/src/app/(portal)/modules/proxmox/components/proxmox-status.ts" && grep -q "proxmox: 'proxmox'" packages/shared/src/index.ts && grep -q "registerWidget('proxmox'" "apps/web/src/app/(portal)/page.tsx" Typ proxmox steht in WIDGET_TYPES (zuletzt) und in WIDGET_MODULE_SLUGS; die API-Whitelist akzeptiert ihn, getModuleSlugForWidgetType('proxmox') liefert 'proxmox'; Registry, Katalog und Seite kennen die Kachel; die Kachel lädt die Serverliste und zeigt kompakten Balken plus Zusammenfassung bzw. Lade-, Fehler- und Leerzustand. Die vier gemeinsamen Dateien liegen unter components/proxmox/, die Modulseite verhält sich unverändert (ihre Tests grün). Web- und API-Tests der betroffenen Bereiche sowie beide type-checks grün. Commit feat(260924-i8v): ….

Aufgabe 2: Serverliste mit Kennzahl je Zeile, Sortierung, Serverfilter, Links, Größenstufen per Container-Query und Minutentakt apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts, apps/web/src/components/dashboard/widgets/proxmox-widget-model.test.ts, apps/web/src/components/dashboard/widgets/proxmox-widget.tsx, apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx, apps/web/src/components/proxmox/proxmox-status.ts, apps/web/src/components/proxmox/proxmox-status.test.ts, apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json - Modell: resolveProxmoxWidgetConfig — Titel kein String wird zu leer; serverIds kein Array wird zu leer; Nicht-Strings und leere Strings fallen weg; doppelte Kennungen einmal - Modell: selectServers — leere Auswahl liefert alle Server; Auswahl filtert nach Kennung; nur noch gelöschte Kennungen ausgewählt liefert leere Liste mit selectionGone true - Modell: widgetKeyFigure — down/idle/orphan liefern kind status; PVE ok liefert guests (running, total = running + stopped), total 0 liefert noGuests; PVE warn liefert load mit dem höchsten bekannten Anteil aus Knoten-CPU, Knoten-RAM und Speicher; PBS liefert backup mit der ÄLTESTEN bekannten letzten Sicherung samt stale-Flag, Datenspeicher ohne jede Sicherung liefern noBackup, keine Datenspeicher liefern unknown; PMG liefert mailIn aus countIn, null liefert unknown; metrics null bei erreichbarem Server liefert unknown - proxmox-status: formatPercent(0.87, 'de') passt auf /87\s%/; formatCount(12904, 'de') ergibt „12.904“ - Kachel: Zeilen erscheinen in der Reihenfolge down, warn, ok, idle, orphan - Kachel: Kennzahlen „3/4 Gäste laufen“, „Auslastung 87 %“ (warn), „Sicherung vor 5 Std.“, „12.904 eingehend“, „nicht erreichbar“, „offline & verwaist“, „noch nicht abgefragt“; unbekannte Werte zeigen „unbekannt“ und nie eine 0 (kein „0 eingehend“, kein „0 %“) - Kachel: config.serverIds beschränkt Zeilen UND Balken/Zusammenfassung auf die Auswahl; nur gelöschte Kennungen → Satz selectionGone - Kachel: Ansichtsmodus — jede Zeile ist ein Link auf /modules/proxmox; Bearbeitungsmodus — keine Links in der Kachel - Kachel: config.title nicht leer → Überschrift h2; leer → keine Kopfzeile - Kachel mit falschen Zeitgebern: nach 60 s zweiter Aufruf von listServers; bei document.visibilityState hidden kein Aufruf im Takt; visibilitychange zurück auf visible lädt sofort; nach dem Aushängen keine weiteren Aufrufe; die manuelle Abfragefunktion wird in keinem Fall aufgerufen - Kachel: scheitert ein späteres Nachladen, bleibt die zuletzt geladene Liste stehen (kein Fehlersatz) - Kachel: Kennzahl trägt die Klasse @max-[15rem]:hidden, die Liste [@container(max-height:7.5rem)]:hidden und @max-[8rem]:hidden **RED zuerst**: `proxmox-widget-model.test.ts` neu und die Fälle in `proxmox-widget.test.tsx` gemäß `` schreiben, laufen lassen, Fehlschlag belegen, dann umsetzen.

1. Zahlformat nicht verdoppeln. formatPercent(fraction, locale) und formatCount(value, locale) aus ServerCard.tsx unverändert als Exporte nach components/proxmox/proxmox-status.ts ziehen; ServerCard.tsx importiert sie von dort. Zwei Tests in proxmox-status.test.ts (deutsches Prozentformat hat ein geschütztes Leerzeichen — per Regex mit \s prüfen).

2. Modell erweitern (proxmox-widget-model.ts, rein):

  • resolveProxmoxWidgetConfig(config) => { title: string; serverIds: string[] }, abwehrend wie picture-frame-config.ts (siehe <behavior>).
  • selectServers(servers, serverIds) => { servers: ProxmoxServer[]; selectionGone: boolean }.
  • widgetKeyFigure(server, now) => Unterscheidungstyp KeyFigure: { kind: 'status'; health: 'down'|'idle'|'orphan' }, { kind: 'guests'; running; total }, { kind: 'noGuests' }, { kind: 'load'; fraction; level: MeterLevel }, { kind: 'backup'; at: number; stale: boolean }, { kind: 'noBackup' }, { kind: 'mailIn'; count }, { kind: 'unknown' }. Regeln: zuerst serverHealth — bei down, idle, orphan immer status (ein verwaister Server zeigt keine alten Messwerte, wie in h7x). Bei ok/warn und metrics === null => unknown. PVE: im Zustand warn der höchste bekannte Anteil aus nodes[].cpu, ratio(mem, maxmem) und ratio(disk, maxdisk) der Speicher (level = meterLevel(fraction)), sonst Gäste; ist guestsRunning/guestsStopped keine Zahl => unknown. PBS: die älteste bekannte lastBackupAt über alle Datenspeicher (sie ist der Grund für eine Warnung „Sicherung zu alt“), stale = isBackupStale(at, now). PMG: countIn. Ein unbekannter Wert ist nie 0 (Grundregel aus proxmox-status.ts).

3. Kachel ausbauen (proxmox-widget.tsx):

  • Konfiguration über resolveProxmoxWidgetConfig(config); Kopfzeile nur bei nicht leerem Titel, Markup wie die Favoriten-Kopfzeile (Rand unten, truncate text-sm font-semibold als h2). Die Eingabe im Bearbeitungsmodus folgt in Aufgabe 3.
  • Ablauf: selectServers => Balken und Zusammenfassung aus der gefilterten Liste => sortServersByHealth(gefiltert, now) => Zeilen. selectionGone => Satz widgets.proxmox.selectionGone statt Balken und Liste.
  • Liste als ul, min-h-0 flex-1 overflow-y-auto, dazu [@container(max-height:7.5rem)]:hidden und @max-[8rem]:hidden (sehr kleine Kachel: nur Balken und Zusammenfassung). Keinen weiteren Container in der Kachel setzen — der Rumpf im Wrapper ist bereits @container-size, ein innerer Container würde die Abfragen umlenken.
  • Zeile: flex items-center gap-2 rounded-md px-1.5 py-1 text-sm; Punkt h-2 w-2 shrink-0 rounded-full + HEALTH_STYLE[h].fill, aria-hidden; Name min-w-0 flex-1 truncate (bei orphan opacity-70, wie h7x nur den Namen dämpfen); sr-only das Zustandswort proxmox.health.<h>, damit der Zustand nie nur an der Farbe hängt; Kennzahl shrink-0 tabular-nums text-xs rechtsbündig mit @max-[15rem]:hidden (schmale Kachel: nur Punkte und Namen). Wiederholt die Kennzahl nur das Zustandswort (kind: 'status'), ist sie aria-hidden.
  • Kennzahltexte über useLocale(): guests => widgets.proxmox.guests; noGuests => widgets.proxmox.noGuests; load => widgets.proxmox.load mit formatPercent; backup => widgets.proxmox.backupAgo mit formatAge(at, now, locale); noBackup => proxmox.card.pbs.noBackupYet; mailIn => widgets.proxmox.mailIn mit formatCount; status => proxmox.legend.<h> mit count: 1; unknown => proxmox.card.unknownValue. Farben: status => HEALTH_STYLE[h].text; load => METER_TEXT[level]; backup mit stale => HEALTH_STYLE.warn.text; unknown und alle übrigen => text-muted-foreground.
  • Ansichtsmodus: jede Zeile ist ein next/link auf /modules/proxmox mit hover:bg-muted/60 und sichtbarem Fokusring. Bearbeitungsmodus: dieselbe Zeile als div ohne Ziel und ohne Tabstopp. Bewusste Abweichung vom Favoriten-Muster (dort Anker mit verhindertem Klick): Links stehen im Abbruch-Selektor von dashboard-grid.tsx, Anker-Zeilen würden das Ziehen über fast die ganze Kachel blockieren. Das gilt auch für den Admin-Link im Leerzustand.
  • Minutentakt: Konstante REFRESH_MS = 60_000; setInterval ruft nur dann listServers auf, wenn document.visibilityState !== 'hidden'; ein visibilitychange-Hörer lädt sofort, sobald die Seite wieder sichtbar ist. Aufräumen beim Aushängen: Intervall, Hörer, Abbruch-Flag. Scheitert ein Nachladen, nachdem schon eine Liste da war, bleibt sie stehen; den Fehlersatz gibt es nur ohne jede Liste. now wird bei jedem erfolgreichen Laden gesetzt (relative Zeitangaben).

4. Texte (de/en) unter widgets.proxmox: guests „{running}/{total} {total, plural, one {Gast läuft} other {Gäste laufen}}“ / „{running}/{total} {total, plural, one {guest running} other {guests running}}“; noGuests „keine Gäste“/„no guests“; load „Auslastung {percent}“/„Load {percent}“; backupAgo „Sicherung {age}“/„Backup {age}“; mailIn „{count} eingehend“/„{count} incoming“; selectionGone „Die ausgewählten Server gibt es nicht mehr. Wählen Sie im Bearbeitungsmodus andere aus.“/„The selected servers no longer exist. Choose others in edit mode.“ Umlaute echt schreiben; die Umlaut-Wache (umlaut-guard.spec.ts) muss grün bleiben.

5. Tests für die Zeitgeber: vi.useFakeTimers(), Zeitvorschub mit await act(async () => { await vi.advanceTimersByTimeAsync(60_000) }), document.visibilityState per Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => … }) umschalten und visibilitychange auf document auslösen; nach jedem Test echte Zeitgeber zurück und die Eigenschaft wiederherstellen. Die Container-Query-Stufen sind in jsdom nicht auswertbar — dort nur die Klassen prüfen; die tatsächliche Wirkung prüft der Browser-Nachweis. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run src/components/dashboard/widgets/proxmox-widget src/components/proxmox "src/app/(portal)/modules/proxmox" src/messages && test "$(grep -vE '^\s*(//|*|/*|{/*)' apps/web/src/components/dashboard/widgets/proxmox-widget.tsx apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts | grep -c 'pollServer')" -eq 0 && test "$(grep -c 'function formatPercent' "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx")" -eq 0 Alle <behavior>-Fälle grün, RED-Phase im Commit-Verlauf belegt (Test-Commit vor Umsetzungs-Commit). Die Kachel zeigt die sortierte, gefilterte Serverliste mit genau einer Kennzahl je Zeile, verlinkt im Ansichtsmodus, bleibt im Bearbeitungsmodus ziehbar, stuft sich per Container-Query ab und lädt minütlich nur aus dem Zwischenlager nach. Kein Zahlformat doppelt (ServerCard nutzt die gemeinsamen Funktionen), Modulseiten-Tests weiter grün.

Aufgabe 3: Titel und Serverauswahl einstellen (an der Kachel im Bearbeitungsmodus und unter Einstellungen > Dashboard), Doku, Changelog, Gesamt-Tore apps/web/src/components/proxmox/proxmox-server-picker.tsx, apps/web/src/components/proxmox/proxmox-server-picker.test.tsx, apps/web/src/components/dashboard/widgets/proxmox-widget.tsx, apps/web/src/components/dashboard/widgets/proxmox-widget.test.tsx, apps/web/src/components/settings/proxmox-widget-config-form.tsx, apps/web/src/components/settings/proxmox-widget-config-form.test.tsx, apps/web/src/components/settings/widget-settings-panel.tsx, apps/web/src/components/settings/widget-settings-panel.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json, docs/anleitung-anwender.md, docs/anleitung-entwicklung.md, CHANGELOG.md - Auswahl-Bauteil: zeigt je Server ein Kästchen, sortiert nach position, Beschriftung Name plus Produktwort (Virtualisierung/Datensicherung/Mail-Gateway); ausgewählte Kennungen sind angehakt - Auswahl-Bauteil: Anhaken/Abhaken ruft onChange mit der neuen Kennungsliste in Listenreihenfolge; Kennungen gelöschter Server fallen dabei heraus; alles abhaken ergibt eine leere Liste - Auswahl-Bauteil: der Hinweis „Ohne Auswahl zeigt die Kachel alle Server.“ steht sichtbar da - Kachel im Bearbeitungsmodus: Titelfeld (Beschriftung „Titel“) speichert entprellt nach 1500 ms per updateWidgetConfig mit { title } - Kachel im Bearbeitungsmodus: Knopf „Server auswählen“ mit aria-expanded öffnet die Auswahl an Stelle der Liste; eine Änderung speichert sofort per updateWidgetConfig mit { serverIds } und filtert die Anzeige - Kachel: Verlassen des Bearbeitungsmodus schließt die Auswahl - Einstellungsformular: lädt die Server einmal per listServers, zeigt Lade-, Fehler- und Leersatz; Titeländerung ruft onChange({ title }), Auswahländerung onChange({ serverIds }) - Einstellungsbereich: Kachel-Typ proxmox rendert das Formular; ein gesetzter Titel erscheint hinter „Proxmox #1“ - Weder Auswahl-Bauteil noch Formular noch Kachel rufen die manuelle Abfragefunktion auf **RED zuerst** für Auswahl-Bauteil, Formular, Einstellungsbereich und die neuen Kachel-Fälle gemäß ``, dann umsetzen.

1. Gemeinsames Auswahl-Bauteil components/proxmox/proxmox-server-picker.tsx: ProxmoxServerPicker({ servers, selectedIds, onChange }) als fieldset mit legend widgets.proxmox.serversLabel („Angezeigte Server“), Hinweis widgets.proxmox.serversHint („Ohne Auswahl zeigt die Kachel alle Server.“), je Server ein Kästchen, Reihenfolge nach position, Beschriftung Name plus gedämpftes Produktwort proxmox.card.product.<typ>. Eindeutige Feldkennungen je Instanz über useId. onChange(next): Kennungen in Listenreihenfolge, nur vorhandene Server — so räumt jede Änderung Kennungen gelöschter Server mit auf. Keine eigene Datenabfrage im Bauteil; es bekommt die Liste hereingereicht.

2. Kachel im Bearbeitungsmodus (proxmox-widget.tsx): Innenabstand oben pt-5, damit die 20 px hohe Griffleiste des Wrappers das Titelfeld nicht verdeckt (vgl. top-6 im XFrame). Die Kopfzeile erscheint im Bearbeitungsmodus immer: Titelfeld im Favoriten-Muster (Klasse widgetNoDrag, Beschriftung widgets.proxmox.titleLabel, Platzhalter widgets.proxmox.titlePlaceholder, Entprellung 1500 ms, updateWidgetConfig(instanceId, { title }), Zeitgeber beim Aushängen löschen), daneben ein kleiner Textknopf widgets.proxmox.chooseServers („Server auswählen“) mit aria-expanded und data-no-drag. Offen ersetzt die Auswahl den Listenbereich (scrollbar, Hülle mit widgetNoDrag, damit Klicks auf Beschriftungen kein Ziehen starten); sie bekommt die bereits geladene, ungefilterte Liste — kein zusätzlicher Abruf. Eine Änderung setzt den lokalen Zustand und speichert sofort mit updateWidgetConfig(instanceId, { serverIds }) (Muster Ansichtswechsel der Favoriten); die Anzeige filtert sofort mit. Verlässt der Nutzer den Bearbeitungsmodus, schließt die Auswahl. Lokaler Zustand für Titel und Auswahl wird aus config initialisiert (wie viewMode bei den Favoriten). Grund für die Auswahl direkt an der Kachel: die Seite Einstellungen > Dashboard zeigt nur die Kacheln des ersten Reiters (bekannte Grenze aus quick-260923-ad9); eine Proxmox-Kachel auf einem zweiten Reiter wäre sonst nicht einstellbar. Die Einstellungsseite selbst wird nicht geändert.

3. Einstellungsformular components/settings/proxmox-widget-config-form.tsx: ProxmoxWidgetConfigForm({ config, onChange }) — Titelfeld (Kennung proxmox-widget-title, Aufbau wie FavoritesConfig, sendet den rohen Tippwert), darunter nach einmaligem listServers() das Auswahl-Bauteil; Ladesatz proxmox.loading, Fehlersatz proxmox.loadError, ohne Server widgets.proxmox.empty. Aus @/lib/proxmox-api nur listServers und Typen importieren. In widget-settings-panel.tsx einen Zweig widget.widgetType === 'proxmox' mit dem Formular ergänzen und 'proxmox' in die Bedingung für den Titel-Zusatz in der Instanz-Kopfzeile aufnehmen.

4. Texte (de/en) unter widgets.proxmox: titleLabel „Titel“/„Title“, titlePlaceholder „Titel (optional)“/„Title (optional)“, serversLabel „Angezeigte Server“/„Servers shown“, serversHint „Ohne Auswahl zeigt die Kachel alle Server.“/„With nothing selected, the tile shows all servers.“, chooseServers „Server auswählen“/„Choose servers“.

5. Doku in Alltagssprache:

  • docs/anleitung-anwender.md: neue Zeile „Proxmox“ in der Tabelle „Verfügbare Widgets“ (was die Kachel zeigt, Klick führt zur Proxmox-Seite, nur mit Zugriff auf das Modul sichtbar, aktualisiert sich jede Minute aus dem zuletzt gespeicherten Stand und fragt die Server dabei nicht neu ab, Titel und Serverauswahl im Bearbeitungsmodus oder unter Einstellungen > Dashboard); Proxmox in die Aufzählung „Für Uhr, Suchleiste, … gibt es zusätzliche Einstellungen“ und in den Absatz „Dashboard > Widgets“ aufnehmen; im Abschnitt „### Proxmox“ ein kurzer Absatz zur Kachel.
  • docs/anleitung-entwicklung.md, Abschnitt „Eine Kachel zum Modul“: Proxmox als erstes echtes Beispiel nennen (proxmox-widget.tsx, Modellfunktionen in proxmox-widget-model.ts) und dass die gemeinsame Statuslogik seit 260924-i8v unter apps/web/src/components/proxmox/ liegt.

6. CHANGELOG.md unter „## Unveröffentlicht“, Abschnitt „### Neu“, ein Stichpunkt in Nutzersprache, im Stil der Nachbarn (Aufzählung mit Semikolon oder Gedankenstrich, keine Mittelpunkte): Dashboard-Kachel „Proxmox“ — farbiger Balken mit „Alles in Ordnung“ oder z. B. „1 nicht erreichbar“, darunter die Server, auffällige zuerst, mit je einer Kennzahl (laufende Gäste, letzte Sicherung, eingehende Mails); Klick öffnet die Proxmox-Seite; eigener Titel und Auswahl einzelner Server; aktualisiert sich jede Minute, ohne die Server neu abzufragen; nur für Benutzer mit Zugriff auf das Modul.

7. Gesamt-Tore (alle Befehle aus <verify>): Web-Tests vollständig, API-Tests vollständig, pnpm turbo run type-check lint, Biome-Warnungen Web höchstens 53 (Stand vorher: 53), Stilprüfung der neuen Dateien und der neuen Texte, Tailwind-Probelauf: ein Wegwerf-Skript im Scratchpad, das im Ordner apps/web @tailwindcss/postcss mit @import "tailwindcss" source(none); und @source auf proxmox-widget.tsx laufen lässt und prüft, dass die Ausgabe @container (width < 15rem), @container (width < 8rem) und @container (max-height:7.5rem) enthält; das Skript danach löschen, nichts davon committen. Keine neuen any, !-Nicht-null-Behauptungen oder Biome-Ausnahmen in den neuen Dateien. Docker wird nicht neu gebaut — den Browser-Nachweis macht der Orchestrator. cd /home/vicolab/projects/tessera-ctl && pnpm --filter @tessera/web exec vitest run && pnpm --filter @tessera/api exec vitest run && pnpm turbo run type-check lint && W=$(pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -oE '^Found [0-9]+ warning' | grep -oE '[0-9]+'); test "${W:-0}" -le 53 && test -z "$(grep -nE 'uppercase|tracking-widest|·|→|dangerouslySetInnerHTML' apps/web/src/components/dashboard/widgets/proxmox-widget.tsx apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts apps/web/src/components/proxmox/proxmox-server-picker.tsx apps/web/src/components/settings/proxmox-widget-config-form.tsx)" && test "$(grep -vE '^\s*(//|*|/*|{/*)' apps/web/src/components/dashboard/widgets/proxmox-widget.tsx apps/web/src/components/proxmox/proxmox-server-picker.tsx apps/web/src/components/settings/proxmox-widget-config-form.tsx | grep -c 'pollServer')" -eq 0 && node -e 'for (const f of ["de","en"]) { const m = require("./apps/web/src/messages/" + f + ".json"); const s = JSON.stringify(m.widgets.proxmox); for (const k of ["name","description","allOk","empty","guests","noGuests","load","backupAgo","mailIn","selectionGone","titleLabel","titlePlaceholder","serversLabel","serversHint","chooseServers"]) if (!(k in m.widgets.proxmox)) throw new Error(f + ": fehlt widgets.proxmox." + k); if (/·|→/.test(s)) throw new Error(f + ": verbotenes Zeichen"); }' && grep -q "Proxmox" CHANGELOG.md && grep -q "| Proxmox |" docs/anleitung-anwender.md && grep -q "components/proxmox" docs/anleitung-entwicklung.md Titel und Serverauswahl lassen sich an der Kachel im Bearbeitungsmodus (jeder Reiter) und unter Einstellungen > Dashboard setzen; keine Auswahl = alle Server; Kennungen gelöschter Server räumen sich bei der nächsten Änderung selbst auf. Anwender- und Entwicklerdoku sowie CHANGELOG ergänzt. Web-Tests vollständig, API-Tests vollständig, type-check und lint grün, Biome-Warnungen Web ≤ 53, Tailwind erzeugt alle drei Container-Stufen, Stil- und Abfrage-Grep leer. Commits je Aufgabe mit feat(260924-i8v)/test(260924-i8v)/docs(260924-i8v); .planning/** wird vom Executor nicht committet.

<threat_model>

Trust Boundaries

Boundary Description
Browser → API GET /modules/proxmox/servers Kachel und Formular lesen Serverdaten; Zugriff entscheidet @UseModule('proxmox') serverseitig
Browser → API PATCH /dashboard/widgets/:id (updateWidgetConfig) config.title/config.serverIds kommen vom Client und sind nicht vertrauenswürdig
API → Proxmox-Hosts Abfragen laufen nur über Zeitplaner bzw. Admin-Knopf der Modulseite; die Kachel darf diese Grenze nie auslösen

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-I8V-01 Information Disclosure Proxmox-Kachel für Benutzer ohne Modulzugriff high mitigate WIDGET_MODULE_SLUGS.proxmox = 'proxmox' → DashboardService.getWidgets filtert fail-closed; GET servers bleibt hinter @UseModule('proxmox'); Katalogfilter nur Komfort (T-M1H-01). API-Spec prüft die Zuordnung.
T-I8V-02 Denial of Service Kachel/Formular lösen Live-Abfragen bei Proxmox aus (je Kachel und Benutzer alle 60 s) high mitigate Nur listServers (Zwischenlager) importiert; Test mit Spion: manuelle Abfragefunktion nie aufgerufen; Grep-Tor in Aufgabe 2 und 3; Takt pausiert bei verborgenem Tab.
T-I8V-03 Tampering config.serverIds / config.title vom Client low mitigate Nur als clientseitiger Anzeigefilter über die vom Server gelieferte Liste genutzt; resolveProxmoxWidgetConfig verwirft Nicht-Strings; kein Zugriff auf Server außerhalb der eigenen Liste möglich.
T-I8V-04 Tampering (XSS) Servername und Titel in der Kachel medium mitigate Ausgabe nur als React-Text, kein Roh-HTML-Einschub (Grep-Tor in Aufgabe 3).
T-I8V-05 Information Disclosure Fehlerdetails auf gemeinsam sichtbaren Dashboards low mitigate Die Kachel zeigt bei down nur „nicht erreichbar“, weder errorDetail noch rawSample noch Adresse; Details bleiben auf der Modulseite.
</threat_model>
Executor (in Aufgabe 3 gebündelt): - `pnpm --filter @tessera/web exec vitest run` — vollständig grün - `pnpm --filter @tessera/api exec vitest run` — vollständig grün (API-Spec wurde angefasst) - `pnpm turbo run type-check lint` — grün; Biome-Warnungen Web ≤ 53 - Stil- und Abfrage-Grep leer; Tailwind-Probelauf erzeugt alle drei Container-Stufen

Browser-Nachweis — führt der Orchestrator durch, kein Executor-Task (Playwright, lokaler Stack mit --build, hell UND dunkel, 1400 px Breite):

  1. Admin: „Widget hinzufügen“ zeigt „Proxmox“ als zehnte Kachel; anlegen → 8×8, kein 400.
  2. Kachel zeigt 6-px-Balken und „Alles in Ordnung“ (grün) bzw. die Zusammenfassung in der Farbe des schlimmsten Zustands; Zeilen in der Reihenfolge down, warn, ok, idle, orphan, Kennzahlen rechts.
  3. Klick auf eine Zeile öffnet /modules/proxmox; im Bearbeitungsmodus nicht, die Kachel lässt sich über den Zeilen ziehen und vergrößern.
  4. Größenstufen: auf 4 Spalten verkleinern → nur Punkte und Namen; auf 3×4 → nur Balken und Zusammenfassung.
  5. Bearbeitungsmodus: Titel setzen, „Server auswählen“ → einen Server abwählen → Kachel filtert, nach Neuladen bleibt es so; Einstellungen > Dashboard zeigt dieselbe Auswahl.
  6. Netzwerk über gut 60 s: nur GET /modules/proxmox/servers im Minutentakt, kein POST …/poll; Tab verbergen → kein Abruf.
  7. Benutzer ohne Proxmox-Zugriff: Katalog ohne „Proxmox“, vorhandene Kachel erscheint nicht.
  8. Englisch umschalten: keine rohen Schlüssel.

<success_criteria>

  • Die Kachel „Proxmox“ ist im Katalog für berechtigte Benutzer anlegbar und zeigt den Zustand in der Statussprache der Modulseite (Balken, Zusammenfassung, sortierte Liste, eine Kennzahl je Server).
  • Gemeinsame Proxmox-Teile liegen einmal unter apps/web/src/components/proxmox/; keine kopierte Statuslogik, kein doppeltes Zahlformat.
  • Kein Aufruf der manuellen Abfrage aus Kachel, Auswahl oder Formular; minütliches Nachladen nur aus dem Zwischenlager, pausiert bei verborgenem Tab.
  • Titel und Serverauswahl an der Kachel und in den Einstellungen; unbekannte Werte heißen „unbekannt“.
  • Alle Tore grün, Biome-Warnungen Web ≤ 53, Doku und CHANGELOG in Nutzersprache ergänzt. </success_criteria>
Create `.planning/quick/260924-i8v-proxmox-kachel-fuers-dashboard/260924-i8v-SUMMARY.md` when done (deutsch, Muster der h7x-Summary: Was gebaut wurde, Abweichungen, Tore mit gemessenen Zahlen, Hinweise für den Browser-Nachweis, „Bewusst offen“: Einstellungsseite zeigt weiterhin nur den ersten Reiter).