525 lines
45 KiB
Markdown
525 lines
45 KiB
Markdown
---
|
||
phase: quick-260924-i8v
|
||
plan: 01
|
||
quick_id: 260924-i8v
|
||
type: execute
|
||
wave: 1
|
||
depends_on: []
|
||
autonomous: true
|
||
requirements: [QUICK-260924-i8v]
|
||
files_modified:
|
||
- 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
|
||
|
||
estimate:
|
||
tokens: 60000
|
||
raw_tokens: 60000
|
||
tasks: 3
|
||
confidence: low
|
||
|
||
must_haves:
|
||
truths:
|
||
- "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"
|
||
artifacts:
|
||
- path: "apps/web/src/components/dashboard/widgets/proxmox-widget.tsx"
|
||
provides: "ProxmoxWidget (WidgetProps) — Balken, Zusammenfassung, Serverliste, Minutentakt, Bearbeitungsmodus"
|
||
- path: "apps/web/src/components/dashboard/widgets/proxmox-widget-model.ts"
|
||
provides: "reine Funktionen: resolveProxmoxWidgetConfig, selectServers, healthSummary, widgetKeyFigure"
|
||
- path: "apps/web/src/components/proxmox/"
|
||
provides: "gemeinsamer Ort für proxmox-status.ts, HealthBar.tsx (mit variant compact), status-styles.ts, proxmox-server-picker.tsx"
|
||
- path: "apps/web/src/components/settings/proxmox-widget-config-form.tsx"
|
||
provides: "Einstellungsformular der Kachel (Titel + Serverauswahl) für Einstellungen > Dashboard"
|
||
- path: "packages/shared/src/index.ts"
|
||
provides: "WIDGET_TYPES enthält 'proxmox', WIDGET_MODULE_SLUGS = { proxmox: 'proxmox' }"
|
||
key_links:
|
||
- from: "packages/shared/src/index.ts WIDGET_MODULE_SLUGS"
|
||
to: "apps/api/src/dashboard/dashboard.service.ts getWidgets (über widget-module-map.ts)"
|
||
via: "getModuleSlugForWidgetType('proxmox') === 'proxmox'"
|
||
pattern: "proxmox: 'proxmox'"
|
||
- from: "apps/web/src/app/(portal)/page.tsx"
|
||
to: "proxmox-widget.tsx"
|
||
via: "registerWidget('proxmox', ProxmoxWidget)"
|
||
pattern: "registerWidget\\('proxmox'"
|
||
- from: "proxmox-widget.tsx"
|
||
to: "apps/web/src/lib/proxmox-api.ts listServers"
|
||
via: "einziger Import aus proxmox-api; Intervall 60 s + visibilitychange"
|
||
pattern: "listServers"
|
||
- from: "proxmox-widget.tsx"
|
||
to: "apps/web/src/components/proxmox/HealthBar.tsx"
|
||
via: "<HealthBar variant=\"compact\" counts=... />"
|
||
pattern: "variant=\"compact\""
|
||
- from: "apps/web/src/components/settings/widget-settings-panel.tsx"
|
||
to: "proxmox-widget-config-form.tsx"
|
||
via: "widget.widgetType === 'proxmox'"
|
||
pattern: "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.
|
||
|
||
<objective>
|
||
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.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@~/.claude/gsd-core/workflows/execute-plan.md
|
||
@~/.claude/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<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`).
|
||
</context>
|
||
|
||
<tasks>
|
||
|
||
<task type="tracer">
|
||
<name>Aufgabe 1: Durchstich — Kacheltyp „proxmox“ von der Typliste über API-Whitelist, Registry und Katalog bis zur Anzeige von Balken und Zusammenfassung</name>
|
||
<files>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</files>
|
||
<action>
|
||
**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.
|
||
</action>
|
||
<verify>
|
||
<automated>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"</automated>
|
||
</verify>
|
||
<done>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): …`.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Aufgabe 2: Serverliste mit Kennzahl je Zeile, Sortierung, Serverfilter, Links, Größenstufen per Container-Query und Minutentakt</name>
|
||
<files>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</files>
|
||
<behavior>
|
||
- 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
|
||
</behavior>
|
||
<action>
|
||
**RED zuerst**: `proxmox-widget-model.test.ts` neu und die Fälle in `proxmox-widget.test.tsx` gemäß
|
||
`<behavior>` 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.
|
||
</action>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<done>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.</done>
|
||
</task>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Aufgabe 3: Titel und Serverauswahl einstellen (an der Kachel im Bearbeitungsmodus und unter Einstellungen > Dashboard), Doku, Changelog, Gesamt-Tore</name>
|
||
<files>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</files>
|
||
<behavior>
|
||
- 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
|
||
</behavior>
|
||
<action>
|
||
**RED zuerst** für Auswahl-Bauteil, Formular, Einstellungsbereich und die neuen Kachel-Fälle gemäß
|
||
`<behavior>`, 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.
|
||
</action>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<done>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.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<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>
|
||
|
||
<verification>
|
||
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.
|
||
</verification>
|
||
|
||
<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>
|
||
|
||
<output>
|
||
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).
|
||
</output>
|