diff --git a/.planning/STATE.md b/.planning/STATE.md
index 5a0737b..1377c4c 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -5,9 +5,9 @@ current_phase: 18
current_phase_name: desktop-client-fertigstellen
status: verified
stopped_at: "22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren."
-last_updated: "2026-09-23T15:12:00.000Z"
+last_updated: "2026-09-23T15:30:00.000Z"
last_activity: 2026-09-23
-last_activity_desc: Quick 260923-ku6 — drei Nachbesserungen aus dem Browser-Rundgang zu 260923-dhh (Proxmox-Modul): Verbindungstest prueft jetzt das Formular statt des gespeicherten Stands, ruhiger "noch nicht abgefragt"-Zustand statt Fehlermeldung, Adresse nicht mehr grossgeschrieben; davor Quick 260923-ad9 — Dashboard-Reiter (mehrere Dashboards je Benutzer, Ziehen sortiert, erster ist Standard); davor Freigabe 1.3.1
+last_activity_desc: Quick 260923-dhh — Proxmox-Modul gebaut und gegen einen Nachbau geprueft; drei Befunde in 260923-ku6 behoben, ein Befund der Abnahme (PMG-Teilsumme) offen
state_head: 4d485432c003a6caf68f6d85aff7de0bd27794e2
progress:
total_phases: 18
@@ -465,6 +465,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| 260922-m1h | **Ein Modul bringt seine Dashboard-Kachel jetzt selbst mit (Vorarbeit fuer Proxmox).** Bestandsaufnahme (lesend) hatte ergeben: ein neuer Widget-Typ war an SIEBEN Stellen hartkodiert (Union-Typ, Constraints, Registry, eigene `wireXWidget()` je Typ, Aufruf in page.tsx, zweite Liste im Katalogfenster, `@IsIn` im API-DTO); die Verbindung Kachel↔Modul existierte als `WIDGET_MODULE_MAP` in `dashboard.service.ts` (filtert fail-closed), war aber nie befuellt; der Katalog zeigte jedem alle Kacheln, auch die gesperrter Module. Umbau: `WIDGET_TYPES`/`WidgetType`/`WIDGET_MODULE_SLUGS` in `packages/shared` als EINE Quelle (API validiert per `@IsIn` gegen genau sie), ein generisches `registerWidget()` statt neun Funktionen, Katalog leitet seine Liste aus der Registry ab und filtert ueber `/modules/active` (fail-closed bei Fehler, reine Funktion `visibleWidgetTypes`), nicht verfuegbare Kachel zeigt `widgets.unavailable` statt leer zu bleiben. Deckungsgleichheits-Test faengt kuenftig jede vergessene Stelle. **Befund des Executors, geprueft statt vermutet:** `apps/web` hatte KEINE Abhaengigkeit auf `@tessera/shared` (frueher bewusst) — vor der Umsetzung nachgemessen, dass Bau und Produktions-Abbild das tragen (node:24-alpine strippt die Typen nativ); Folgeregel „nur loeschbare Syntax in shared“ steht als Warnung in der Datei. Verhalten der neun Kacheln unveraendert, im Browser bestaetigt (Reihenfolge, Anlegen, Entfernen, keine rohen Schluessel). Bewusst offen: der Einstellungs-Zweig je Typ in `widget-settings-panel.tsx` und die Live-Aktualisierung des Katalogs. **Zahlen:** api 1188 → 1202, web 640 → 659, type-check 4/4, lint 5/5 (74/53 wie Basis). | 2026-09-22 | 56c07c3,8be0725 | [260922-m1h-dashboard-widgets-ein-modul-bringt-seine](./quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/) |
| 260922-vdk | **Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus.** Meldung des Nutzers aus dem **Linux-Client**: rechts neben dem Kalender freie Flaeche, in die sich keine Kachel schieben laesst — „als ob es keinen Anker gibt“. Aus dem Bildschirmfoto zurueckgerechnet (Spaltenbreite 51,5 px, Platzhalter auf Spalte 13 = letzte moegliche, Rasterende bei x=1459 bei ~1660 px Inhaltsbreite): das Raster rechnete mit **1200 px** statt mit der echten Breite, rechts blieben ~460 px totes Feld. Ursache: die Breitenmessung hing in `useEffect(..., [])` mit `if (!containerRef.current) return` — haengt `DashboardGrid` mit NULL Kacheln ein, rendert der fruehe Ruecksprung in den Leerzustand den gemessenen `
` gar nicht, der Effekt bricht ab und laeuft nie wieder, auch nicht wenn spaeter die erste Kachel entsteht. `width` blieb die ganze Sitzung auf dem Startwert 1200; react-grid-layout vergleicht strikt (`width > breakpoint`), 1200 ist damit `md` (20 Spalten, 51,6 px) statt `lg`. Fix: Ref-Rueckruf `measureRef` statt Einmal-Effekt — folgt dem Knoten ueber den Wechsel Leerzustand ↔ gefuellt, misst synchron in der Commit-Phase, haengt den ResizeObserver dort an; Fenster-Horcher als zusaetzliches Netz; `applyWidth` verwirft 0 und nicht endliche Werte. **Verhalten sonst unveraendert** — belegte Plaetze bleiben gesperrt, nichts weicht aus (Ansage des Nutzers). **Geprueft im echten Client**, nicht im Browser: `Tessera-1.3.0.AppImage` auf `DISPLAY=:10` ueber den WebKit-Remote-Inspektor gesteuert. Gleicher Fehlerfall vorher/nachher: Kachel 469 px → **389 px** bei 1000 px Bereich, Ziehen endet jetzt bei 603 px = `1000 − 8 − 389`, exakt der rechte Rand. **Messfalle notiert:** im Client gegen `style.width`/`style.transform` messen, nie gegen `getBoundingClientRect()` — bei Fenster im Hintergrund friert WebKitGTK die Animationsuhr ein und der `width`-Uebergang bleibt auf dem alten Wert stehen. **Zahlen:** web 659 → 661 Tests, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. | 2026-09-22 | d9f2af3,cf67c8a | [260922-vdk-dashboard-raster-misst-seine-breite-nich](./quick/260922-vdk-dashboard-raster-misst-seine-breite-nich/) |
| 260923-ad9 | **Dashboard-Reiter: mehrere Dashboards je Benutzer.** Wunsch des Nutzers (23.09.): mehrere Dashboards als Reiter, per Ziehen sortierbar, der erste ist der Standard und wird beim Oeffnen geladen; „als Favorit festlegen“ = nach vorn ziehen, kein zusaetzliches Kennzeichen. Umsetzung in 5 Schritten: neues Modell `Dashboard` (userId, tenantId, name, position) mit RLS wie die Nachbartabellen; `WidgetInstance.dashboardId` und `DashboardLayout.dashboardId @unique` — Kacheln und Anordnung haengen jetzt am Reiter statt am Benutzer. Handgeschriebene Migration `20260923120000_dashboard_tabs` haengt den Bestand um: Bestandsuebernahme VOR `NOT NULL`/Fremdschluessel, danach 0 verwaiste Kacheln, 0 verwaiste Anordnungen, je Benutzer genau ein Reiter auf Position 0. Fuenf Endpunkte unter `/dashboard/tabs`; `assertOwnedDashboard` laeuft als erstes in JEDEM Lese- und Schreibweg und antwortet fuer „gibt es nicht“, „Kollege“ und „fremder Mandant“ identisch (kein Orakel) — acht eigene Tests dafuer. Umsortieren und Loeschen je EINE Transaktion nach dem Muster `FavoritesService.reorder`. Riegel: 20 Reiter, 40 Zeichen, 20 Kennungen je Anfrage. Ziehen per Pointer-Ereignissen ohne neue Abhaengigkeit (Muster xframe-Ausschnitt), ausserhalb des Bearbeitungsmodus moeglich, weil „nach vorn ziehen“ das Festlegen des Standards IST; Umbenennen und Loeschen bleiben im Bearbeitungsmodus, Loeschen mit `alertdialog`-Rueckfrage. **Raster unangetastet** (`FREE_PLACEMENT_COMPACTOR`/`preventCollision` und die Breitenmessung aus 260922-vdk) — vom Verifizierer per `git diff` nachgewiesen. **Rundgang mit zwoelf Punkten bestanden** (Bestand 5 Kacheln erhalten, Reiter leer angelegt, Kacheln je Reiter getrennt, Ziehen ordnet um, nach Neuladen kommt der erste Reiter, Umbenennen, Loeschen mit Rueckfrage, letzter Reiter ohne Loeschknopf, Kachelbreite 531 px bei 1625 px Bereich). **Kleiner Befund, offen:** die Knopf-Beschriftungen nennen den betroffenen Reiter nicht (nur das Bestaetigungsfenster tut es). **Zahlen:** api 1202 → 1240 Tests, web 661 → 693, type-check 4/4, lint 5/5 mit 53 Warnungen unveraendert, `migrate diff` ohne Unterschied. | 2026-09-23 | 9c51823,df7a5e7,d34f682,05feaa3,58ce88e | [260923-ad9-dashboard-reiter-mehrere-dashboards-je-b](./quick/260923-ad9-dashboard-reiter-mehrere-dashboards-je-b/) |
+| 260923-dhh | **Proxmox-Modul (PVE, PBS, PMG) — nur beobachten.** Sieben Aufgaben: Tabellen `ProxmoxServer`/`ProxmoxServerStatus` mit RLS, Zugang verschluesselt per `CryptoService`, undici-Klient mit Dispatcher nur fuer die eingetragene Adresse, Zwischenlager statt Live-Abfrage, Hintergrunddienst je Mandant (`onApplicationBootstrap`, Tender-Muster), Einstellungsseite mit Verbindungstest, Modulseite, Doku. Zugang wahlweise API-Token oder Benutzer/Passwort; **PMG nur Passwort** (Recherche A1: PMG kennt offenbar keine Token). Kopfzeilen-Formate unterscheiden sich je Produkt (`PVEAPIToken=…=…` vs. `PBSAPIToken=…:…`) und liegen an EINER Stelle. **Riegel „nur lesen“ maschinell erzwungen:** `proxmox-nur-lesen.spec.ts` zaehlt die nicht-lesenden Aufrufe gegen eine benannte Konstante — einzige Ausnahme ist die Ticket-Anmeldung. **Keine SSRF-Adresssperre** (Proxmox steht per Definition im internen Netz, eine Sperre wuerde jede echte Adresse blockieren) — Schutz ist, dass nur ein Administrator Adressen eintraegt. **Rundgang gegen einen selbst gebauten Proxmox-Nachbau** (HTTPS, selbstsigniert, echte Antwortformen): Modul im Marktplatz freigeben, Server anlegen, Zertifikatsfehler korrekt benannt, nach gesetzter Ausnahme „Verbindung erfolgreich“, Zahlen der Modulseite exakt wie im Nachbau (18/42 % Last, 3 laufend / 1 gestoppt), unerreichbarer Server meldet „Der Server ist nicht erreichbar“. **Drei Befunde daraus in 260923-ku6 behoben.** **Ein Befund der Abnahme OFFEN:** `sumOrNull` in `normalizePmg` liefert bei EINEM fehlenden Teilwert die halbe Summe statt `null` — stiller Falschwert genau dort, wo die Feldnamen am schlechtesten belegt sind. **Zahlen:** api 1240 → 1311 Tests, web 693 → 708, type-check 4/4, lint 5/5, 53 Warnungen unveraendert. | 2026-09-23 | 3a1bfd9,4f8a368,998aba9,fccaf8d,723cf68,06fcdc0,3091b04 | [260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n](./quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/) |
+| 260923-ku6 | **Drei Befunde aus dem Proxmox-Rundgang behoben.** (1) „Verbindung testen“ pruefte den GESPEICHERTEN Stand statt der Eingabe — wer den Zugang tippt und vor dem Speichern testet, bekam die Antwort zum alten Wert; jetzt eigene Route `POST servers/test` mit Merge-Regel: normale Felder folgen dem Formular (auch geleert), Geheimnisfelder folgen „leer → gespeicherten Wert behalten“, weil das Formular Geheimnisse nie vorbefuellt. (2) Ein frisch angelegter Server zeigte „Ein unerwarteter Fehler ist aufgetreten“, obwohl nur noch nichts abgefragt war — jetzt eigener ruhiger Zustand mit Verweis auf „Jetzt aktualisieren“. (3) Die Klasse `uppercase` faerbte die ganze Zeile und zeigte die Adresse als „HTTPS://…“ — jetzt nur noch das Produktkuerzel. **Zahlen:** api 1311 → 1320, web 708 → 714, 53 Warnungen gehalten (eine neu ausgeloeste `useOptionalChain`-Warnung gleich mit aufgeloest). | 2026-09-23 | 710034c,f1bb7f7 | [260923-ku6-drei-nachbesserungen-aus-dem-browser-run](./quick/260923-ku6-drei-nachbesserungen-aus-dem-browser-run/) |
| 260923-ku6 | **Drei Nachbesserungen aus dem Browser-Rundgang zu 260923-dhh (Proxmox-Modul).** Befund 1 (wichtig): „Verbindung testen" pruefte den gespeicherten Server statt des Formulars — im Formular abgeschaltete Zertifikatspruefung oder ein neu eingetipptes Geheimnis griffen erst nach dem Speichern. Fix: neues `TestProxmoxServerDto` + Merge-Baustein `resolveEffectiveTestServer` in `ProxmoxService`, neue Route `POST servers/test` fuer die Neuanlage (noch kein gespeicherter Server), Geheimnisfelder behalten die bestehende „leer gelassen -> gespeicherten Wert weiterverwenden"-Regel. Befund 2 (wichtig): ein frisch angelegter, nie abgefragter Server zeigte faelschlich „Ein unerwarteter Fehler ist aufgetreten" statt eines ruhigen Hinweises — behoben ueber `status.lastPolledAt === null`. Befund 3 (kosmetisch): `uppercase` faerbte die ganze Statuszeile inkl. Adresse gross — jetzt nur noch das Produktkuerzel. **Zahlen:** api 1311 → 1316, web 708 → 712, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. | 2026-09-23 | 710034c,f1bb7f7 | [260923-ku6-drei-nachbesserungen-aus-dem-browser-run](./quick/260923-ku6-drei-nachbesserungen-aus-dem-browser-run/) |
## Deferred Items
diff --git a/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md
new file mode 100644
index 0000000..a135214
--- /dev/null
+++ b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md
@@ -0,0 +1,295 @@
+---
+phase: quick-260923-dhh
+plan: 01
+subsystem: infrastructure
+tags: [proxmox, pve, pbs, pmg, undici, scheduler, rls, module-registry, nestjs, next-intl]
+dependency-graph:
+ requires: []
+ provides: [proxmox-module, proxmox-server-model, proxmox-background-poller]
+ affects: [apps/api/src/proxmox, apps/web/src/app/(portal)/modules/proxmox, apps/web/src/lib/proxmox-api.ts]
+tech-stack:
+ added: []
+ patterns:
+ - "undiciFetch statt globalem fetch fuer einen bedingten TLS-Dispatcher (zweites, unabhaengiges Auftreten nach icon-discovery.service.ts)"
+ - "Nur-Lese-Riegel per Quelltext-Analyse (proxmox-nur-lesen.spec.ts), Vorbild rls-access-inventory.spec.ts"
+ - "Scheduler kombiniert DkvSchedulerService-Mandanten-Fan-out mit TenderSchedulerService-onApplicationBootstrap-Timing"
+ - "select ohne Geheimnisfelder statt nachtraeglicher Maskierung"
+key-files:
+ created:
+ - apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql
+ - apps/api/src/proxmox/proxmox.types.ts
+ - apps/api/src/proxmox/proxmox-auth.ts
+ - apps/api/src/proxmox/proxmox-client.service.ts
+ - apps/api/src/proxmox/proxmox-normalize.ts
+ - apps/api/src/proxmox/proxmox.service.ts
+ - apps/api/src/proxmox/proxmox-scheduler.service.ts
+ - apps/api/src/proxmox/proxmox.controller.ts
+ - apps/api/src/proxmox/proxmox.module.ts
+ - apps/api/src/proxmox/proxmox.seed.ts
+ - apps/api/src/proxmox/dto/proxmox-server.dto.ts
+ - apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
+ - apps/web/src/app/(portal)/modules/proxmox/page.tsx
+ - apps/web/src/app/(portal)/modules/proxmox/layout.tsx
+ - apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
+ - apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx
+ - apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx
+ - apps/web/src/lib/proxmox-api.ts
+ modified:
+ - apps/api/prisma/schema.prisma
+ - apps/api/src/app.module.ts
+ - apps/api/src/prisma/rls-access-inventory.spec.ts
+ - apps/web/src/lib/module-loader.ts
+ - apps/web/src/messages/de.json
+ - apps/web/src/messages/en.json
+ - apps/web/src/messages/umlaut-dictionary.ts
+ - docs/mandantentrennung-zugriffsklassifikation.md
+ - docs/anleitung-entwicklung.md
+ - docs/anleitung-anwender.md
+decisions:
+ - "D-01 bis D-11 aus dem Plan woertlich umgesetzt, keine Abweichung."
+ - "proxmoxGet uebergibt bewusst KEIN method-Feld an undiciFetch (GET ist der Grundwert) — dadurch ist loginTicket() in proxmox-auth.ts die einzige Stelle, die ein Anfrageverfahren explizit uebergibt, und proxmox-nur-lesen.spec.ts kann das maschinell auf genau EINS pruefen."
+ - "Ticket-Erneuerung sitzt je POLL-DURCHLAUF, nicht je Aufruf: ein PBS-Durchlauf mit mehreren Folgeabfragen (Belegung + je Datenspeicher Sicherungen) loggt sich bei 401 hoechstens einmal neu ein, nicht einmal je Anfrage."
+ - "proxmox.service.ts ist der EINZIGE forSystem()-Aufrufer des Moduls (loadActiveServersForScheduler) — in FORSYSTEM_ALLOWED_CALL_SITES eingetragen, Stand von ProxmoxServer auf system-gebunden gehoben."
+ - "docs/anwenderhandbuch.md aus dem Plan existiert nicht im Repo — der echte Dateiname ist docs/anleitung-anwender.md; dort den Proxmox-Abschnitt eingefuegt (Rule 3)."
+metrics:
+ duration: "~5h (Session unterbrochen und fortgesetzt)"
+ completed: 2026-09-23
+actuals:
+ tokens: 50829
+ tasks: 7
+ commits: 7
+plan_head_before: ec9c779
+status: complete
+---
+
+# Quick 260923-dhh: Proxmox-Modul (PVE/PBS/PMG) — nur beobachten Summary
+
+Vollstaendiges Proxmox-Modul (Datenbank, Dienst, API, Hintergrundabfrage je Mandant, Einstellungsseite, Modulseite, Dokumentation) — PVE/PBS/PMG werden per API-Token (PVE/PBS) oder Ticket-Anmeldung (alle drei) nur gelesen, kein Weg im Modul veraendert je etwas bei Proxmox.
+
+## Gemessene Torzahlen
+
+| Tor | Ausgangswert (23.09., vor Beginn) | Endstand (nach Aufgabe 7) |
+|---|---|---|
+| `pnpm --filter @tessera/api test` | 1240 Tests, 77 Dateien | **1311 Tests, 82 Dateien** |
+| `pnpm --filter @tessera/web test` | 693 Tests, 82 Dateien | **708 Tests, 84 Dateien** |
+| `rls-coverage.spec.ts` / `rls-access-inventory.spec.ts` | 5 / 30 | **5 / 30** (unveraendert gruen) |
+| `proxmox-nur-lesen.spec.ts` | (existierte nicht) | **2 Tests, gruen** |
+| `pnpm type-check` | 4/4 | **4/4** |
+| `pnpm lint` | 5/5 | **5/5** |
+| Biome-Warnungen in `apps/web` | 53 | **53** (exakt unveraendert) |
+
+## Performance
+
+- **Duration:** ~5h (inklusive einer Unterbrechung durch Nutzungslimit, an derselben Stelle fortgesetzt)
+- **Tasks:** 7/7
+- **Files modified:** 34 (18 neu, 16 geaendert)
+
+## Accomplishments
+
+- `ProxmoxServer`/`ProxmoxServerStatus` mit RLS (`tenant_isolation_policy` auf beiden,
+ `system_read_policy` zusaetzlich auf `ProxmoxServer` fuer den Planer-Startpfad)
+- `proxmox-auth.ts` als einzige Stelle, die Kopfzeilen/Cookies baut: Token-Schema je Produkt
+ (PVE `=`, PBS `:`, PMG lehnt ab) und Ticket-Anmeldung (die einzige nicht-lesende Anfrage
+ des Moduls)
+- `proxmox-client.service.ts`/`proxmox-normalize.ts`: nachsichtige Fehler-/Feldbehandlung,
+ sieben stabile Fehlerschluessel, nie ein Wurf bei unerwarteter Form
+- `proxmox-scheduler.service.ts`: ein Cron-Auftrag je Mandant (`proxmox-poll:`),
+ `onApplicationBootstrap`, Abfrageintervall = kleinstes `pollIntervalMin` der aktiven Server
+- Einstellungsseite (anlegen/bearbeiten/loeschen/testen) und Modulseite (Serverliste mit
+ produktabhaengiger Auslastung, `null` immer als „unbekannt")
+- Anwenderhandbuch- und Entwicklungsanleitung-Abschnitte, Zugriffsklassifikation vollstaendig
+ nachgezogen
+
+## Task Commits
+
+Jede Aufgabe wurde einzeln committet:
+
+1. **Aufgabe 1: PVE per Token, Ende-zu-Ende** — `3a1bfd9` (feat)
+2. **Aufgabe 2: Benutzer/Passwort, Fehlerklassen, Nur-Lesen-Riegel** — `4f8a368` (test)
+3. **Aufgabe 3: PBS und PMG auswerten** — `998aba9` (feat)
+4. **Aufgabe 4: Hintergrundabfrage je Mandant, Verbindungstest** — `fccaf8d` (feat)
+5. **Aufgabe 5: Einstellungsseite (anlegen, bearbeiten, loeschen, testen)** — `723cf68` (feat)
+6. **Aufgabe 6: Modulseite mit Auslastung** — `06fcdc0` (feat)
+7. **Aufgabe 7: Dokumentation und Nachmessung aller Tore** — `3091b04` (docs)
+
+_Kein separater Metadaten-Commit — STATE.md/SUMMARY.md werden laut Auftrag nicht committet._
+
+## Files Created/Modified
+
+Siehe `key-files` im Frontmatter — vollstaendige Liste, hier die wichtigsten:
+
+- `apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql` — RLS-Migration,
+ von Hand geschrieben (Vorbild `20260923120000_dashboard_tabs`)
+- `apps/api/src/proxmox/proxmox-client.service.ts` — `proxmoxGet`, `classifyFailure`,
+ `parseJsonLenient`
+- `apps/api/src/proxmox/proxmox-auth.ts` — `buildTokenAuthHeader`, `loginTicket`,
+ `buildTicketCookieHeader`
+- `apps/api/src/proxmox/proxmox-normalize.ts` — `normalizePve`/`normalizePbs`/`normalizePmg`
+ plus `readNumber`/`readText`/`readBool`/`readList`
+- `apps/api/src/proxmox/proxmox.service.ts` — CRUD, Poll-Logik, Zehn-Sekunden-Sperre,
+ `loadActiveServersForScheduler` (einziger `forSystem()`-Aufruf)
+- `apps/api/src/proxmox/proxmox-scheduler.service.ts` — Planer je Mandant
+- `apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx` — einzige Stelle,
+ die einen Messwert in Text verwandelt
+
+## Decisions Made
+
+Siehe `decisions` im Frontmatter. Zusaetzlich zwei technische Entwurfsentscheidungen, die der
+Plan nicht bis auf diese Ebene vorschrieb:
+
+- **Signatur `proxmoxGet(target, path)`:** `target` traegt fertige Kopfzeilen
+ (`{ baseUrl, tlsRejectUnauthorized, headers }`), gebaut ausschliesslich von `proxmox-auth.ts`
+ — der Klient selbst kennt keine Anmeldeform, nur HTTP-Transport und Fehlerklassifikation.
+- **`proxmox-nur-lesen.spec.ts` erkennt Aufrufformen ueber Klammertiefen-Bilanzierung**
+ (nicht per einfachem Zeilen-Regex), weil Proxmox-Pfade und `proxmoxGet(`/`getWithRetry(`-
+ Aufrufe im Quelltext ueber mehrere Zeilen verteilt sind.
+
+## Deviations from Plan
+
+### Auto-fixed Issues
+
+**1. [Rule 3 - Blocking] `docs/anwenderhandbuch.md` existiert nicht im Repo**
+- **Found during:** Aufgabe 7
+- **Issue:** Das Plan-Frontmatter nennt `docs/anwenderhandbuch.md` als zu aendernde Datei; diese
+ Datei gibt es im Repository nicht. Der tatsaechliche Anwenderhandbuch-Dateiname ist
+ `docs/anleitung-anwender.md` (bestaetigt per `git log --diff-filter=A`).
+- **Fix:** Den Proxmox-Abschnitt in `docs/anleitung-anwender.md` eingefuegt statt eine neue,
+ falsch benannte Datei anzulegen.
+- **Files modified:** `docs/anleitung-anwender.md`
+- **Verification:** Datei existiert, Abschnitt „Proxmox" lesbar, Modulzahl „vier" auf „fuenf"
+ korrigiert.
+- **Committed in:** `3091b04` (Aufgabe-7-Commit)
+
+**2. [Rule 3 - Blocking] Umlaut-Regressionswaechter (`umlaut-guard.spec.ts`) schlug fehl**
+- **Found during:** Aufgabe 5 und erneut Aufgabe 6
+- **Issue:** Neue, bereits korrekte deutsche Woerter mit „ss" (`bewusst`, `gemessene`,
+ `Messung`, `Prozessorlast`) in den neuen `de.json`-Texten wurden vom Waechter als
+ moegliche ae/oe/ue/ss-Ersatzschreibung markiert, weil sie noch nicht auf der Positivliste
+ standen.
+- **Fix:** Alle vier Woerter zu `UMLAUT_ALLOWLIST` in `apps/web/src/messages/umlaut-dictionary.ts`
+ hinzugefuegt (kein Ersatzschreibung — bereits korrektes Deutsch).
+- **Files modified:** `apps/web/src/messages/umlaut-dictionary.ts`
+- **Verification:** `umlaut-guard.spec.ts` gruen, `pnpm --filter @tessera/web test` vollstaendig
+ gruen.
+- **Committed in:** `723cf68` (Aufgabe 5), `06fcdc0` (Aufgabe 6)
+
+**3. [Rule 3 - Blocking] `proxmox-nur-lesen.spec.ts` erkannte den `getWithRetry`-Umschlag nicht**
+- **Found during:** Aufgabe 3 (beim Einbau der PBS-Mehrfachabfrage)
+- **Issue:** Der urspruengliche Riegel erkannte Proxmox-Pfade nur innerhalb direkter
+ `proxmoxGet(...)`-Aufrufe; nach der Extraktion der Ticket-Erneuerung in einen privaten
+ Umschlag `getWithRetry()` (Aufgabe 2/3) lagen alle Pfade jetzt in dessen Argumenten, nicht
+ mehr direkt in `proxmoxGet(...)`.
+- **Fix:** Die erlaubte Aufrufform-Liste um `getWithRetry` erweitert (dokumentierte Ausnahme,
+ selbst durch dieselbe erste Aussage des Riegels abgesichert: `getWithRetry` ruft
+ ausschliesslich `proxmoxGet`).
+- **Files modified:** `apps/api/src/proxmox/proxmox-nur-lesen.spec.ts`
+- **Verification:** Beide Aussagen des Riegels gruen, bewusster Test bestaetigt weiterhin genau
+ eine `undiciFetch`-Methodenstelle.
+- **Committed in:** `998aba9` (Aufgabe 3)
+
+---
+
+**Total deviations:** 3 auto-fixed (alle Rule 3 — blockierende Fehler beim Ausfuehren, keine
+davon eine architektonische Entscheidung)
+**Impact on plan:** Keine Abweichung vom fachlichen Umfang des Plans; alle drei Korrekturen
+waren notwendig, damit die vom Plan selbst verlangten Tore (Aufgabe 7: alle Testsuiten gruen)
+ueberhaupt erreichbar waren.
+
+## Issues Encountered
+
+Die Ausfuehrung wurde durch ein Nutzungslimit mitten in Aufgabe 4 unterbrochen (nach dem
+Schreiben von `proxmox-scheduler.service.ts` und dem Wiring in `proxmox.controller.ts`/
+`proxmox.module.ts`, vor dem Schreiben der zugehoerigen Testdatei). Nach Fortsetzung wurde der
+Stand anhand von `git status`/`git log` verifiziert und exakt an der protokollierten Stelle
+weitergearbeitet — keine Wiederholung bereits committeter Aufgaben.
+
+## User Setup Required
+
+**Es gibt in dieser Umgebung keinen echten PVE-/PBS-/PMG-Server.** Alle Tests laufen gegen
+erfundene Antworten in der von der Recherche dokumentierten Form (`vi.mock('undici', …)`).
+Folgende Annahmen der Recherche sind vor dem ersten echten Test explizit zu bestaetigen bzw.
+bei Abweichung an genau einer Stelle nachzuziehen:
+
+- **Annahme A2 — Ticket-Cookie-Namen fuer PBS/PMG:** `PBSAuthCookie`/`PMGAuthCookie` sind aus
+ dem PVE-Muster ABGELEITET, nicht aus Primaerdoku bestaetigt. Nachzuziehende Stelle:
+ `TICKET_COOKIE_NAME` in `apps/api/src/proxmox/proxmox-auth.ts`.
+- **Annahme A3 — PBS-Belegungs-/Snapshot-Feldnamen:** `store`/`total`/`used`/`avail` und
+ `backup-time`/`verification` sind aus Forenbelegen abgeleitet. Nachzuziehende Stelle:
+ `PBS_USAGE_FIELDS`/`PBS_SNAPSHOT_FIELDS` in `apps/api/src/proxmox/proxmox-normalize.ts`
+ (mehrere plausible Namen je Feld moeglich, der Leser nimmt den ersten vorhandenen).
+- **Annahme A5 — PMG-Statistikfelder:** `count_in`/`count_out`/`spamcount_in`/`spamcount_out`/
+ `viruscount_in`/`viruscount_out` sind aus `pmgsh`-Community-Belegen abgeleitet.
+ Nachzuziehende Stelle: `PMG_STATS_FIELDS` in `apps/api/src/proxmox/proxmox-normalize.ts`.
+- **NUR-LESE-Rollen am Proxmox-Server selbst anlegen** (aus dem Plan-Frontmatter
+ `user_setup`, unveraendert offen): PVE `PVEAuditor`, PBS `Audit`/`DatastoreAudit`,
+ PMG `Auditor` — je Produkt fuer den Zugang, den Tessera nutzt.
+
+Weicht die Wirklichkeit an einer dieser Stellen ab, zeigt die Modulseite dank der
+nachsichtigen Leser „unbekannt" statt eines Absturzes, und die gekuerzte Rohantwort bleibt im
+Zwischenlager erhalten (`rawSample`, bis 20 000 Zeichen) — der Nutzer sieht darin, wie das
+Feld tatsaechlich heisst.
+
+## Known Stubs
+
+Keine — jede in `` genannte Wahrheit ist durch mindestens einen automatisierten
+Test belegt (siehe Aufgaben 1–6). Die drei oben genannten Annahmen sind keine Stubs, sondern
+dokumentierte, noch nicht am echten Server bestaetigte Feldnamen — die Auswertung fuer sie ist
+vollstaendig gebaut, nur ihre exakten externen Namen sind ungeprueft.
+
+## Next Phase Readiness
+
+- Das Modul ist vollstaendig gebaut und alle automatisierten Tore sind gruen; die
+ Dashboard-Kachel (D-11) ist bewusst nicht Teil dieses Auftrags und folgt separat
+ (`WIDGET_TYPES`/`WIDGET_MODULE_SLUGS`/`registerWidget`, siehe
+ `docs/anleitung-entwicklung.md`, Abschnitt „Eine Kachel zum Modul").
+- **Blocker fuer den naechsten Schritt:** keiner auf Code-Ebene. Der Nutzer muss das Modul
+ gegen mindestens einen echten PVE-/PBS-/PMG-Server pruefen (siehe „User Setup Required"),
+ bevor die drei Annahmen als bestaetigt gelten koennen.
+- Container wurden in dieser Ausfuehrung bewusst NICHT neu gebaut/neu gestartet und es wurde
+ keine Browser-Pruefung durchgefuehrt (Auftragsvorgabe) — das uebernimmt der Nutzer bzw. eine
+ spaetere Sitzung.
+
+---
+*Phase: quick-260923-dhh*
+*Completed: 2026-09-23*
+
+## Self-Check: PASSED
+
+All 24 files listed under `key-files` (created + modified) verified present on disk. All 7
+task commits (`3a1bfd9`, `4f8a368`, `998aba9`, `fccaf8d`, `723cf68`, `06fcdc0`, `3091b04`)
+verified present in `git log`.
+
+## Nachbesserungen aus dem Rundgang
+
+Drei Befunde aus dem menschlichen Browser-Rundgang zu diesem Modul wurden behoben — Details,
+Tasks und Tests in einem eigenen Quick-Task:
+[260923-ku6-drei-nachbesserungen-aus-dem-browser-run](../260923-ku6-drei-nachbesserungen-aus-dem-browser-run/260923-ku6-SUMMARY.md)
+(Commits `710034c`, `f1bb7f7`).
+
+**Befund 1 (wichtig): „Verbindung testen" pruefte den gespeicherten Stand, nicht das
+Formular.** Eine im Formular abgeschaltete Zertifikatspruefung oder ein neu eingetipptes
+Token-/Passwort-Geheimnis wurden vom Test ignoriert und griffen erst nach „Speichern" — eine
+Falle fuer den naheliegenden Ablauf (eintippen, testen, dann erst speichern). Behoben durch ein
+neues `TestProxmoxServerDto` samt Merge-Baustein `resolveEffectiveTestServer` in
+`ProxmoxService`: normale Formularfelder gewinnen immer (auch wenn absichtlich geleert),
+Geheimnisfelder behalten die bestehende „leer gelassen -> gespeicherten Wert weiterverwenden"-
+Regel aus `updateServer`, weil `ServerForm` sie beim Laden nie aus der Datenbank vorbefuellt.
+Neue Route `POST servers/test` (ohne `:id`) deckt denselben Test waehrend der Neuanlage ab, wo
+es noch keinen gespeicherten Server gibt; der Testen-Knopf steht jetzt immer zur Verfuegung,
+nicht mehr erst nach dem ersten Speichern.
+
+**Befund 2 (wichtig): falsche Meldung fuer „noch nie abgefragt".** Ein frisch angelegter
+Server zeigte „Letzte Abfrage: unbekannt" UND faelschlich „Ein unerwarteter Fehler ist
+aufgetreten" — die leere Zwischenlagerzeile aus `createServer` hat `reachable: false` und
+`errorKind: null`, was bisher blind in die Fehleruebersetzung `unbekannt` lief. Behoben durch
+einen eigenen, ruhigen Zustand fuer `status.lastPolledAt === null`, der auf „Jetzt
+aktualisieren" verweist; die bestehenden Fehlermeldungen (inkl. `unbekannt` fuer echte
+unbekannte Fehler) bleiben fuer bereits abgefragte, aber nicht erreichbare Server unveraendert.
+
+**Befund 3 (kosmetisch): die Adresse wurde in Grossbuchstaben angezeigt.** Die Klasse
+`uppercase` sass auf der ganzen Statuszeile statt nur auf dem Produktkuerzel und faerbte
+dadurch auch die Adresse gross. Jetzt nur noch auf dem Produktkuerzel (``).
+
+**Zahlen nach der Nachbesserung:** api 1311 → 1316 Tests, web 708 → 712 Tests, type-check
+4/4, lint 5/5, Biome `apps/web` weiterhin exakt 53 Warnungen. Container wurden nicht neu
+gebaut, keine Browser-Pruefung in diesem Lauf (macht der Orchestrator danach).
diff --git a/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-VERIFICATION.md b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-VERIFICATION.md
new file mode 100644
index 0000000..b9b520e
--- /dev/null
+++ b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-VERIFICATION.md
@@ -0,0 +1,126 @@
+---
+phase: quick-260923-dhh
+verified: 2026-09-23T14:58:00Z
+status: gaps_found
+score: 8/9 must-haves verified
+covered_files: [".planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md", ".planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-RESEARCH.md", ".planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md", "apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql", "apps/api/prisma/schema.prisma", "apps/api/src/prisma/rls-access-inventory.spec.ts", "apps/api/src/proxmox/dto/proxmox-server.dto.ts", "apps/api/src/proxmox/proxmox-auth.ts", "apps/api/src/proxmox/proxmox-client.service.spec.ts", "apps/api/src/proxmox/proxmox-client.service.ts", "apps/api/src/proxmox/proxmox-normalize.spec.ts", "apps/api/src/proxmox/proxmox-normalize.ts", "apps/api/src/proxmox/proxmox-nur-lesen.spec.ts", "apps/api/src/proxmox/proxmox-scheduler.service.spec.ts", "apps/api/src/proxmox/proxmox-scheduler.service.ts", "apps/api/src/proxmox/proxmox.controller.ts", "apps/api/src/proxmox/proxmox.module.ts", "apps/api/src/proxmox/proxmox.seed.ts", "apps/api/src/proxmox/proxmox.service.spec.ts", "apps/api/src/proxmox/proxmox.service.ts", "apps/api/src/proxmox/proxmox.types.ts", "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx", "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx", "apps/web/src/app/(portal)/modules/proxmox/layout.tsx", "apps/web/src/app/(portal)/modules/proxmox/page.tsx", "apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx", "apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx", "apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx", "apps/web/src/lib/module-loader.ts", "apps/web/src/lib/proxmox-api.ts", "apps/web/src/messages/de.json", "apps/web/src/messages/en.json", "apps/web/src/messages/umlaut-dictionary.ts", "docs/anleitung-anwender.md", "docs/anleitung-entwicklung.md", "docs/mandantentrennung-zugriffsklassifikation.md"]
+covered_digest: "v1:sha256:2ee19956636c68304958254f2e1d979a6763c3fae7dc11cd781fe24d56e498e8"
+behavior_unverified: 0
+overrides_applied: 0
+gaps:
+ - truth: "Ein fehlendes, anders benanntes oder falsch typisiertes Feld einer Proxmox-Antwort fuehrt zu unbekannt in der Anzeige, nie zu einem Absturz, einer leeren Seite oder einem stillen Falschwert."
+ status: partial
+ reason: "normalizePmg() kombiniert spamcount_in/spamcount_out (und viruscount_in/viruscount_out) ueber sumOrNull(a, b), das einen fehlenden Teilwert stillschweigend als 0 behandelt statt die Summe als unbekannt zu markieren. sumOrNull(10, null) liefert 10 — dieser Wert erscheint in der Modulseite als vollstaendige Tageszahl 'Spam: 10', obwohl eine der beiden Quellfelder (spamcount_out) fehlte oder anders heisst. Genau dieses Szenario ist der zentrale Risikofall des Moduls: PMG-Feldnamen sind Annahme A5 (Forenbeleg, unbestaetigt), und ein teilweise falscher, aber plausibel aussehender Wert ist laut eigenem Kommentar in proxmox-normalize.ts ('ein still falscher Wert waere schlimmer als ein ehrliches unbekannt') genau das, was das Modul verhindern soll. Alle uebrigen Einzelwerte (readNumber/readText/readBool je Feld, PBS readFirstPresent-Alternativnamen) sind korrekt nachsichtig und liefern bei fehlendem Feld null — nur diese eine Aggregation (zwei Teilwerte zu einer Summe) durchbricht das Muster."
+ artifacts:
+ - path: "apps/api/src/proxmox/proxmox-normalize.ts"
+ issue: "sumOrNull(a, b) (Zeile 240-243) gibt (a??0)+(b??0) zurueck, sobald mindestens einer von a/b nicht null ist — ein fehlender Halbwert wird als 0 addiert statt die Summe auf null zu setzen. Betrifft spamCount und virusCount in normalizePmg()."
+ missing:
+ - "sumOrNull so aendern, dass die Summe null ist, sobald a ODER b null ist (nicht erst wenn beide null sind) — oder spamCount/virusCount nur berechnen, wenn beide Teilwerte vorhanden sind."
+ - "Test in proxmox-normalize.spec.ts ergaenzen: 'nur spamcount_in vorhanden, spamcount_out fehlt' -> spamCount muss null sein, nicht der Teilwert."
+---
+
+# Quick 260923-dhh: Proxmox-Modul (PVE/PBS/PMG) — nur beobachten Verification Report
+
+**Goal:** PVE/PBS/PMG per Modul beobachten (nicht veraendern): Server in den Einstellungen anlegen mit verschluesseltem Zugang, Zertifikatsfehler nur je Server dulden, Hintergrundabfrage mit Zwischenlager, Modulseite mit Serverliste und Auslastung, Verbindungstest mit Klartext-Ursache.
+**Verified:** 2026-09-23T14:58Z
+**Status:** gaps_found
+**Re-verification:** No — initial verification
+
+## Goal Achievement
+
+### Observable Truths
+
+| # | Truth | Status | Evidence |
+|---|-------|--------|----------|
+| 1 | Kein Weg im Modul veraendert etwas bei Proxmox; die einzige Nicht-GET-Anfrage ist die Ticket-Anmeldung, maschinell nachgezaehlt | ✓ VERIFIED | `proxmox-nur-lesen.spec.ts` liest den Quelltext (Klammertiefen-Bilanzierung), zaehlt genau 1 `method:`-Uebergabe an `undiciFetch` in `proxmox-auth.ts`, und verlangt, dass jeder API-Pfad ausserhalb der Ticket-Anmeldung durch `proxmoxGet`/`getWithRetry` laeuft. `getWithRetry` (proxmox.service.ts:275) ruft ausschliesslich `proxmoxGet` — keine verdeckte zweite Schreibstelle. Grep ueber `apps/api/src/proxmox` bestaetigt: kein bare `fetch(` ausserhalb `undiciFetch`. Test lief gruen (2/2). |
+| 2 | Administrator legt Server (Name, Typ, Adresse, Zugang) an; Geheimnis nie im Klartext sichtbar | ✓ VERIFIED | `createServer`/`updateServer` verschluesseln via `CryptoService`; `SAFE_SERVER_SELECT` (proxmox.service.ts:26-42) waehlt `encryptedTokenSecret`/`encryptedPassword` nicht aus — die Felder verlassen die DB nie. `proxmox.service.spec.ts` bestaetigt `'encryptedTokenSecret' in list[0]` ist `false`. Frontend `ServerForm.tsx`: Geheimnisfelder immer leer geladen (`tokenSecret: ''`, `password: ''`), leer gelassen = unveraendert (Backend-Logik in `updateServer`). |
+| 3 | PVE/PBS: Token ODER Passwort; PMG nur Passwort, Token-Feld verschwindet und wird serverseitig abgelehnt | ✓ VERIFIED | `ServerForm.tsx`: `{form.productType !== 'pmg' &&