Files

127 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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' && <option value="token">...}` — Token-Option fehlt bei PMG. `PmgOhneTokenConstraint` im DTO UND zusaetzliche Pruefung in `updateServer` gegen den EFFEKTIVEN Stand (verhindert Umgehung ueber Teil-Updates). `buildTokenAuthHeader('pmg', ...)` wirft. Getestet in `proxmox-client.service.spec.ts` (DTO-Validierung PMG+Token). |
| 4 | Zertifikatsfehler nur je Server geduldet, Default "pruefen" | ✓ VERIFIED | `proxmoxGet`/`loginTicket` bauen den `Agent`-Dispatcher JE AUFRUF aus `target.tlsRejectUnauthorized` der jeweiligen Zeile — kein Modul-Singleton, keine Env-Variable. DTO-Default `tlsRejectUnauthorized ?? true`. Test bestaetigt: `true` → kein Dispatcher, `false` → genau ein `Agent` mit `rejectUnauthorized: false`. |
| 5 | Modulseite und jede Anzeige lesen ausschliesslich aus dem Zwischenlager | ✓ VERIFIED | `GET servers` → `listWithStatus()` liest nur aus der DB (kein `proxmoxGet`-Aufruf). `page.tsx`/`ServerCard.tsx` rendern nur `server.status`, das aus derselben Response stammt. Live-Abfrage findet nur ueber `pollServer`/`testConnection` statt, explizit durch Nutzerklick oder Scheduler ausgeloest. |
| 6 | Knopf "Verbindung testen" nennt Ursache in Alltagssprache | ✓ VERIFIED | Alle 7 `ProxmoxErrorKind`-Werte haben deutsche Klartexttexte in `de.json`/`en.json` (Sie-Form, mit Ursache und naechstem Schritt). `testConnection()` schreibt NICHT ins Zwischenlager (Vorbild LDAP-Test). |
| 7 | Fehlendes/anders benanntes/falsch typisiertes Feld → "unbekannt", nie Absturz/leere Seite/stiller Falschwert | ✗ PARTIAL | Siehe Gap unten: `sumOrNull()` in `normalizePmg()` liefert bei einem fehlenden Teilwert (z. B. `spamcount_out` fehlt) einen scheinbar vollstaendigen, tatsaechlich unvollstaendigen Zahlenwert statt `null`/"unbekannt". Alle uebrigen Einzelwerte (PVE/PBS, PMG countIn/countOut) sind korrekt nachsichtig — verifiziert in `proxmox-normalize.spec.ts` (20 Tests gruen) und live nachgerechnet (`node -e`). |
| 8 | Ohne Server: Modulseite ruhig, erklaert dass noch keiner eingetragen ist | ✓ VERIFIED | `page.tsx`: `servers.length === 0` → `t('emptyState')` plus Link zu den Einstellungen fuer Admins, kein Fehlertext. |
| 9 | Beide Tabellen tragen tenantId mit RLS-Policy; rls-coverage/rls-access-inventory bleiben gruen | ✓ VERIFIED | Migration erstellt `tenant_isolation_policy` auf beiden Tabellen plus `system_read_policy` nur auf `ProxmoxServer`. **Live in der Dev-DB bestaetigt** (`psql`): `relrowsecurity=t`, `relforcerowsecurity=t` auf beiden Tabellen; `pg_policies` zeigt exakt die erwarteten drei Policies. `rls-coverage.spec.ts` (5/5) und `rls-access-inventory.spec.ts` (30/30) gruen, inkl. `FORSYSTEM_ALLOWED_CALL_SITES`-Eintrag fuer den einzigen `forSystem()`-Aufruf. Klassifikationsdoku nachgemessen (`grep -c` bestaetigt 11 gebundene + 1 System-Rohtreffer, Doku sagt dasselbe). |
**Score:** 8/9 truths verified (0 present-but-behavior-unverified)
### Required Artifacts
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql` | RLS-Migration | ✓ VERIFIED | Existiert, angewendet (Tabellen + Policies live in der Dev-DB bestaetigt) |
| `apps/api/src/proxmox/proxmox-auth.ts` | einzige Kopfzeilen-Stelle | ✓ VERIFIED | `buildTokenAuthHeader`, `loginTicket`, `buildTicketCookieHeader`; keine andere Datei im Repo baut PVEAPIToken/PBSAPIToken/Cookie-Header |
| `apps/api/src/proxmox/proxmox-client.service.ts` | nur-lesender HTTP-Zugang | ✓ VERIFIED | `proxmoxGet`, `classifyFailure`, `parseJsonLenient` — kein `method`-Parameter |
| `apps/api/src/proxmox/proxmox-normalize.ts` | nachsichtige Leser | ⚠️ SUBSTANTIVE MIT LUECKE | Grundfunktionen (`readNumber`/`readText`/`readBool`/`readList`) korrekt; `normalizePmg`s Aggregation (`sumOrNull`) durchbricht das Muster (siehe Gap) |
| `apps/api/src/proxmox/proxmox-scheduler.service.ts` | Planer je Mandant | ✓ VERIFIED | `onApplicationBootstrap`, ein Cron-Auftrag je Mandant, Fan-out getestet (9/9 Tests) |
| `apps/api/src/proxmox/proxmox-nur-lesen.spec.ts` | maschineller Riegel D-01 | ✓ VERIFIED | 2/2 Tests gruen, Klammertiefen-Analyse statt naiver Regex |
| `apps/web/src/app/(portal)/modules/proxmox/page.tsx` | Modulseite | ✓ VERIFIED | Leerzustand, Serverliste, "Jetzt aktualisieren" |
| `apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx` | Einstellungsseite | ✓ VERIFIED | Rollen-Gate (Anzeige), CRUD, Loeschbestaetigung |
### Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `proxmox-auth.ts` | Klient/Planer/Verbindungstest | einzige Kopfzeilen-Bau-Stelle (D-03) | ✓ WIRED | `proxmox.service.ts` importiert ausschliesslich `buildTicketCookieHeader`/`buildTokenAuthHeader`/`loginTicket` aus dieser Datei; kein Nachbau anderswo |
| `proxmox-client.service.ts` | `tlsRejectUnauthorized`-Feld der Serverzeile | Dispatcher je Aufruf (D-04) | ✓ WIRED | `target.tlsRejectUnauthorized ? undefined : new Agent(...)` in `proxmoxGet` und `loginTicket`, je aus der uebergebenen Serverzeile |
| `proxmox-scheduler.service.ts` | `proxmox.controller.ts` | `onApplicationBootstrap` + `refreshTenant` nach jedem Speichern | ✓ WIRED | Controller ruft `scheduler.refreshTenant(tenantId)` nach `create`/`update`/`remove` |
| `proxmox.controller.ts` | `@UseModule`/`@Roles` | Modulfreigabe + Rollenschutz (D-09) | ✓ WIRED | `@UseModule('proxmox')` auf Klassenebene, `@Roles(ADMIN, SUPER_ADMIN)` auf allen Schreibwegen |
| Jeder DB-Zugriff | `forTenant()`/`forSystem()` | Mandantenbindung (D-08) | ✓ WIRED | `grep -c` bestaetigt 11 `tenantPrisma.(proxmoxServer\|proxmoxServerStatus).`-Treffer, 1 `systemPrisma.proxmoxServer.`-Treffer — deckungsgleich mit `FORSYSTEM_ALLOWED_CALL_SITES` und der Klassifikationsdoku |
### Data-Flow Trace
| Artifact | Data Variable | Source | Produces Real Data | Status |
|----------|---------------|--------|---------------------|--------|
| `ServerCard.tsx` | `server.status.metrics` | `GET modules/proxmox/servers` → `listWithStatus()` → DB (`ProxmoxServerStatus`) | Ja (mit Testdaten belegt, kein echter Proxmox verfuegbar — s. unten) | ✓ FLOWING |
| `ServerForm.tsx` Testergebnis | `testResult` | `POST servers/:id/test` → `testConnection()` → `pollOne()` (kein DB-Schreiben) | Ja | ✓ FLOWING |
### Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| Nur-Lesen-Riegel haelt (Klammertiefen-Analyse, nicht nur Praesenz) | `vitest run src/proxmox/proxmox-nur-lesen.spec.ts` | 2/2 gruen | ✓ PASS |
| Ticket-Erneuerung: genau EIN zweiter Versuch, zweites 401 bleibt Fehler | `vitest run src/proxmox` (enthaelt beide Faelle) | gruen | ✓ PASS |
| Scheduler: zwei Mandanten verdraengen sich nicht, leere Serverliste → kein Auftrag | `vitest run src/proxmox/proxmox-scheduler.service.spec.ts` | 9/9 gruen | ✓ PASS |
| RLS tatsaechlich in der Dev-DB aktiv (nicht nur im SQL-Text) | `docker exec ... psql -c "SELECT relrowsecurity, relforcerowsecurity FROM pg_class WHERE relname IN (...)"` | `t / t` auf beiden Tabellen, 3 erwartete Policies vorhanden | ✓ PASS |
| `sumOrNull`-Aggregationsluecke (eigener Nachbau, nicht Teil der Testsuite) | `node -e "sumOrNull(10, null)"` | `10` (haette bei ehrlichem Verhalten `null` sein muessen) | ✗ FAIL — bestaetigt den Gap oben |
| Volle Testsuiten | `pnpm --filter @tessera/api test`, `pnpm --filter @tessera/web test` | 1311/1311 bzw. 708/708 gruen, identisch zu SUMMARY-Zahlen | ✓ PASS |
| type-check / lint / Biome | `pnpm type-check`, `pnpm lint`, `pnpm --filter @tessera/web exec biome lint .` | 4/4, 5/5, "Found 53 warnings" | ✓ PASS |
### Requirements Coverage
Kein separates REQUIREMENTS.md fuer Quick-Tasks; Abdeckung erfolgt ueber die elf D-Nummern im Plan-Frontmatter (`<source_audit>`), alle als COVERED gefuehrt und hier gegengeprueft — kein Widerspruch gefunden ausser dem oben genannten Gap zu D-08/T-DHH-08 (stiller Falschwert).
### Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `apps/api/src/proxmox/proxmox-normalize.ts` | 240-243 | Aggregation verschluckt fehlenden Teilwert (`sumOrNull`) | 🛑 Blocker (verletzt explizites must-have) | PMG "Spam"/"Viren"-Zahl kann eine unvollstaendige, aber vertrauenswuerdig aussehende Zahl zeigen statt "unbekannt" |
| — | — | Keine TBD/FIXME/XXX in den neuen Dateien gefunden | ℹ️ Info | — |
| `apps/web/.../page.tsx` | 54 | "Jetzt aktualisieren"-Knopf wird JEDEM Nutzer mit Modulzugriff gezeigt, `POST servers/:id/poll` ist aber `@Roles(ADMIN, SUPER_ADMIN)`; Fehler wird mit `.catch(() => undefined)` still verschluckt | ⚠️ Warning (UX, keine Sicherheitsluecke — Backend blockt korrekt) | Normale Nutzer sehen einen Knopf, der bei ihnen wirkungslos bleibt, ohne Rueckmeldung |
## Human Verification Required
Diese Punkte kann kein automatisierter Check abschliessend pruefen — teils weil kein echter Proxmox-Server in dieser Umgebung erreichbar ist (vom Auftrag selbst so benannt), teils weil es sich um visuelles/Browser-Verhalten handelt.
### 1. Modulseite im Browser (vom Plan als `<human-check>` in Aufgabe 6 vorgesehen)
**Test:** `/modules/proxmox` oeffnen: ohne Server pruefen, dass der ruhige Hinweis erscheint; danach in den Einstellungen einen Server anlegen und pruefen, dass er in der Liste auftaucht; einen absichtlich falschen Zugang eintragen und pruefen, dass Klartext statt einer leeren Flaeche erscheint.
**Expected:** Ruhiger Leerzustand, danach korrekte Anzeige, dann Klartext-Fehlermeldung.
**Why human:** Erfordert echten Browser-Durchlauf; die laufenden Container wurden fuer diese Verifikation bewusst nicht neu gebaut (Auftragsvorgabe), ein visueller Check ist damit nicht ohne Weiteres moeglich.
### 2. Annahmen A2/A3/A5 gegen echte PVE-/PBS-/PMG-Server
**Test:** Cookie-Namen (`PBSAuthCookie`/`PMGAuthCookie`), PBS-Belegungs-/Snapshot-Feldnamen und PMG-Statistikfelder gegen einen echten Server pruefen.
**Expected:** Die in `TICKET_COOKIE_NAME`/`PBS_USAGE_FIELDS`/`PBS_SNAPSHOT_FIELDS`/`PMG_STATS_FIELDS` hinterlegten Namen stimmen, oder werden an der jeweils benannten EINEN Stelle nachgezogen.
**Why human:** Kein PVE/PBS/PMG-Server in dieser Umgebung erreichbar — vom Plan selbst so benannt und in `user_setup` dokumentiert, keine Verifikationsluecke dieser Pruefung.
## Gaps Summary
Ein konkreter, durch Code und einen eigenen Nachrechenlauf bestaetigter Gap: `normalizePmg()`s `sumOrNull()`-Hilfsfunktion behandelt einen fehlenden Teilwert (`spamcount_out`/`viruscount_out` bzw. deren `_in`-Gegenstuecke) als `0` statt die kombinierte Summe als `null`/"unbekannt" zu markieren. Das widerspricht direkt dem im Plan-Frontmatter (`must_haves.truths`) UND im eigenen Code-Kommentar ("ein still falscher Wert waere schlimmer als ein ehrliches unbekannt") formulierten Anspruch. Da PMG-Feldnamen die am wenigsten abgesicherte Annahme des gesamten Auftrags sind (Annahme A5, reiner Forenbeleg), ist genau dieses Szenario — ein Teilfeld feuert, das andere heisst anders — nicht hypothetisch, sondern der wahrscheinlichste erste Fehlerfall beim echten Test durch den Nutzer. Kein Test in `proxmox-normalize.spec.ts` deckt den Fall "nur eine Haelfte des Paares vorhanden" ab; alle vorhandenen Tests pruefen entweder "beide vorhanden" oder "beide fehlen".
Alle uebrigen acht Wahrheiten aus dem Plan sind vollstaendig verifiziert, mehrfach durch automatisierte Tests UND durch eigene Stichproben (Live-RLS-Abfrage gegen die tatsaechliche Dev-Datenbank, Grep-Nachzaehlung der Mandantenbindung, direkte Pruefung des Nur-Lesen-Riegels, manuelles Nachrechnen der Klammertiefen-Logik). Alle sieben Commits, alle 24 im Frontmatter genannten Dateien und alle gemessenen Torzahlen (1311/1311 API-Tests, 708/708 Web-Tests, 4/4 type-check, 5/5 lint, exakt 53 Biome-Warnungen) wurden unabhaengig nachvollzogen und stimmen exakt mit der SUMMARY ueberein.
---
_Verified: 2026-09-23T14:58Z_
_Verifier: Claude (gsd-verifier)_