45 KiB
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 — 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_SLUGSinpackages/shared/src/index.ts(die API validiert per@IsIngegen genau diese Liste),registerWidget()in(portal)/page.tsx,visibleWidgetTypes()als Katalogfilter. Eine Kachel mitmoduleSlugverschwindet für Benutzer ohne Modulzugriff automatisch aus Katalog und Dashboard (serverseitigDashboardService.getWidgets, fail-closed). Für „gesperrt“ ist deshalb nichts zu bauen — die Kachel erscheint schlicht nicht;widgets.unavailableim 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-*-fginglobals.css. Diese Teile werden wiederverwendet, nicht kopiert — dafür ziehen sie an einen neutralen Ortapps/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.
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>
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 Messwertenumber | 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), RolleADMINoderSUPER_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]:hiddenerzeugt@container (width < 15rem),[@container(max-height:7.5rem)]:hiddenerzeugt@container (max-height:7.5rem). Der Container ist der Kachelrumpf im Wrapper (@container-size).
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 mitmotion-reduce:animate-none, dazu sr-onlyproxmox.loading. - Laden fehlgeschlagen und noch nie eine Liste: Satz
proxmox.loadError, gedämpft, zentriert. - Leere Liste: Satz
widgets.proxmox.empty; für Admins darunterproxmox.card.settingsLinkals Link auf/modules/proxmox/settings— im Bearbeitungsmodus nur als Text, ohne Link. - Sonst:
HealthBar variant="compact"mitsummarizeHealth(servers, now)und darunter die Zusammenfassung alsp(text-sm font-medium truncate,title= voller Text): beiallOkwidgets.proxmox.allOkinHEALTH_STYLE.ok.text; sonst die Einträge als „Anzahl + Wort ausproxmox.legend.<h>(mitcount)“, mit Komma und Leerzeichen verbunden, inHEALTH_STYLE[worst].text. Die Anzahlokwird 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_TYPESum'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 mitaccessibleModuleSlugs={['proxmox']}=> alle zehn inWIDGET_TYPES-Reihenfolge; mit[]=> neun ohne Proxmox. Die beiden Tests, die bisher vorübergehendclockzur Modul-Kachel gemacht haben, prüfen jetzt die echte Kachel „Proxmox“ (fehlt bei[], erscheint bei['proxmox'], fehlt beinullwä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'überit.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): ….
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 wiepicture-frame-config.ts(siehe<behavior>).selectServers(servers, serverIds)=>{ servers: ProxmoxServer[]; selectionGone: boolean }.widgetKeyFigure(server, now)=> UnterscheidungstypKeyFigure:{ 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: zuerstserverHealth— bei down, idle, orphan immerstatus(ein verwaister Server zeigt keine alten Messwerte, wie in h7x). Bei ok/warn undmetrics === null=>unknown. PVE: im Zustand warn der höchste bekannte Anteil ausnodes[].cpu,ratio(mem, maxmem)undratio(disk, maxdisk)der Speicher (level = meterLevel(fraction)), sonst Gäste; istguestsRunning/guestsStoppedkeine Zahl =>unknown. PBS: die älteste bekanntelastBackupAtü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-semiboldalsh2). Die Eingabe im Bearbeitungsmodus folgt in Aufgabe 3. - Ablauf:
selectServers=> Balken und Zusammenfassung aus der gefilterten Liste =>sortServersByHealth(gefiltert, now)=> Zeilen.selectionGone=> Satzwidgets.proxmox.selectionGonestatt Balken und Liste. - Liste als
ul,min-h-0 flex-1 overflow-y-auto, dazu[@container(max-height:7.5rem)]:hiddenund@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; Punkth-2 w-2 shrink-0 rounded-full+HEALTH_STYLE[h].fill,aria-hidden; Namemin-w-0 flex-1 truncate(bei orphanopacity-70, wie h7x nur den Namen dämpfen); sr-only das Zustandswortproxmox.health.<h>, damit der Zustand nie nur an der Farbe hängt; Kennzahlshrink-0 tabular-nums text-xsrechtsbündig mit@max-[15rem]:hidden(schmale Kachel: nur Punkte und Namen). Wiederholt die Kennzahl nur das Zustandswort (kind: 'status'), ist siearia-hidden. - Kennzahltexte über
useLocale(): guests =>widgets.proxmox.guests; noGuests =>widgets.proxmox.noGuests; load =>widgets.proxmox.loadmitformatPercent; backup =>widgets.proxmox.backupAgomitformatAge(at, now, locale); noBackup =>proxmox.card.pbs.noBackupYet; mailIn =>widgets.proxmox.mailInmitformatCount; status =>proxmox.legend.<h>mitcount: 1; unknown =>proxmox.card.unknownValue. Farben: status =>HEALTH_STYLE[h].text; load =>METER_TEXT[level]; backup mitstale=>HEALTH_STYLE.warn.text; unknown und alle übrigen =>text-muted-foreground. - Ansichtsmodus: jede Zeile ist ein
next/linkauf/modules/proxmoxmithover:bg-muted/60und sichtbarem Fokusring. Bearbeitungsmodus: dieselbe Zeile alsdivohne Ziel und ohne Tabstopp. Bewusste Abweichung vom Favoriten-Muster (dort Anker mit verhindertem Klick): Links stehen im Abbruch-Selektor vondashboard-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;setIntervalruft nur dannlistServersauf, wenndocument.visibilityState !== 'hidden'; einvisibilitychange-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.nowwird 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.
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 inproxmox-widget-model.ts) und dass die gemeinsame Statuslogik seit 260924-i8v unterapps/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> |
Browser-Nachweis — führt der Orchestrator durch, kein Executor-Task (Playwright, lokaler Stack
mit --build, hell UND dunkel, 1400 px Breite):
- Admin: „Widget hinzufügen“ zeigt „Proxmox“ als zehnte Kachel; anlegen → 8×8, kein 400.
- 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.
- Klick auf eine Zeile öffnet
/modules/proxmox; im Bearbeitungsmodus nicht, die Kachel lässt sich über den Zeilen ziehen und vergrößern. - Größenstufen: auf 4 Spalten verkleinern → nur Punkte und Namen; auf 3×4 → nur Balken und Zusammenfassung.
- Bearbeitungsmodus: Titel setzen, „Server auswählen“ → einen Server abwählen → Kachel filtert, nach Neuladen bleibt es so; Einstellungen > Dashboard zeigt dieselbe Auswahl.
- Netzwerk über gut 60 s: nur
GET /modules/proxmox/serversim Minutentakt, keinPOST …/poll; Tab verbergen → kein Abruf. - Benutzer ohne Proxmox-Zugriff: Katalog ohne „Proxmox“, vorhandene Kachel erscheint nicht.
- 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>