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>
This commit is contained in:
2026-09-23 10:01:40 +02:00
parent 35c7f5a1ac
commit ec9c77956d
@@ -0,0 +1,910 @@
---
phase: quick-260923-dhh
plan: 01
type: execute
wave: 1
depends_on: []
autonomous: true
requirements: [D-01, D-02, D-03, D-04, D-05, D-06, D-07, D-08, D-09, D-10, D-11]
files_modified:
- apps/api/prisma/schema.prisma
- apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql
- apps/api/src/app.module.ts
- apps/api/src/proxmox/proxmox.types.ts
- apps/api/src/proxmox/proxmox-auth.ts
- apps/api/src/proxmox/proxmox-normalize.ts
- apps/api/src/proxmox/proxmox-client.service.ts
- apps/api/src/proxmox/proxmox.service.ts
- apps/api/src/proxmox/proxmox-scheduler.service.ts
- apps/api/src/proxmox/proxmox.controller.ts
- apps/api/src/proxmox/proxmox.module.ts
- apps/api/src/proxmox/proxmox.seed.ts
- apps/api/src/proxmox/dto/proxmox-server.dto.ts
- apps/api/src/proxmox/proxmox-client.service.spec.ts
- apps/api/src/proxmox/proxmox.service.spec.ts
- apps/api/src/proxmox/proxmox-normalize.spec.ts
- apps/api/src/proxmox/proxmox-scheduler.service.spec.ts
- apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
- apps/api/src/prisma/rls-access-inventory.spec.ts
- apps/web/src/lib/proxmox-api.ts
- apps/web/src/lib/module-loader.ts
- apps/web/src/app/(portal)/modules/proxmox/layout.tsx
- apps/web/src/app/(portal)/modules/proxmox/page.tsx
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx
- apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx
- apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.tsx
- apps/web/src/app/(portal)/modules/proxmox/settings/components/ServerForm.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- docs/mandantentrennung-zugriffsklassifikation.md
- docs/anleitung-entwicklung.md
- docs/anwenderhandbuch.md
user_setup:
- service: proxmox
why: "Nur der Nutzer hat echte PVE-/PBS-/PMG-Server. Ohne sie bleibt die Feldnamen-Annahme A2/A3 der Recherche unbestaetigt."
dashboard_config:
- task: "Je Produkt einen NUR-LESE-Zugang anlegen: PVE-Rolle PVEAuditor, PBS-Rolle Audit bzw. DatastoreAudit, PMG-Rolle Auditor"
location: "Proxmox-Oberflaeche -> Datacenter/Configuration -> Permissions"
estimate:
tokens: 320000
raw_tokens: 210000
tasks: 7
confidence: low
must_haves:
truths:
- "Kein Weg im gesamten Modul veraendert etwas bei Proxmox: die einzige Nicht-GET-Anfrage im ganzen Modul ist die Ticket-Anmeldung, und sie wird maschinell nachgezaehlt (D-01)."
- "Ein Administrator legt in den Einstellungen Server an (Name, Typ pve|pbs|pmg, Adresse, Zugang) und sieht die Zugangsdaten nie wieder im Klartext (D-02)."
- "PVE und PBS bieten API-Token ODER Benutzer/Passwort; PMG bietet nur Benutzer/Passwort — die Token-Felder erscheinen bei PMG gar nicht und ein Token-Zugang fuer PMG wird serverseitig abgelehnt (D-03)."
- "Zertifikatsfehler werden nur fuer die Server geduldet, bei denen der Administrator es einzeln eingeschaltet hat; Voreinstellung ist pruefen (D-04)."
- "Die Modulseite und jede Anzeige lesen ausschliesslich aus dem Zwischenlager, nie live bei Proxmox (D-05)."
- "Der Knopf Verbindung testen nennt die Ursache in Alltagssprache: nicht erreichbar, Zugang abgelehnt, Rechte reichen nicht, Zertifikat, unerwartete Antwort (D-06)."
- "Ein fehlendes, anders benanntes oder falsch typisiertes Feld einer Proxmox-Antwort fuehrt zu unbekannt in der Anzeige, nie zu einem Absturz, einer leeren Seite oder einem stillen Falschwert."
- "Ohne angelegten Server ist die Modulseite ruhig und erklaert, dass noch keiner eingetragen ist."
- "Beide neuen Tabellen tragen tenantId mit RLS-Policy; rls-coverage.spec.ts und rls-access-inventory.spec.ts bleiben gruen (D-08)."
artifacts:
- apps/api/prisma/migrations/20260923140000_proxmox_server/migration.sql
- apps/api/src/proxmox/proxmox-auth.ts
- apps/api/src/proxmox/proxmox-client.service.ts
- apps/api/src/proxmox/proxmox-normalize.ts
- apps/api/src/proxmox/proxmox-scheduler.service.ts
- apps/api/src/proxmox/proxmox-nur-lesen.spec.ts
- "apps/web/src/app/(portal)/modules/proxmox/page.tsx"
- "apps/web/src/app/(portal)/modules/proxmox/settings/page.tsx"
key_links:
- "proxmox-auth.ts ist die EINZIGE Stelle, die Kopfzeilen und Anmelde-Cookies je Produkt baut — Klient, Verbindungstest und Planer rufen sie, keiner baut sie nach (D-03)."
- "proxmox-client.service.ts baut den undici-Dispatcher je Aufruf aus dem Feld tlsRejectUnauthorized genau dieser Serverzeile (D-04)."
- "proxmox-scheduler.service.ts haengt an onApplicationBootstrap und faechert je Mandant auf; der Controller zieht nach jedem Speichern nach (D-05)."
- "proxmox.controller.ts traegt @UseModule('proxmox'); die Schreibwege zusaetzlich @Roles(ADMIN, SUPER_ADMIN) (D-09)."
- "Jeder Datenbankzugriff laeuft ueber forTenant(); nur der Startpfad des Planers ueber forSystem() und steht in FORSYSTEM_ALLOWED_CALL_SITES (D-08)."
---
<objective>
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.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<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
</context>
<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>
<tasks>
<task type="tracer">
<name>Aufgabe 1: Ein PVE-Server per Token — von der Tabelle bis zur Modulseite</name>
<files>
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
</files>
<precondition>
`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`.
</precondition>
<action>
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`.
</action>
<verify>
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-coverage.spec.ts src/prisma/rls-access-inventory.spec.ts</automated>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm --filter @tessera/web test</automated>
<automated>pnpm type-check</automated>
</verify>
<done>
`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.
</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 2: Zugang per Benutzer/Passwort, Fehler in Alltagssprache, Beobachtungs-Riegel</name>
<files>
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
</files>
<behavior>
- 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.
</behavior>
<action>
Zuerst die Tests aus `<behavior>` 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.
</action>
<verify>
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox</automated>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm type-check</automated>
</verify>
<done>
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.
</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 3: PBS und PMG auswerten — nachsichtig gegen jede Antwortform</name>
<files>
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
</files>
<behavior>
- 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.
</behavior>
<action>
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 `<behavior>` 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.
</action>
<verify>
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox</automated>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm type-check</automated>
</verify>
<done>
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.
</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 4: Hintergrundabfrage je Mandant und der Knopf „Verbindung testen"</name>
<files>
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
</files>
<behavior>
- Beim Start registriert der Planer je Mandant mit mindestens einem aktiven Server genau einen Auftrag unter dem Registry-Namen `proxmox-poll:<tenantId>`.
- 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.
</behavior>
<action>
Zuerst `proxmox-scheduler.service.spec.ts` schreiben (rot) — Vorbild
`dkv-scheduler.service.spec.ts`, je Aussage aus `<behavior>` 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.
</action>
<verify>
<automated>pnpm --filter @tessera/api exec vitest run src/proxmox src/prisma/rls-access-inventory.spec.ts src/prisma/rls-coverage.spec.ts</automated>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm type-check</automated>
</verify>
<done>
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.
</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 5: Einstellungsseite — Server anlegen, bearbeiten, loeschen, testen</name>
<files>
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
</files>
<behavior>
- 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.
</behavior>
<action>
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.
</action>
<verify>
<automated>pnpm --filter @tessera/web exec vitest run proxmox</automated>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm --filter @tessera/web test</automated>
<automated>pnpm lint</automated>
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
</verify>
<done>
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.
</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 6: Modulseite — Serverliste mit Auslastung, Klartext bei Stoerungen</name>
<files>
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
</files>
<behavior>
- 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.
</behavior>
<action>
Zuerst `ServerCard.test.tsx` schreiben (rot) — je Punkt aus `<behavior>` 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.
</action>
<verify>
<automated>pnpm --filter @tessera/web exec vitest run proxmox</automated>
<automated>pnpm --filter @tessera/web test</automated>
<automated>pnpm type-check</automated>
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
</verify>
<human-check>
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.
</human-check>
<done>
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.
</done>
</task>
<task type="auto">
<name>Aufgabe 7: Dokumentation und Nachmessung aller Tore</name>
<files>
docs/anleitung-entwicklung.md,
docs/anwenderhandbuch.md,
docs/mandantentrennung-zugriffsklassifikation.md
</files>
<action>
`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.
</action>
<verify>
<automated>pnpm --filter @tessera/api test</automated>
<automated>pnpm --filter @tessera/web test</automated>
<automated>pnpm type-check</automated>
<automated>pnpm lint</automated>
<automated>pnpm --filter @tessera/web exec biome lint . 2>&1 | grep -c 'Found 53 warnings'</automated>
</verify>
<done>
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.
</done>
</task>
</tasks>
<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>
<verification>
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.
</verification>
<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>
<output>
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).
</output>