From ec9c77956da8ab8b463deecb06e909812c9cb5de Mon Sep 17 00:00:00 2001 From: Schalli Date: Wed, 23 Sep 2026 10:01:40 +0200 Subject: [PATCH] docs(quick-260923-dhh): Proxmox-Modul PVE/PBS/PMG - Plan Sieben Aufgaben in einem Plan: Tracer (PVE/Token end-to-end), Ticket-Zugang mit Fehler-Klartext und Beobachtungs-Riegel, PBS/PMG nachsichtig auswerten, Hintergrundabfrage je Mandant plus Verbindungstest, Einstellungsseite, Modulseite, Doku und Nachmessung. Ausgangswerte der Tore gemessen: api 77/1240, web 82/693, type-check 4/4, lint 5/5, Biome-web genau 53 Warnungen. Co-Authored-By: Claude Opus 5 (1M context) --- .../260923-dhh-PLAN.md | 910 ++++++++++++++++++ 1 file changed, 910 insertions(+) create mode 100644 .planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md diff --git a/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md new file mode 100644 index 0000000..15fc37c --- /dev/null +++ b/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md @@ -0,0 +1,910 @@ +--- +phase: quick-260923-dhh +plan: 01 +type: execute +wave: 1 +depends_on: [] +autonomous: true +requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11] + +files_modified: + - apps/api/prisma/schema.prisma + - apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql + - apps/api/src/app.module.ts + - apps/api/src/proxmox/proxmox.types.ts + - apps/api/src/proxmox/proxmox-auth.ts + - apps/api/src/proxmox/proxmox-normalize.ts + - apps/api/src/proxmox/proxmox-client.service.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-client.service.spec.ts + - apps/api/src/proxmox/proxmox.service.spec.ts + - apps/api/src/proxmox/proxmox-normalize.spec.ts + - apps/api/src/proxmox/proxmox-scheduler.service.spec.ts + - apps/api/src/proxmox/proxmox-nur-lesen.spec.ts + - apps/api/src/prisma/rls-access-inventory.spec.ts + - apps/web/src/lib/proxmox-api.ts + - apps/web/src/lib/module-loader.ts + - 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/components/ServerCard.tsx + - apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.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/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx + - apps/web/src/messages/de.json + - apps/web/src/messages/en.json + - docs/mandantentrennung-zugriffsklassifikation.md + - docs/anleitung-entwicklung.md + - docs/anwenderhandbuch.md + +user_setup: + - service: proxmox + why: "Nur der Nutzer hat echte PVE-/PBS-/PMG-Server. Ohne sie bleibt die Feldnamen-Annahme A2/A3 der Recherche unbestaetigt." + dashboard_config: + - task: "Je Produkt einen NUR-LESE-Zugang anlegen: PVE-Rolle PVEAuditor, PBS-Rolle Audit bzw. DatastoreAudit, PMG-Rolle Auditor" + location: "Proxmox-Oberflaeche -> Datacenter/Configuration -> Permissions" + +estimate: + tokens: 320000 + raw_tokens: 210000 + tasks: 7 + confidence: low + +must_haves: + truths: + - "Kein Weg im gesamten Modul veraendert etwas bei Proxmox: die einzige Nicht-GET-Anfrage im ganzen Modul ist die Ticket-Anmeldung, und sie wird maschinell nachgezaehlt (D-01)." + - "Ein Administrator legt in den Einstellungen Server an (Name, Typ pve|pbs|pmg, Adresse, Zugang) und sieht die Zugangsdaten nie wieder im Klartext (D-02)." + - "PVE und PBS bieten API-Token ODER Benutzer/Passwort; PMG bietet nur Benutzer/Passwort — die Token-Felder erscheinen bei PMG gar nicht und ein Token-Zugang fuer PMG wird serverseitig abgelehnt (D-03)." + - "Zertifikatsfehler werden nur fuer die Server geduldet, bei denen der Administrator es einzeln eingeschaltet hat; Voreinstellung ist pruefen (D-04)." + - "Die Modulseite und jede Anzeige lesen ausschliesslich aus dem Zwischenlager, nie live bei Proxmox (D-05)." + - "Der Knopf Verbindung testen nennt die Ursache in Alltagssprache: nicht erreichbar, Zugang abgelehnt, Rechte reichen nicht, Zertifikat, unerwartete Antwort (D-06)." + - "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." + - "Ohne angelegten Server ist die Modulseite ruhig und erklaert, dass noch keiner eingetragen ist." + - "Beide neuen Tabellen tragen tenantId mit RLS-Policy; rls-coverage.spec.ts und rls-access-inventory.spec.ts bleiben gruen (D-08)." + artifacts: + - apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql + - 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-scheduler.service.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/settings/page.tsx" + key_links: + - "proxmox-auth.ts ist die EINZIGE Stelle, die Kopfzeilen und Anmelde-Cookies je Produkt baut — Klient, Verbindungstest und Planer rufen sie, keiner baut sie nach (D-03)." + - "proxmox-client.service.ts baut den undici-Dispatcher je Aufruf aus dem Feld tlsRejectUnauthorized genau dieser Serverzeile (D-04)." + - "proxmox-scheduler.service.ts haengt an onApplicationBootstrap und faechert je Mandant auf; der Controller zieht nach jedem Speichern nach (D-05)." + - "proxmox.controller.ts traegt @UseModule('proxmox'); die Schreibwege zusaetzlich @Roles(ADMIN, SUPER_ADMIN) (D-09)." + - "Jeder Datenbankzugriff laeuft ueber forTenant(); nur der Startpfad des Planers ueber forSystem() und steht in FORSYSTEM_ALLOWED_CALL_SITES (D-08)." +--- + + +Das Proxmox-Modul anbinden: PVE, PBS und PMG **nur beobachten**. Der Administrator legt in +den Moduleinstellungen beliebig viele Server an (Name, Typ, Adresse, Zugang — verschluesselt +gespeichert). Ein Hintergrunddienst fragt sie periodisch ab und legt die Messwerte in einem +Zwischenlager ab. Die Modulseite zeigt die Serverliste mit Auslastung und liest dabei +ausschliesslich aus dem Zwischenlager. + +Purpose: Der Nutzer sieht den Zustand seiner Proxmox-Landschaft in Tessera, ohne die +Proxmox-Oberflaechen einzeln zu oeffnen — und ohne dass Tessera je etwas an ihnen aendern kann. + +Output: Ein vollstaendiges Modul `proxmox` (Datenbank, Dienst, API, Hintergrundabfrage, +Einstellungsseite, Modulseite, Dokumentation), sieben eigenstaendige Commits. + +## Herkunft der Entscheidungen (D-Nummern) + +Die D-Nummern in diesem Plan verweisen auf die elf bereits getroffenen Entscheidungen aus dem +Auftrag (``), in derselben Reihenfolge: + +| ID | Entscheidung | +|---|---| +| D-01 | Nur beobachten — kein veraendernder Weg gegen Proxmox | +| D-02 | Administrator legt Server an; Zugangsdaten verschluesselt, nie im Klartext zurueck | +| D-03 | PVE/PBS: Token oder Benutzer/Passwort; PMG nur Benutzer/Passwort; Kopfzeilen aus EINER Stelle | +| D-04 | Zertifikatsfehler nur je Server umschaltbar dulden, nie global | +| D-05 | Zwischenlager statt Live-Abfrage; Planer nach TENDER-Muster (`onApplicationBootstrap`) | +| D-06 | Knopf „Verbindung testen" mit Klartext-Ursache | +| D-07 | Keine neue npm-Abhaengigkeit — `undici` ist bereits da | +| D-08 | Mandantentrennung Pflicht: `tenantId` + RLS + Klassifikationsdoku + gruene Waechter-Tests | +| D-09 | Zugriff ueber die normale Modulfreigabe | +| D-10 | Oberflaechentexte Deutsch in der Sie-Form ueber next-intl; Kommentare Deutsch | +| D-11 | Dashboard-Kachel ist NICHT in diesem Auftrag | + +## Ausgangswerte der Tore (gemessen 2026-09-23, vor Beginn) + +| Tor | Ausgangswert | +|---|---| +| `pnpm --filter @tessera/api test` | 77 Dateien, 1240 Tests, alle gruen | +| `pnpm --filter @tessera/web test` | 82 Dateien, 693 Tests, alle gruen | +| `pnpm type-check` | 4 von 4 erfolgreich | +| `pnpm lint` | 5 von 5 erfolgreich | +| Biome-Warnungen in `apps/web` | genau 53 (253 Dateien geprueft) | + +Zielwert nach jeder Aufgabe: Testzahlen **groesser oder gleich** dem Ausgangswert und gruen, +type-check 4/4, lint 5/5, Biome-Warnungen in `apps/web` **exakt 53** — nicht mehr, nicht weniger. + +## Ausdruecklich NICHT im Umfang + +Dashboard-Kachel (D-11, kommt als eigener Auftrag ueber `WIDGET_TYPES` / +`WIDGET_MODULE_SLUGS` / `registerWidget`), Zeitreihen und Verlaufsgrafiken (`/rrddata`), +Eingriffe jeder Art (Start, Stopp, Sichern, Freigeben), Quarantaene-Verwaltung bei PMG. + + + +@~/.claude/gsd-core/workflows/execute-plan.md +@~/.claude/gsd-core/templates/summary.md + + + +@.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-RESEARCH.md +@.planning/STATE.md +@CLAUDE.md + +Bestandsmuster, die dieser Plan wortgetreu wiederverwendet (vor der jeweiligen Aufgabe lesen, +nicht raten): + +@apps/api/src/favorites/icon-discovery.service.ts +@apps/api/src/ldap/ldap-config.service.ts +@apps/api/src/dkv/dkv-scheduler.service.ts +@apps/api/src/tenders/tender-scheduler.service.ts +@apps/api/src/domaincheck/domaincheck.module.ts +@apps/api/src/prisma/prisma-tenant.extension.ts + + + +Signaturen und Konstanten, auf die jede Aufgabe aufsetzt — so gemessen im Bestand, nicht erfunden: + +- `CryptoService` (`apps/api/src/crypto/crypto.service.ts`): `encrypt(plaintext: string): string` + und `decrypt(stored: string): string`. Format `iv:authTag:ciphertext`, alles hex, + Doppelpunkt-getrennt. `CryptoModule` steht bereits in `app.module.ts`. +- Erkennungsform fuer „schon verschluesselt" (`ldap-config.service.ts:17`): + `/^[0-9a-f]+:[0-9a-f]+:[0-9a-f]*$/i`. +- `forTenant(prisma, tenantId, userId?)` und `forSystem(prisma)` aus + `apps/api/src/prisma/prisma-tenant.extension.ts`. Konvention: lokale Konstante + `const tenantPrisma = forTenant(this.prisma, tenantId);` — keine andere Form, sonst schlaegt + `rls-access-inventory.spec.ts` fehl. +- `undici`: `import { Agent, fetch as undiciFetch } from 'undici'`. Nodes globales `fetch` + ignoriert einen `Agent` aus dem npm-Paket (gemessen, `icon-discovery.service.ts:33-37`). +- `@UseModule(slug)` aus `apps/api/src/module-registry/module.guard.ts`, + `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` aus `apps/api/src/auth/decorators/roles.decorator.ts`. +- `ModuleRegistryService.seedModule({ slug, name, version, category, description: {de, en}, isSystem })` + — Vorlage `apps/api/src/domaincheck/domaincheck.seed.ts`. +- `CronJobClass` wird per `require('cron').CronJob` aufgeloest (pnpm-Isolation, Kommentar in + `dkv-scheduler.service.ts:6-16` woertlich uebernehmen). +- RLS-Policy-Form ohne Benutzerdimension (`20260909140000`, DkvModuleConfig): + `CREATE POLICY tenant_isolation_policy ON "X" USING ("tenantId" = current_tenant_id());` +- Systemlese-Form (`20260914120000`): + `CREATE POLICY system_read_policy ON "X" FOR SELECT USING (is_system_context());` +- Frontend-Datenzugriff: ein Helfer `apps/web/src/lib/-api.ts` (Vorbild `dkv-api.ts`), + `API_URL` aus `process.env.NEXT_PUBLIC_API_URL`, `credentials: 'include'`. +- Modulseiten liegen unter `apps/web/src/app/(portal)/modules//`, `layout.tsx` umschliesst + mit ``, Eintrag in `MODULE_REGISTRY` + (`apps/web/src/lib/module-loader.ts`). + + + + + + Aufgabe 1: Ein PVE-Server per Token — von der Tabelle bis zur Modulseite + +apps/api/prisma/schema.prisma, +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.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.service.spec.ts, +apps/api/src/app.module.ts, +apps/web/src/lib/proxmox-api.ts, +apps/web/src/lib/module-loader.ts, +apps/web/src/app/(portal)/modules/proxmox/layout.tsx, +apps/web/src/app/(portal)/modules/proxmox/page.tsx, +apps/web/src/messages/de.json, +apps/web/src/messages/en.json, +docs/mandantentrennung-zugriffsklassifikation.md + + +`TESSERA_ENCRYPTION_KEY` ist gesetzt (64 Hex-Zeichen) und die Entwicklungsdatenbank ist vom +Host erreichbar — sonst scheitert `prisma migrate dev`. Erreichbarkeit siehe +`docs/anleitung-entwicklung.md`, Abschnitt „Datenbank vom Host erreichen"; die Datenbank hat +keinen Host-Port, der Zugriff laeuft ueber die Container-IP mit `tessera:tessera_dev`. + + +Duenner, aber durchgehender Schnitt durch ALLE Schichten, die dieses Modul anfasst — genau EIN +Weg: ein PVE-Server, Zugang per API-Token, vom Anlegen ueber die Abfrage und das Zwischenlager +bis zur Anzeige im Browser. Kein PBS, kein PMG, kein Benutzer/Passwort, kein Planer, keine +Einstellungsoberflaeche, kein Bearbeiten oder Loeschen — das bauen die Aufgaben 2 bis 6 auf +diesem bewiesenen Geruest auf. Was hier entsteht, ist Endstand, kein Wegwerfstueck: dieselbe +Fehlerbehandlung, dieselbe Mandantenbindung, dieselben Tore wie jede spaetere Aufgabe. + +**Schema** (`schema.prisma`) — zwei Modelle nach dem Vorbild `CalendarSource` (mehrere +verschluesselte Fremdzugaenge je Mandant), NICHT nach `DkvModuleConfig` (Singleton je Mandant): + +`ProxmoxServer`: `id` (uuid), `tenantId`, `name`, `productType` (Zeichenkette `pve|pbs|pmg`, +kommentiert wie `CalendarSource.type`), `baseUrl`, `authMethod` (`token|password`), `tokenId` +(nullable), `encryptedTokenSecret` (nullable, Kommentar „AES-256-GCM ciphertext +(iv:authTag:ciphertext hex)" wie `CalendarSource.encryptedPassword`), `username` (nullable), +`encryptedPassword` (nullable), `tlsRejectUnauthorized Boolean @default(true)` (Feldname +woertlich von `LdapConfig`, D-04), `isActive Boolean @default(true)`, +`pollIntervalMin Int @default(5)`, `position Int @default(0)`, `createdAt`, `updatedAt`, +Relation `status ProxmoxServerStatus?`, `@@index([tenantId])`. + +`ProxmoxServerStatus`: `id`, `serverId String @unique` mit Relation auf `ProxmoxServer` +(`onDelete: Cascade`), `tenantId`, `lastPolledAt DateTime?`, `lastOkAt DateTime?`, +`reachable Boolean @default(false)`, `errorKind String?`, `errorDetail String?`, +`metrics Json?` (normalisierte Messwerte), `rawSample Json?` (gekuerzte Rohantwort zur +Fehlersuche beim Nutzer), `updatedAt`, `@@index([tenantId])`. + +**Migration** (`20260923140000_proxmox_server/migration.sql`) — von Hand geschrieben nach dem +Vorbild `20260923120000_dashboard_tabs`: deutscher Kopfkommentar, der Zweck und die +Entscheidungen benennt. Beide Tabellen mit `ENABLE`/`FORCE ROW LEVEL SECURITY` und +`tenant_isolation_policy` in der Form OHNE Benutzerdimension +(`USING ("tenantId" = current_tenant_id())`, Vorbild `DkvModuleConfig`) — Proxmox-Server sind +Verwaltungsdaten des Mandanten, nicht persoenliche Daten eines Benutzers. Zusaetzlich auf +`ProxmoxServer` (und NUR dort) `system_read_policy … FOR SELECT USING (is_system_context())` +mit der Begruendung im Kommentar, dass der Planer aus Aufgabe 4 beim Start die aktiven Server +ALLER Mandanten sehen muss; `ProxmoxServerStatus` bekommt sie bewusst nicht, weil dort nur je +Mandant gebunden geschrieben wird. Indizes auf `tenantId` sowie `serverId` (unique). Rechte +fuer `tessera_app` kommen automatisch ueber `ALTER DEFAULT PRIVILEGES` aus +`20260909130000_rls_app_role` — im Kommentar erwaehnen, nichts tun. + +**`proxmox.types.ts`** — die gemeinsamen Typen: `ProxmoxProductType = 'pve' | 'pbs' | 'pmg'`, +`ProxmoxAuthMethod = 'token' | 'password'`, `ProxmoxErrorKind = +'netz' | 'zugang' | 'rechte' | 'zertifikat' | 'antwortform' | 'server' | 'unbekannt'` (diese +sieben Werte landen so in der Datenbank und werden erst im Frontend uebersetzt — stabile +Schluessel, uebersetzbarer Text), und die Ergebnisform `ProxmoxPollResult` mit +`{ reachable, errorKind, errorDetail, metrics, rawSample }`. + +**`proxmox-auth.ts`** — die EINZIGE Stelle im ganzen Modul, die Anmeldeinformationen in +Kopfzeilen uebersetzt (D-03, key_link). In dieser Aufgabe nur der Token-Zweig: eine reine +Funktion `buildTokenAuthHeader(productType, tokenId, tokenSecret)`, die fuer `pve` das Schema +`PVEAPIToken` mit Gleichheitszeichen vor dem Geheimnis und fuer `pbs` das Schema `PBSAPIToken` +mit Doppelpunkt vor dem Geheimnis liefert (Recherche, Block 1) und fuer `pmg` einen Fehler +wirft, weil PMG keine Token kennt. Die Funktion nimmt Klartext entgegen und gibt nur die +Kopfzeile zurueck — sie protokolliert nie, sie wirft das Geheimnis nie in eine Fehlermeldung. + +**`proxmox-client.service.ts`** — der HTTP-Zugang, und ausschliesslich lesend (D-01). +Genau EINE oeffentliche Datenabruf-Funktion `proxmoxGet(server, path)`, die das +Anfrageverfahren fest auf Lesen setzt (kein Parameter dafuer, kein Durchreichen von aussen). +Zwingend `undiciFetch` aus dem `undici`-Paket, nicht das globale `fetch` — sonst wird der +Dispatcher stillschweigend ignoriert (gemessen, `icon-discovery.service.ts:33-37`); diesen +Grund als deutschen Kommentar in die Datei schreiben. Der Dispatcher wird JE AUFRUF aus der +gelesenen Serverzeile gebaut: ist `tlsRejectUnauthorized` wahr, wird kein Dispatcher +uebergeben (Normalweg, echte Pruefung); ist es falsch, ein frischer +`new Agent({ connect: { rejectUnauthorized: false } })` nur fuer diesen einen Aufruf (D-04). +Ausdruecklich KEINE Modulkonstante wie in `icon-discovery.service.ts` und ausdruecklich keine +Node-Umgebungsvariable — beides als Kommentar festhalten. Abbruch nach 8 Sekunden ueber +`AbortController`. Keine SSRF-Adresspruefung wie `isPublicHttpUrl`: Proxmox-Server stehen +per Definition im privaten Netz, eine solche Pruefung wuerde jede reale Adresse blockieren; +die Absicherung ist stattdessen, dass nur ein Administrator Adressen eintragen darf (siehe +Bedrohungsmodell T-DHH-02). Rueckgabe ist ein Ergebnisobjekt mit `ok`, `status`, `body` und +`errorKind` — geworfen wird nichts nach aussen; Netzfehler und Zertifikatsfehler werden +abgefangen und in `errorKind` uebersetzt. + +**`proxmox.service.ts`** — die Fachlogik, jeder Datenbankzugriff ueber +`const tenantPrisma = forTenant(this.prisma, tenantId);` (Konvention woertlich, D-08): +`createServer(tenantId, dto)` verschluesselt das Token-Geheimnis mit `this.crypto.encrypt(...)` +und legt Server plus leere Zwischenlagerzeile an; `listWithStatus(tenantId)` liefert Server +samt Zwischenlager OHNE jedes Geheimnisfeld (`select` ohne `encryptedTokenSecret` und +`encryptedPassword`, nicht nachtraeglich maskiert — die Felder verlassen die Datenbank gar +nicht erst); `pollServer(tenantId, serverId)` entschluesselt in genau EINER privaten Methode +`decryptSecret(stored)` nach dem Vorbild `LdapConfigService.decryptBindPassword` (Form +erkennen, unveraenderte Altwerte durchreichen), ruft fuer `pve` den Pfad +`/api2/json/cluster/resources`, normalisiert das Ergebnis und schreibt es ins Zwischenlager. +In dieser Aufgabe nur PVE und nur eine Grundauswertung: Anzahl Knoten, Anzahl laufender und +gestoppter Gaeste, und je Knoten `cpu`/`maxcpu`/`mem`/`maxmem` — jeder Einzelwert nachsichtig +gelesen (fehlt er, steht `null` im Zwischenlager und spaeter „unbekannt" in der Anzeige, nie +ein Absturz und nie eine 0, die wie ein Messwert aussieht). Die Rohantwort wird auf hoechstens +20 000 Zeichen gekuerzt in `rawSample` abgelegt, damit der Nutzer beim Testen an seinen echten +Servern sieht, was tatsaechlich kam. + +**`proxmox.controller.ts`** — `@Controller('modules/proxmox')` und `@UseModule('proxmox')` auf +Klassenebene (D-09, Vorbild `domaincheck.controller.ts`). Drei Wege: `GET servers` (Liste mit +Zwischenlager, fuer jeden Benutzer mit Modulzugriff), `POST servers` und +`POST servers/:id/poll` — beide Schreibwege zusaetzlich mit +`@Roles(Role.ADMIN, Role.SUPER_ADMIN)`. `tenantId` kommt ausschliesslich aus `req.tenantId`, +nie aus Body oder Query. + +**`dto/proxmox-server.dto.ts`** — `class-validator`: `name` nicht leer, `productType` per +`@IsIn(['pve','pbs','pmg'])`, `baseUrl` per `@IsUrl({ protocols: ['http','https'], require_tld: false })` +(ohne `require_tld`, weil interne Namen wie `pve.intern` sonst abgelehnt wuerden), +`authMethod` per `@IsIn(['token','password'])`, `tlsRejectUnauthorized` optional boolesch, +`pollIntervalMin` als Ganzzahl zwischen 1 und 1440. + +**`proxmox.module.ts` / `proxmox.seed.ts` / `app.module.ts`** — Vorbild Domaincheck: +`seedProxmoxModule` mit `slug: 'proxmox'`, `name: 'Proxmox'`, `version: '1.0.0'`, +`category: 'infrastructure'`, deutscher und englischer Beschreibung, `isSystem: true`; +`ProxmoxModule` importiert `ModuleRegistryModule` und ruft den Seed in `onModuleInit`; +Eintrag in `app.module.ts` unter `imports` hinter `BugReportsModule`. + +**Frontend** — `apps/web/src/lib/proxmox-api.ts` nach dem Vorbild `dkv-api.ts` +(`listServers()`); `modules/proxmox/layout.tsx` mit +``; `modules/proxmox/page.tsx` als Client-Komponente, +die die Serverliste laedt und je Server Name, Typ, Adresse und die vorhandenen Messwerte +anzeigt — fehlende Werte als „unbekannt", bei leerer Liste ein ruhiger Hinweis, dass noch kein +Server eingetragen ist (kein Fehlergewitter, keine weisse Flaeche); Eintrag `proxmox` in +`MODULE_REGISTRY` (`module-loader.ts`). Alle sichtbaren Texte ueber `useTranslations('proxmox')` +mit neuen Schluesseln in `de.json` UND `en.json` — deutsche Texte in der Sie-Form (D-10). + +**Doku** — in `docs/mandantentrennung-zugriffsklassifikation.md` die neuen Fundstellen als +Tabellenzeilen im Format `| Datei | Modell | Klasse | Stand | Begruendung |` eintragen +(`apps/api/src/proxmox/proxmox.service.ts` / `proxmoxServer` und `proxmoxServerStatus`, Klasse +`muss-mandantengebunden`, Stand `gebunden`), sonst schlaegt `rls-access-inventory.spec.ts` fehl. +Die Bereichs- und Summenzeilen mit der Gate-Schleife NACHMESSEN, nicht abschreiben. + +Deutsche Kommentare im Code wie in den Nachbardateien (D-10). Keine neue npm-Abhaengigkeit +(D-07) — `undici` steht bereits als direkte Abhaengigkeit in `apps/api/package.json`. + + + pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-coverage.spec.ts src/prisma/rls-access-inventory.spec.ts + pnpm --filter @tessera/api test + pnpm --filter @tessera/web test + pnpm type-check + + +`proxmox.service.spec.ts` fuehrt den ganzen Weg mit einer gefaelschten `undici`-Antwort durch +(Vorbild der Attrappe: `icon-discovery.service.spec.ts`, `vi.mock('undici', …)`): Server +anlegen, abfragen, Zwischenlager gelesen — und weist nach, dass (a) das Geheimnis +verschluesselt in der Datenbank steht und in der Antwort von `listWithStatus` ueberhaupt nicht +vorkommt, (b) bei `tlsRejectUnauthorized: true` KEIN Dispatcher uebergeben wird und bei +`false` genau einer mit abgeschalteter Pruefung, (c) ein fehlendes Feld der Antwort zu `null` +fuehrt und nicht zu einem Wurf. `pnpm --filter @tessera/api test` gruen mit mindestens 1240 +Tests, `pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests, `pnpm type-check` +4 von 4. Im Browser ist `/modules/infrastructure/proxmox` erreichbar und zeigt bei leerer +Liste den ruhigen Hinweis. + + + + + Aufgabe 2: Zugang per Benutzer/Passwort, Fehler in Alltagssprache, Beobachtungs-Riegel + +apps/api/src/proxmox/proxmox-auth.ts, +apps/api/src/proxmox/proxmox-client.service.ts, +apps/api/src/proxmox/proxmox.service.ts, +apps/api/src/proxmox/dto/proxmox-server.dto.ts, +apps/api/src/proxmox/proxmox-client.service.spec.ts, +apps/api/src/proxmox/proxmox-nur-lesen.spec.ts + + +- Ticket-Anmeldung: `POST /api2/json/access/ticket` mit `username`/`password` als Formularfeldern liefert `data.ticket`; Folgeanfragen tragen das Ticket als Cookie mit produktabhaengigem Namen (`PVEAuthCookie`, `PBSAuthCookie`, `PMGAuthCookie`). +- Kein `CSRFPreventionToken` wird jemals mitgesendet — dieses Modul liest nur, und fuer Leseanfragen verlangt Proxmox ihn laut offizieller Doku nicht. +- Ein Server vom Typ `pmg` mit `authMethod: 'token'` wird beim Anlegen und beim Bearbeiten mit einer deutschen Klartextmeldung abgelehnt (400), nicht erst beim Abfragen. +- Antwortstatus 401 wird zu `errorKind: 'zugang'`, 403 zu `'rechte'`, 404 zu `'antwortform'` mit dem Hinweis auf eine falsche Adresse, 5xx zu `'server'`. +- Ein geworfener Netzfehler ohne Antwort (Verbindung verweigert, Zeitablauf, Name nicht aufloesbar) wird zu `errorKind: 'netz'`. +- Ein Zertifikatsfehler (Meldungstext enthaelt eine der bekannten Zertifikatskennungen) wird zu `errorKind: 'zertifikat'` und NICHT zu `'netz'`. +- Eine Antwort, die kein JSON ist (HTML-Anmeldeseite, leerer Rumpf), wird zu `errorKind: 'antwortform'` — kein geworfener Parserfehler, kein Absturz. +- Laeuft ein Ticket ab (401 bei `authMethod: 'password'`), wird GENAU EINMAL neu angemeldet und die Abfrage wiederholt; erst ein zweites 401 wird zu `errorKind: 'zugang'`. +- Keine Fehlermeldung, kein Protokolleintrag und kein `rawSample` enthaelt jemals Passwort, Token-Geheimnis oder das Ticket. +- Im gesamten Verzeichnis `apps/api/src/proxmox` gibt es ausserhalb der Ticket-Anmeldung keine einzige Stelle, die ein anderes Anfrageverfahren als Lesen an Proxmox schickt. + + +Zuerst die Tests aus `` in `proxmox-client.service.spec.ts` schreiben (rot), dann +implementieren. Die `undici`-Attrappe wie in `icon-discovery.service.spec.ts`. + +`proxmox-auth.ts` waechst um den Ticket-Zweig und bleibt dabei die EINZIGE Stelle, die +Kopfzeilen und Cookies baut (D-03, key_link): `buildTokenAuthHeader` wie in Aufgabe 1, neu +`loginTicket(server, password)` und `buildTicketCookieHeader(productType, ticket)`. Die +Cookie-Namen je Produkt stehen als benannte Konstante in dieser einen Datei, mit deutschem +Kommentar, dass die Namen fuer PBS und PMG aus der Recherche nur abgeleitet sind (Annahme A2) +und der Nutzer sie an seinen echten Servern bestaetigt — steht dort ein anderer Name, ist es +genau diese eine Konstante, die angepasst wird. + +`loginTicket` ist die EINZIGE Stelle im Modul, die eine nicht-lesende Anfrage an Proxmox +schickt, und sie aendert dort nichts — sie holt nur einen Nachweis ab (D-01). Diesen +Sonderstatus als deutschen Kommentar in der Datei festhalten. + +`proxmox-client.service.ts` bekommt die Fehler-Uebersetzung: eine reine Funktion +`classifyFailure(status, thrownError)`, die genau die sieben Werte aus `ProxmoxErrorKind` +liefert, und eine Funktion `parseJsonLenient(text)`, die bei nicht-JSON kein Werfen zulaesst +sondern das Scheitern meldet. Die Zertifikatserkennung laeuft ueber die bekannten +Fehlerkennungen von Node/undici (selbstsigniert, abgelaufen, Name passt nicht, unbekannter +Aussteller) — im Zweifel `'zertifikat'` nur bei eindeutigem Treffer, sonst `'netz'`. +Zusaetzlich `errorDetail` als KURZE, deutsche Ergaenzung (Statuszahl, Fehlerkennung), aus der +niemals ein Geheimnis hervorgeht; die Weiterverarbeitung des `errors`-Feldes der Proxmox-Antwort +ist erlaubt, aber gekuerzt auf 500 Zeichen. + +Die Ticket-Erneuerung sitzt in `proxmox.service.ts` (nicht im Klienten): ein Zaehler, der genau +einen zweiten Versuch erlaubt. Der Grund als Kommentar: bei Ticketdauer von zwei Stunden +erzeugt ein normaler Ablauf sonst alle zwei Stunden einen Fehlalarm. + +`dto/proxmox-server.dto.ts` bekommt die produktabhaengige Pruefung (PMG plus Token ist +ungueltig) — Pflichtfelder je nach `authMethod` mit `@ValidateIf`, damit ein Token-Zugang +`tokenId` und Geheimnis verlangt und ein Passwort-Zugang `username` und Passwort. + +`proxmox-nur-lesen.spec.ts` ist der maschinelle Riegel zu D-01, gebaut nach dem Vorbild von +`apps/api/src/prisma/rls-access-inventory.spec.ts` (Test liest den Quelltext, nicht das +Laufzeitverhalten): er liest alle `.ts`-Dateien unter `apps/api/src/proxmox`, entfernt vor +dem Zaehlen Kommentarzeilen und Zeichenkettenliterale aus Testdateien, und prueft zwei +Aussagen — erstens, dass die Summe der Stellen, die ein Anfrageverfahren an `undiciFetch` +uebergeben, genau EINS ist und in `proxmox-auth.ts` liegt; zweitens, dass jeder gegen einen +Proxmox-Pfad gebaute Aufruf ausser dieser einen ueber `proxmoxGet` laeuft. Die erwartete Zahl +steht als benannte Konstante mit ausgeschriebener Begruendung in der Testdatei, damit eine +spaetere Erhoehung eine bewusste Entscheidung erzwingt und nicht unbemerkt durchrutscht. + + + pnpm --filter @tessera/api exec vitest run src/proxmox + pnpm --filter @tessera/api test + pnpm type-check + + +Alle Punkte aus `` sind je durch mindestens einen Test belegt. +`proxmox-nur-lesen.spec.ts` ist gruen und wuerde rot, wenn irgendwo im Modul eine zweite +nicht-lesende Anfrage an Proxmox entstuende. `pnpm --filter @tessera/api test` gruen mit +mindestens 1240 Tests, `pnpm type-check` 4 von 4. + + + + + Aufgabe 3: PBS und PMG auswerten — nachsichtig gegen jede Antwortform + +apps/api/src/proxmox/proxmox-normalize.ts, +apps/api/src/proxmox/proxmox.service.ts, +apps/api/src/proxmox/proxmox.types.ts, +apps/api/src/proxmox/proxmox-normalize.spec.ts + + +- PVE: aus `/api2/json/cluster/resources` entstehen Knotenzahl, Zahl laufender und gestoppter Gaeste, je Knoten Prozessorlast und Speicherbelegung, je Speicherort Belegung. +- PBS: aus `/api2/json/status/datastore-usage` entsteht je Datenspeicher Gesamt, Belegt, Frei; aus `/api2/json/admin/datastore/{store}/snapshots` je Datenspeicher der Zeitpunkt der letzten Sicherung und das Ergebnis der letzten Pruefung. +- PMG: aus `/api2/json/statistics/mail` entstehen die Tageszahlen eingehend, ausgehend, Spam, Viren. +- Fehlt ein erwartetes Feld vollstaendig, ist der Einzelwert `null` — nie `0`, nie `NaN`, nie ein Wurf. +- Kommt eine Zahl als Zeichenkette (`"42"`, `"0.37"`), wird sie als Zahl gelesen; kommt sie als nicht umwandelbarer Text, ist der Wert `null`. +- Ist die gesamte Antwort eine Zeichenkette, ein Array statt eines Objekts, `null` oder leer, entsteht ein leeres Messwertobjekt mit `errorKind: 'antwortform'` — nie ein Wurf. +- Heisst ein Feld anders als erwartet, bleibt der zugehoerige Einzelwert `null` und die gekuerzte Rohantwort bleibt in `rawSample` erhalten, damit der Nutzer am echten Server erkennt, wie das Feld wirklich heisst. +- Ein Datenspeicher ohne Sicherungen ergibt „noch keine Sicherung" und keinen Fehler. +- Ein PBS-Server mit vielen Datenspeichern erzeugt hoechstens 10 Folgeabfragen je Durchlauf. + + +Zuerst `proxmox-normalize.spec.ts` schreiben (rot), mit ERFUNDENEN Antworten in der von der +Recherche dokumentierten Form — es gibt in dieser Umgebung keinen echten Proxmox-Server, und +es wird auch keiner angefragt. Je Punkt aus `` mindestens ein Fall, und zusaetzlich +je Produkt ein Fall „Feld fehlt", „Zahl kommt als Zeichenkette" und „Antwort ist HTML statt +JSON". + +`proxmox-normalize.ts` traegt die nachsichtigen Leser als reine Funktionen ohne +Datenbankbezug: `readNumber(value)` (Zahl, umwandelbare Zeichenkette, sonst `null`), +`readText(value)`, `readBool(value)` und `readList(value)` (liefert bei allem, was kein Array +ist, eine leere Liste). Darauf setzen `normalizePve(body)`, `normalizePbs(usage, snapshots)` +und `normalizePmg(body)` auf. Keine dieser Funktionen wirft jemals — der gesamte Umgang mit +einer unerwarteten Form ist ein Rueckgabewert, nicht eine Ausnahme; als deutscher Kommentar +festhalten, warum: der Nutzer prueft dieses Modul allein an seinen echten Servern, und ein Wurf +wuerde ihm eine leere Seite statt eines Hinweises zeigen. + +Die Feldnamen von PBS und PMG sind aus der Recherche nur abgeleitet (Annahmen A2, A3, A5). In +`proxmox-normalize.ts` je Produkt eine benannte Konstante mit den erwarteten Feldnamen und +einem deutschen Kommentar, dass genau diese Liste anzupassen ist, falls der echte Server +andere Namen liefert — dadurch gibt es EINE Stelle zum Nachziehen statt verstreuter +Zeichenketten im Auswertungscode. Wo ein Feld unter mehreren plausiblen Namen auftreten kann, +darf die Konstante mehrere Namen in Reihenfolge nennen, und der Leser nimmt den ersten +vorhandenen. + +`proxmox.service.ts` waechst um die produktabhaengige Abfragefolge: `pve` eine Abfrage, `pbs` +die Belegungsabfrage plus je Datenspeicher hoechstens zehn Folgeabfragen (Deckel als benannte +Konstante mit Begruendung), `pmg` eine Abfrage. Jede dieser Abfragen laeuft ueber `proxmoxGet` +— keine neue Aufrufform (Riegel aus Aufgabe 2 bleibt gruen). Das Zwischenlager bekommt je +Produkt seine Messwertform; `ProxmoxMetrics` in `proxmox.types.ts` als unterscheidbare Union +ueber `productType`, damit das Frontend typsicher verzweigen kann. + + + pnpm --filter @tessera/api exec vitest run src/proxmox + pnpm --filter @tessera/api test + pnpm type-check + + +Alle Punkte aus `` sind je durch mindestens einen Test belegt, einschliesslich der +vier ausdruecklich verlangten Fehlformen (Feld fehlt, Zahl als Zeichenkette, HTML statt JSON, +Statuscodes 401/403/404/500 — Letztere aus Aufgabe 2 weiterhin gruen). +`proxmox-nur-lesen.spec.ts` bleibt gruen. `pnpm --filter @tessera/api test` gruen mit +mindestens 1240 Tests, `pnpm type-check` 4 von 4. + + + + + Aufgabe 4: Hintergrundabfrage je Mandant und der Knopf „Verbindung testen" + +apps/api/src/proxmox/proxmox-scheduler.service.ts, +apps/api/src/proxmox/proxmox.service.ts, +apps/api/src/proxmox/proxmox.controller.ts, +apps/api/src/proxmox/proxmox.module.ts, +apps/api/src/proxmox/proxmox-scheduler.service.spec.ts, +apps/api/src/prisma/rls-access-inventory.spec.ts, +docs/mandantentrennung-zugriffsklassifikation.md + + +- Beim Start registriert der Planer je Mandant mit mindestens einem aktiven Server genau einen Auftrag unter dem Registry-Namen `proxmox-poll:`. +- Der Tick eines Mandanten geht ueber dessen Server und fragt jeden einzeln ab; ein fehlgeschlagener Server bricht die Schleife nicht ab. +- Ein zweiter Mandant verdraengt den Auftrag des ersten nicht — beide Auftraege bestehen nebeneinander. +- Keine aktiven Server bedeutet: kein Auftrag, ein Protokolleintrag, kein Fehler, nichts geloescht. +- Nach dem Speichern eines Servers zieht der Controller den Auftrag dieses Mandanten sofort nach — ohne Neustart. +- Der Planer haengt an `onApplicationBootstrap`, nicht an `onModuleInit`. +- Ein Fehler beim Start wird gefangen und protokolliert, nie weitergeworfen — die Anwendung startet trotzdem. +- `POST servers/:id/test` liefert bei Erfolg eine Erfolgsmeldung und bei Misserfolg genau einen der sieben Fehlerschluessel samt kurzer Ergaenzung, ohne den Zwischenlagerstand zu ueberschreiben. +- `POST servers/:id/poll` verweigert einen zweiten Durchlauf innerhalb von zehn Sekunden und liefert stattdessen den vorhandenen Zwischenlagerstand. + + +Zuerst `proxmox-scheduler.service.spec.ts` schreiben (rot) — Vorbild +`dkv-scheduler.service.spec.ts`, je Aussage aus `` ein Test. + +`proxmox-scheduler.service.ts` kombiniert die zwei Bestandsmuster (Recherche, Block 3): das +Mandanten-Auffaechern von `DkvSchedulerService` (ein Auftrag je Mandant, Registry-Name mit +Mandantenkennung als Suffix — die Vorgaengerform mit EINEM Auftragsfeld war genau der Fehler +WINDOWS #21) und die Lebenszyklus-Stufe von `TenderSchedulerService` +(`implements OnApplicationBootstrap`). Den Grund fuer `onApplicationBootstrap` als deutschen +Kommentar uebernehmen: die Reihenfolge der `onModuleInit`-Haken zwischen Modulen ist nicht +festgelegt, und die Erfahrung „frische Datenbank ingestiert nichts bis zum zweiten Neustart" +steht bereits im Projektgedaechtnis. Die Aufloesung von `CronJob` ueber `require('cron')` +samt Kommentar woertlich aus `dkv-scheduler.service.ts` uebernehmen (pnpm-Isolation). + +Anders als bei DKV ist ein Mandant NICHT gleich ein Server: der Tick eines Mandanten geht ueber +dessen Serverzeilen. Das Abfrageintervall eines Mandanten ist das kleinste `pollIntervalMin` +seiner aktiven Server. Ein fehlgeschlagener Server schreibt seinen Fehler ins Zwischenlager +und die Schleife laeuft weiter — dieser Punkt ausdruecklich als Test. + +Der Startpfad `loadActiveServersForScheduler()` in `proxmox.service.ts` ist der EINZIGE +Systemkontext-Aufruf des Moduls: `const systemPrisma = forSystem(this.prisma);`, nur lesend, +ohne `include` auf das Zwischenlager (die Zwischenlagertabelle hat bewusst keine +Systemlese-Regel — das Nachziehen laeuft je Zeile gebunden). Danach wird je Mandant und je +Server ueber `forTenant(this.prisma, tenantId)` geschrieben, Muster +`DkvSchedulerService`/`DashboardImagesService` (einmal lesen, viele bedienen). Diesen einen +Aufruf in `FORSYSTEM_ALLOWED_CALL_SITES` in `apps/api/src/prisma/rls-access-inventory.spec.ts` +eintragen (`apps/api/src/proxmox/proxmox.service.ts` mit Anzahl 1) und den Kopfkommentar +derselben Datei um den neuen Fall ergaenzen, wie es die bestehenden sieben Faelle vormachen — +sonst schlaegt der Waechter „ein Anfrageweg darf den Systemkontext nie rufen" fehl. In +`docs/mandantentrennung-zugriffsklassifikation.md` den Stand der Zeile +`proxmox.service.ts`/`proxmoxServer` von `gebunden` auf `system-gebunden` heben, mit derselben +Begruendungsform wie bei `dashboard-images.service.ts`; Bereichs- und Summenzeilen mit der +Gate-Schleife nachmessen. + +`proxmox.controller.ts` bekommt `POST servers/:id/test` (ADMIN/SUPER_ADMIN) — es benutzt +denselben Klienten und dieselbe Fehleruebersetzung wie der Planer, schreibt aber NICHT ins +Zwischenlager, damit ein Testklick den zuletzt gemessenen Stand nicht ueberschreibt (Vorbild +`TenderEmailConfigService.testConnection` und der LDAP-Test). Zusaetzlich ruft der Controller +nach jedem erfolgreichen Anlegen und Speichern `scheduler.setInterval(tenantId)` — Vorbild +`DkvController`. `POST servers/:id/poll` bekommt die Zehn-Sekunden-Sperre als Schutz davor, +dass ein Klick in der Oberflaeche zu ungebremsten Anfragen gegen die Fremd-API wird +(Bedrohungsmodell T-DHH-06). + +`proxmox.module.ts` nimmt den Planer in `providers` auf; `ScheduleModule` ist bereits global +in `app.module.ts` registriert — nichts zusaetzlich einzurichten. + + + pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-access-inventory.spec.ts src/prisma/rls-coverage.spec.ts + pnpm --filter @tessera/api test + pnpm type-check + + +Alle Punkte aus `` sind je durch mindestens einen Test belegt. +`rls-access-inventory.spec.ts` und `rls-coverage.spec.ts` sind gruen, einschliesslich des +neuen Erlaubnislisten-Eintrags und der nachgezogenen Dokumentationszeilen. +`proxmox-nur-lesen.spec.ts` bleibt gruen. `pnpm --filter @tessera/api test` gruen mit +mindestens 1240 Tests, `pnpm type-check` 4 von 4. + + + + + Aufgabe 5: Einstellungsseite — Server anlegen, bearbeiten, loeschen, testen + +apps/api/src/proxmox/proxmox.controller.ts, +apps/api/src/proxmox/proxmox.service.ts, +apps/api/src/proxmox/proxmox.service.spec.ts, +apps/web/src/lib/proxmox-api.ts, +apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx, +apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx, +apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx, +apps/web/src/messages/de.json, +apps/web/src/messages/en.json + + +- Bei Typ `pmg` erscheint die Auswahl „API-Token" im Formular gar nicht; nur Benutzer und Passwort sind zu sehen. +- Bei Typ `pve` oder `pbs` und Auswahl „API-Token" erscheinen Token-Kennung und Token-Geheimnis; bei Auswahl „Benutzer/Passwort" stattdessen Benutzer und Passwort. +- Ein gespeichertes Geheimnis wird beim Bearbeiten nie im Klartext angezeigt; das Feld ist leer und ein leer gelassenes Feld laesst das gespeicherte Geheimnis unveraendert. +- Der Schalter fuer die Zertifikatspruefung steht beim Anlegen auf „pruefen" und traegt einen erklaerenden Hinweis, dass die Ausnahme nur fuer diesen einen Server gilt. +- Der Knopf „Verbindung testen" zeigt bei Erfolg eine gruene Bestaetigung und bei Misserfolg den Klartext der Ursache in der Sie-Form. +- Ein Benutzer ohne Verwaltungsrolle sieht die Einstellungsseite nicht, sondern einen Hinweis. +- Loeschen verlangt eine Rueckfrage und entfernt Server samt Zwischenlagerzeile. + + +Zuerst `ServerForm.test.tsx` schreiben (rot), Vorbild +`modules/tender-radar/settings/components/EmailAlertConfigForm.test.tsx` und +`modules/dkv-fleet/settings/components/InboxConfigForm.tsx`. + +Backend: `proxmox.controller.ts` und `proxmox.service.ts` um `PUT servers/:id` und +`DELETE servers/:id` ergaenzen, beide mit `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` und beide +ueber `forTenant()`. Beim Aendern gilt dieselbe Regel wie bei +`LdapConfigService.updateConfig`: ein NICHT gesendetes Geheimnisfeld laesst den gespeicherten +Wert unveraendert, eine LEERE Zeichenkette bedeutet „loeschen" und ein gefuellter Wert wird +neu verschluesselt. Die Ablehnung „PMG mit Token" gilt auch hier. Das Loeschen entfernt die +Zwischenlagerzeile ueber die Fremdschluesselregel mit Loeschweitergabe und zieht anschliessend +den Auftrag des Mandanten nach. + +Frontend: `settings/page.tsx` nach dem Muster von +`modules/tender-radar/settings/page.tsx` — Rollenpruefung ausschliesslich zur Anzeige, mit +Ladezustand solange die Rolle unbekannt ist, damit die Verwaltungsteile fuer einen normalen +Benutzer nie kurz aufblitzen; der verbindliche Riegel bleibt serverseitig. Darin die +Serverliste und das Formular `ServerForm.tsx`: Name, Typ (drei Knoepfe oder Auswahl), +Adresse, Zugangsart, die typabhaengigen Zugangsfelder, Abfrageintervall, Schalter fuer die +Zertifikatspruefung, aktiv/inaktiv. Der Knopf „Verbindung testen" ruft +`POST servers/:id/test` und zeigt das Ergebnis direkt beim Formular. Die Uebersetzung der +sieben Fehlerschluessel liegt im Frontend unter `proxmox.errors.*` — deutsche Texte in der +Sie-Form (D-10), englische Entsprechungen in `en.json`; die Texte nennen die Ursache und den +naechsten Schritt, ohne Fachbegriffe (Beispielform fuer `zugang`: „Der Zugang wurde +abgelehnt. Bitte pruefen Sie Benutzername und Passwort beziehungsweise die Token-Angaben."). +`proxmox-api.ts` bekommt `createServer`, `updateServer`, `deleteServer`, `testServer`, +`pollServer`. + +Biome-Warnungen in `apps/web` muessen danach exakt 53 bleiben — neue Formulareingaben brauchen +daher von Anfang an die im Bestand ueblichen Beschriftungsbezuege und Tastaturbedienbarkeit. + + + pnpm --filter @tessera/web exec vitest run proxmox + pnpm --filter @tessera/api test + pnpm --filter @tessera/web test + pnpm lint + pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings' + + +Alle Punkte aus `` sind je durch mindestens einen Test belegt. +`pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests, +`pnpm --filter @tessera/api test` gruen mit mindestens 1240 Tests, `pnpm lint` 5 von 5, und +`biome lint` in `apps/web` meldet unveraendert 53 Warnungen. + + + + + Aufgabe 6: Modulseite — Serverliste mit Auslastung, Klartext bei Stoerungen + +apps/web/src/app/(portal)/modules/proxmox/page.tsx, +apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx, +apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx, +apps/web/src/lib/proxmox-api.ts, +apps/web/src/messages/de.json, +apps/web/src/messages/en.json + + +- Ohne eingetragenen Server zeigt die Seite einen ruhigen Hinweis mit dem Weg zu den Einstellungen — keine Fehlermeldung, keine leere Flaeche. +- Ein PVE-Server zeigt Knotenzahl, laufende und gestoppte Gaeste sowie je Knoten Prozessorlast und Speicherbelegung. +- Ein PBS-Server zeigt je Datenspeicher Belegung, letzte Sicherung und Ergebnis der letzten Pruefung. +- Ein PMG-Server zeigt die Tageszahlen eingehend, ausgehend, Spam und Viren. +- Ein Messwert, der `null` ist, erscheint als „unbekannt" — nie als `0`, nie als leeres Feld, nie als `NaN`. +- Ein Server mit `reachable: false` zeigt den Klartext seiner Ursache und daneben den Zeitpunkt der letzten erfolgreichen Messung, falls es eine gab. +- Der Zeitpunkt der letzten Abfrage steht bei jedem Server. +- Zaehlerfelder sind ausdruecklich als „gesamt seit Start" beschriftet, nicht als aktueller Durchsatz. +- Der Knopf „Jetzt aktualisieren" loest eine Abfrage aus und laedt danach die Liste neu; waehrend des Laufs ist er gesperrt. + + +Zuerst `ServerCard.test.tsx` schreiben (rot) — je Punkt aus `` ein Fall, mit +erfundenen Zwischenlagerstaenden je Produkttyp, einschliesslich eines Standes, in dem jeder +Einzelwert `null` ist. + +`ServerCard.tsx` ist die Anzeige EINES Servers und verzweigt ueber `productType` auf der +unterscheidbaren Union aus Aufgabe 3. Eine gemeinsame kleine Hilfe stellt jeden Einzelwert +dar: ist er `null` oder `undefined`, erscheint der uebersetzte Text „unbekannt"; sonst der +Wert mit seiner Einheit (Prozentwerte gerundet, Byte-Werte in lesbarer Form). Diese Hilfe ist +die einzige Stelle, die einen Messwert in Text verwandelt — dadurch kann kein Zweig versehentlich +eine `0` anzeigen, wo nichts gemessen wurde. Den Grund als deutschen Kommentar festhalten: die +Feldnamen von PBS und PMG sind bis zur Pruefung am echten Server nur abgeleitet, und ein still +falscher Wert waere schlimmer als ein ehrliches „unbekannt". + +Die Zaehlerfelder aus `cluster/resources` sind kumulative Werte seit dem Start eines Gastes, +keine Rate (Recherche, Fallstricke) — die Beschriftung sagt das ausdruecklich, damit der +Nutzer sie nicht als aktuellen Durchsatz liest. + +`page.tsx` zeigt die Serverliste, oben den Knopf „Jetzt aktualisieren", und fuer Benutzer mit +Verwaltungsrolle einen Verweis auf die Einstellungsseite. Bei leerer Liste der ruhige Hinweis. +Schlaegt der Listenabruf selbst fehl, erscheint eine einzelne verstaendliche Meldung, nicht +mehrere. Alle Texte ueber `useTranslations('proxmox')` in `de.json` UND `en.json`, deutsch in +der Sie-Form (D-10). + +Biome-Warnungen in `apps/web` bleiben exakt 53. + + + pnpm --filter @tessera/web exec vitest run proxmox + pnpm --filter @tessera/web test + pnpm type-check + pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings' + + +Im Browser `/modules/infrastructure/proxmox` oeffnen: ohne Server steht dort der ruhige +Hinweis; nach dem Anlegen eines Servers in den Einstellungen erscheint er in der Liste, und +ein absichtlich falsch eingetragener Zugang zeigt Klartext statt einer leeren Flaeche. + + +Alle Punkte aus `` sind je durch mindestens einen Test belegt. +`pnpm --filter @tessera/web test` gruen mit mindestens 693 Tests, `pnpm type-check` 4 von 4, +`biome lint` in `apps/web` unveraendert 53 Warnungen. + + + + + Aufgabe 7: Dokumentation und Nachmessung aller Tore + +docs/anleitung-entwicklung.md, +docs/anwenderhandbuch.md, +docs/mandantentrennung-zugriffsklassifikation.md + + +`docs/anwenderhandbuch.md` bekommt einen Abschnitt zum Proxmox-Modul in Alltagssprache und in +der Sie-Form (D-10): was das Modul zeigt, wie ein Server in den Einstellungen angelegt wird +(Name, Typ, Adresse, Zugang), welche NUR-LESE-Rolle im jeweiligen Produkt zu vergeben ist +(PVE `PVEAuditor`, PBS `Audit` beziehungsweise `DatastoreAudit`, PMG `Auditor`), dass bei PMG +nur Benutzer und Passwort moeglich sind, wozu der Schalter fuer die Zertifikatspruefung da ist +und dass er nur fuer genau diesen einen Server gilt, was der Knopf „Verbindung testen" sagt +und was „unbekannt" bei einem Messwert bedeutet. Ausdruecklich festhalten: Tessera veraendert +bei Proxmox nichts, es schaut nur zu (D-01). + +`docs/anleitung-entwicklung.md` bekommt im Abschnitt „So entsteht ein neues Modul" einen +Hinweis auf `proxmox` als Vorlage fuer ein Modul mit Fremdsystem-Zugaengen und +Hintergrundabfrage, und an geeigneter Stelle den Merksatz zur `undici`-Falle (globales `fetch` +ignoriert einen Dispatcher aus dem npm-Paket), falls er dort noch nicht steht. + +`docs/mandantentrennung-zugriffsklassifikation.md` abschliessend nachziehen: den neuen Bereich +`proxmox` als eigene Zeile in der Bereichsuebersicht und die Summenzeile — beides mit der +Gate-Schleife NACHGEMESSEN, nicht abgeschrieben, und mit dem Auftragskuerzel `260923-dhh` +versehen wie die bestehenden Eintraege. + +Danach alle Tore einmal vollstaendig durchlaufen und die Endzahlen in der Zusammenfassung +gegen die Ausgangswerte aus dem `` stellen: api-Tests, web-Tests, type-check, +lint, Biome-Warnungen in `apps/web`. Eine Verschlechterung an irgendeinem Tor ist ein +Abbruchgrund, keine Randnotiz. + + + pnpm --filter @tessera/api test + pnpm --filter @tessera/web test + pnpm type-check + pnpm lint + pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings' + + +Anwenderhandbuch und Entwicklungsanleitung beschreiben das Modul; die Klassifikationstabelle +ist nachgemessen und `rls-access-inventory.spec.ts` gruen. Endzahlen dokumentiert: +api-Tests gruen und mindestens 1240, web-Tests gruen und mindestens 693, type-check 4 von 4, +lint 5 von 5, Biome-Warnungen in `apps/web` exakt 53. + + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Browser -> Tessera-API | Der Administrator sendet Serveradressen und Zugangsdaten; jeder Benutzer mit Modulfreigabe liest die Serverliste | +| Tessera-API -> Proxmox (PVE/PBS/PMG) | Ausgehende Verbindung in das interne Netz mit einem Geheimnis im Gepaeck; Gegenstelle ist nicht von Tessera kontrolliert | +| Tessera-API -> PostgreSQL | Verschluesselte Zugangsdaten und Messwerte; Mandantentrennung ueber RLS | +| Mandant A -> Mandant B | Zwei Mandanten duerfen die Proxmox-Zugaenge des jeweils anderen nie sehen | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan | +|-----------|----------|-----------|----------|-------------|-----------------| +| T-DHH-01 | Information Disclosure | Zugangsdaten in Antwort, Protokoll und Fehlermeldung | critical | mitigate | Aufgabe 1: `listWithStatus` waehlt `encryptedTokenSecret`/`encryptedPassword` per `select` gar nicht erst aus (nicht nachtraeglich maskiert). Aufgabe 2: `proxmox-auth.ts` protokolliert nie, `errorDetail` traegt nur Statuszahl und Fehlerkennung, `rawSample` ist auf 20 000 Zeichen gekuerzt und enthaelt nur Antwortdaten, nie die gesendete Kopfzeile. Test in Aufgabe 1/2: keine Geheimnisform in Antwort und Meldung | +| T-DHH-02 | Spoofing / SSRF | Vom Administrator eingetragene Adresse | high | mitigate | Nur ADMIN/SUPER_ADMIN duerfen Adressen eintragen (`@Roles` auf allen Schreibwegen, Aufgabe 1/5) — damit ist jede Adresse eine bewusste Freigabe (D-02). Adressform per `@IsUrl` auf `http`/`https` begrenzt. BEWUSST KEINE Privat-IP-Sperre wie `isPublicHttpUrl`: Proxmox steht per Definition im privaten Netz, eine solche Sperre wuerde das Modul unbrauchbar machen; die Begruendung steht als Kommentar in `proxmox-client.service.ts`. Abbruch nach 8 Sekunden begrenzt den Missbrauch als Portscanner | +| T-DHH-03 | Information Disclosure | Zertifikats-Ausnahme reicht weiter als gewollt | high | mitigate | Aufgabe 1: Dispatcher wird JE AUFRUF aus dem Feld `tlsRejectUnauthorized` genau dieser Serverzeile gebaut; Voreinstellung `true`. Keine Modulkonstante, keine Node-Umgebungsvariable. Test: bei `true` wird kein Dispatcher uebergeben, bei `false` genau einer mit abgeschalteter Pruefung — und die Ausnahme eines Servers wirkt nicht auf einen zweiten | +| T-DHH-04 | Elevation of Privilege | Fremder Mandant liest Proxmox-Zugaenge | critical | mitigate | Aufgabe 1: `tenantId` auf beiden Tabellen, `tenant_isolation_policy` in der Migration, jeder Zugriff ueber `forTenant()`. Aufgabe 4: der einzige `forSystem()`-Aufruf ist der Startpfad des Planers, in `FORSYSTEM_ALLOWED_CALL_SITES` eingetragen und rein lesend; geschrieben wird je Zeile gebunden. Gates: `rls-coverage.spec.ts`, `rls-access-inventory.spec.ts` | +| T-DHH-05 | Elevation of Privilege | Rechteausweitung ueber das Modul | high | mitigate | Aufgabe 1: `@UseModule('proxmox')` auf Klassenebene (Aktivierung UND Freigabe, D-09), zusaetzlich `@Roles(ADMIN, SUPER_ADMIN)` auf jedem Schreibweg. `tenantId` und Rolle kommen ausschliesslich aus dem geprueften Sitzungsnachweis, nie aus Body oder Query. Die Rollenpruefung im Frontend (Aufgabe 5) ist reine Anzeige und ersetzt nichts | +| T-DHH-06 | Denial of Service | Ungebremster Nutzer-Auslöser gegen die Fremd-API | medium | mitigate | Aufgabe 4: `POST servers/:id/poll` sperrt einen zweiten Durchlauf innerhalb von zehn Sekunden und liefert stattdessen den Zwischenlagerstand. Regulaer fragt ausschliesslich der Planer mit begrenzter Frequenz ab; jede Anzeige liest aus dem Zwischenlager (D-05). Aufgabe 3: Deckel von zehn Folgeabfragen je PBS-Durchlauf | +| T-DHH-07 | Tampering | Ein veraendernder Weg gegen Proxmox entsteht (heute oder spaeter) | high | mitigate | Aufgabe 1: nur eine Datenabruf-Funktion `proxmoxGet`, Verfahren fest verdrahtet. Aufgabe 2: `proxmox-nur-lesen.spec.ts` zaehlt maschinell nach, dass die einzige nicht-lesende Anfrage die Ticket-Anmeldung ist, mit benannter Erwartungszahl und ausgeschriebener Begruendung — eine spaetere Erhoehung erzwingt eine bewusste Entscheidung (D-01) | +| T-DHH-08 | Tampering | Zwischenlager zeigt still einen Falschwert | medium | mitigate | Aufgabe 3: jeder Einzelwert wird nachsichtig gelesen und ist bei fehlendem oder unbrauchbarem Feld `null`; Aufgabe 6: `null` erscheint als „unbekannt", nie als `0`. Die gekuerzte Rohantwort bleibt erhalten, damit der Nutzer am echten Server erkennt, wie ein Feld wirklich heisst | +| T-DHH-SC | Tampering | Paketinstallationen | low | accept | Dieser Auftrag installiert kein einziges Paket (D-07) — `undici` ist bereits direkte Abhaengigkeit von `apps/api`. Die Paket-Pruefliste der Recherche weist den Punkt ausdruecklich als nicht anwendbar aus. Entsteht wider Erwarten doch eine Installation, greift die Paket-Pruefung vor dem Einbau | + + + +## Mehrfachquellen-Abdeckung + +**GOAL** (Auftragsbeschreibung) + +| Punkt | Status | Abgedeckt durch | +|---|---|---| +| PVE, PBS und PMG anbinden | COVERED | Aufgabe 1 (PVE), Aufgabe 3 (PBS, PMG) | +| Nur beobachten | COVERED | Aufgabe 1 (`proxmoxGet`), Aufgabe 2 (`proxmox-nur-lesen.spec.ts`) | +| Server in den Einstellungen anlegen (Adresse + Zugang) | COVERED | Aufgabe 1 (Anlegen), Aufgabe 5 (Oberflaeche, Bearbeiten, Loeschen) | +| Zugang Token oder Benutzer/Passwort, verschluesselt | COVERED | Aufgabe 1 (Token), Aufgabe 2 (Benutzer/Passwort), beide ueber `CryptoService` | +| Abfrage im Hintergrund mit Zwischenlager | COVERED | Aufgabe 1 (Zwischenlagertabelle), Aufgabe 4 (Planer) | +| Modulseite mit Serverliste und Auslastung | COVERED | Aufgabe 1 (duenne Liste), Aufgabe 6 (Auslastung je Produkt) | + +**RESEARCH** (`260923-dhh-RESEARCH.md`) + +| Punkt | Status | Abgedeckt durch | +|---|---|---| +| Token-Kopfzeilen je Produkt, PMG ohne Token (A1) | COVERED | Aufgabe 1 und 2 (`proxmox-auth.ts`), Aufgabe 5 (Formular bietet es bei PMG nicht an) | +| Ticket-Anmeldung, Cookie-Namen je Produkt (A2) | COVERED | Aufgabe 2, Cookie-Namen als EINE benannte Konstante mit Annahme-Kommentar | +| Kein CSRF noetig, weil nur gelesen wird | COVERED | Aufgabe 2 (``) | +| `cluster/resources` als eine Abfrage fuer PVE | COVERED | Aufgabe 1 und 3 | +| PBS-Belegung und Snapshot-Felder (A3) | COVERED | Aufgabe 3, Feldnamen als EINE benannte Konstante | +| PMG-Tageszahlen (A5, keine Quarantaene) | COVERED | Aufgabe 3; Quarantaene bleibt ausserhalb des Umfangs | +| Fehlerverhalten 401 breiter als ueblich (A4) | COVERED | Aufgabe 2 (`classifyFailure`) | +| undici-Dispatcher-Falle unter Node 24 | COVERED | Aufgabe 1 (Kommentar und Test), Aufgabe 7 (Anleitung) | +| Pro Zeile umschaltbarer Zertifikats-Bypass | COVERED | Aufgabe 1, T-DHH-03 | +| `onApplicationBootstrap` statt `onModuleInit` | COVERED | Aufgabe 4 | +| Mandanten-Auffaechern je Cron-Auftrag | COVERED | Aufgabe 4 | +| Zwischenlager statt Live-Abfrage | COVERED | Aufgabe 1, 4, 6 | +| RLS-Migration, Klassifikationsdoku, Erlaubnisliste | COVERED | Aufgabe 1 (Migration, Doku), Aufgabe 4 (Erlaubnisliste), Aufgabe 7 (Nachmessung) | +| Nur-Lese-Rollen je Produkt als Hinweis an den Admin | COVERED | Aufgabe 7 (Anwenderhandbuch), `user_setup` im Frontmatter | +| Zaehler sind kumulativ, keine Rate | COVERED | Aufgabe 6 (Beschriftung) | +| Ticket-Erneuerung bei 401 | COVERED | Aufgabe 2 | +| Keine neue npm-Abhaengigkeit | COVERED | Aufgabe 1 (D-07), Paket-Pruefliste nicht anwendbar | + +**CONTEXT** (getroffene Entscheidungen D-01 bis D-11) + +| ID | Status | Abgedeckt durch | +|---|---|---| +| D-01 | COVERED | Aufgabe 1 (`proxmoxGet`), Aufgabe 2 (`proxmox-nur-lesen.spec.ts`), `must_haves.truths`, T-DHH-07 | +| D-02 | COVERED | Aufgabe 1 (Verschluesselung, `select` ohne Geheimnisse), Aufgabe 5 (Formular), T-DHH-01 | +| D-03 | COVERED | Aufgabe 1 und 2 (`proxmox-auth.ts` als einzige Stelle), Aufgabe 5 (Formular ohne Token bei PMG) | +| D-04 | COVERED | Aufgabe 1 (Dispatcher je Aufruf), Aufgabe 5 (Schalter), T-DHH-03 | +| D-05 | COVERED | Aufgabe 1 (Zwischenlager), Aufgabe 4 (Planer nach TENDER-Muster), Aufgabe 6 (Seite liest nur den Cache) | +| D-06 | COVERED | Aufgabe 2 (Fehlerklassen), Aufgabe 4 (Testendpunkt), Aufgabe 5 (Knopf und Klartext) | +| D-07 | COVERED | Aufgabe 1 (nur `undici`), Paket-Pruefliste nicht anwendbar | +| D-08 | COVERED | Aufgabe 1 (Migration, Doku), Aufgabe 4 (Erlaubnisliste, Standwechsel), Aufgabe 7 (Nachmessung), T-DHH-04 | +| D-09 | COVERED | Aufgabe 1 (`@UseModule`, `ModuleAccessGate`), T-DHH-05 | +| D-10 | COVERED | Aufgaben 1, 5, 6 (next-intl, Sie-Form), 7 (Anwenderhandbuch) | +| D-11 | COVERED (als Ausschluss) | ``, Abschnitt „Ausdruecklich NICHT im Umfang" | + +**Keine Luecke.** Nicht abgedeckt sind ausschliesslich die vom Auftrag ausgeschlossenen Punkte +(Dashboard-Kachel, `/rrddata`, Eingriffe, PMG-Quarantaene). + + + +Nach jeder Aufgabe (je Commit): + +- `pnpm --filter @tessera/api test` — gruen, mindestens 1240 Tests +- `pnpm --filter @tessera/web test` — gruen, mindestens 693 Tests +- `pnpm type-check` — 4 von 4 erfolgreich +- `pnpm lint` — 5 von 5 erfolgreich +- `pnpm --filter @tessera/web exec biome lint .` — exakt 53 Warnungen + +Zusaetzlich nach den Aufgaben 1 und 4: + +- `pnpm --filter @tessera/api exec vitest run src/prisma/rls-coverage.spec.ts src/prisma/rls-access-inventory.spec.ts` — gruen + +Ab Aufgabe 2 dauerhaft: + +- `pnpm --filter @tessera/api exec vitest run src/proxmox/proxmox-nur-lesen.spec.ts` — gruen + +**Was diese Tore NICHT beweisen:** die Feldnamen von PBS und PMG (Annahmen A2, A3, A5 der +Recherche). Es gibt hier keinen echten PVE-/PBS-/PMG-Server; alle Tests laufen gegen erfundene +Antworten in der dokumentierten Form. Der Nutzer prueft das Modul selbst auf `alpha` gegen +seine echten Server. Genau dafuer sind die Feldnamen je Produkt als EINE benannte Konstante +gebaut und bleibt die gekuerzte Rohantwort im Zwischenlager erhalten: weicht die Wirklichkeit +ab, ist eine einzige Stelle nachzuziehen und der Nutzer sieht in der Oberflaeche „unbekannt" +statt eines Absturzes. + + + +1. Kein Weg im gesamten Modul veraendert etwas bei Proxmox; der maschinelle Riegel + `proxmox-nur-lesen.spec.ts` weist nach, dass die einzige nicht-lesende Anfrage die + Ticket-Anmeldung ist. +2. Ein Administrator legt in den Moduleinstellungen Server aller drei Typen an; bei PMG wird + die Token-Auswahl gar nicht erst angeboten und serverseitig abgelehnt. +3. Zugangsdaten stehen verschluesselt in der Datenbank und verlassen sie auf keinem Weg im + Klartext — auch nicht in Fehlermeldungen, Protokollen oder der Rohprobe. +4. Die Zertifikats-Ausnahme gilt nur fuer die Server, bei denen sie einzeln eingeschaltet + wurde; Voreinstellung ist pruefen. +5. Der Hintergrunddienst haengt an `onApplicationBootstrap`, faechert je Mandant auf und + ueberschreibt den Auftrag eines zweiten Mandanten nicht. +6. Die Modulseite liest ausschliesslich aus dem Zwischenlager, zeigt fehlende Werte als + „unbekannt" und ohne Server einen ruhigen Hinweis. +7. Der Knopf „Verbindung testen" nennt die Ursache in Alltagssprache. +8. Beide neuen Tabellen tragen `tenantId` mit RLS-Policy; `rls-coverage.spec.ts` und + `rls-access-inventory.spec.ts` sind gruen, die Klassifikationsdoku ist nachgemessen. +9. Alle Tore mindestens auf Ausgangswert: api-Tests ab 1240, web-Tests ab 693, type-check 4/4, + lint 5/5, Biome-Warnungen in `apps/web` exakt 53. +10. Anwenderhandbuch und Entwicklungsanleitung beschreiben das Modul, einschliesslich der + NUR-LESE-Rolle je Produkt. + + + +Nach Abschluss `.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-SUMMARY.md` +schreiben — mit den gemessenen Endzahlen aller Tore neben den Ausgangswerten und einer +ausdruecklichen Liste der Stellen, die der Nutzer beim Test an seinen echten Servern +moeglicherweise nachziehen muss (Cookie-Namen je Produkt, Feldnamen je Produkt). +