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