docs(quick-260923-dhh): Akte - Rundgang gegen den Nachbau, offener Befund aus der Abnahme

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 15:15:16 +02:00
parent a9e0d5b1ae
commit 231bd5e47f
3 changed files with 425 additions and 2 deletions
@@ -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:<tenantId>`),
`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 `<must_haves>` 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 (`<span>`).
**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).
@@ -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' && <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)_