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:
+295
@@ -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).
|
||||
+126
@@ -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)_
|
||||
Reference in New Issue
Block a user