Files
tessera-ctl/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-PLAN.md
T
schalli ec9c77956d 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) <noreply@anthropic.com>
2026-09-23 10:01:40 +02:00

60 KiB

phase, plan, type, wave, depends_on, autonomous, requirements, files_modified, user_setup, estimate, must_haves
phase plan type wave depends_on autonomous requirements files_modified user_setup estimate must_haves
quick-260923-dhh 01 execute 1
true
D-01
D-02
D-03
D-04
D-05
D-06
D-07
D-08
D-09
D-10
D-11
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
service why dashboard_config
proxmox Nur der Nutzer hat echte PVE-/PBS-/PMG-Server. Ohne sie bleibt die Feldnamen-Annahme A2/A3 der Recherche unbestaetigt.
task location
Je Produkt einen NUR-LESE-Zugang anlegen: PVE-Rolle PVEAuditor, PBS-Rolle Audit bzw. DatastoreAudit, PMG-Rolle Auditor Proxmox-Oberflaeche -> Datacenter/Configuration -> Permissions
tokens raw_tokens tasks confidence
320000 210000 7 low
truths artifacts key_links
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).
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
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 (<decisions_already_made>), 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.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

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

<interface_context> 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/<modul>-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/<slug>/, layout.tsx umschliesst mit <ModuleAccessGate moduleSlug="<slug>">, Eintrag in MODULE_REGISTRY (apps/web/src/lib/module-loader.ts). </interface_context>
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 <ModuleAccessGate moduleSlug="proxmox">; 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 <behavior> 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 <behavior> 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 <behavior> 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 <behavior> 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 <behavior> 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 <objective> 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.

<threat_model>

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
</threat_model>

<source_audit>

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 (<behavior>)
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) <objective>, Abschnitt „Ausdruecklich NICHT im Umfang"

Keine Luecke. Nicht abgedeckt sind ausschliesslich die vom Auftrag ausgeschlossenen Punkte (Dashboard-Kachel, /rrddata, Eingriffe, PMG-Quarantaene). </source_audit>

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.

<success_criteria>

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