--- 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).