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>
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 |
|
|
|
|
|
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>
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): stringunddecrypt(stored: string): string. Formativ:authTag:ciphertext, alles hex, Doppelpunkt-getrennt.CryptoModulesteht bereits inapp.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?)undforSystem(prisma)ausapps/api/src/prisma/prisma-tenant.extension.ts. Konvention: lokale Konstanteconst tenantPrisma = forTenant(this.prisma, tenantId);— keine andere Form, sonst schlaegtrls-access-inventory.spec.tsfehl.undici:import { Agent, fetch as undiciFetch } from 'undici'. Nodes globalesfetchignoriert einenAgentaus dem npm-Paket (gemessen,icon-discovery.service.ts:33-37).@UseModule(slug)ausapps/api/src/module-registry/module.guard.ts,@Roles(Role.ADMIN, Role.SUPER_ADMIN)ausapps/api/src/auth/decorators/roles.decorator.ts.ModuleRegistryService.seedModule({ slug, name, version, category, description: {de, en}, isSystem })— Vorlageapps/api/src/domaincheck/domaincheck.seed.ts.CronJobClasswird perrequire('cron').CronJobaufgeloest (pnpm-Isolation, Kommentar indkv-scheduler.service.ts:6-16woertlich 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(Vorbilddkv-api.ts),API_URLausprocess.env.NEXT_PUBLIC_API_URL,credentials: 'include'. - Modulseiten liegen unter
apps/web/src/app/(portal)/modules/<slug>/,layout.tsxumschliesst mit<ModuleAccessGate moduleSlug="<slug>">, Eintrag inMODULE_REGISTRY(apps/web/src/lib/module-loader.ts). </interface_context>
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.
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.
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.
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.
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.
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.
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>
pnpm --filter @tessera/api test— gruen, mindestens 1240 Testspnpm --filter @tessera/web test— gruen, mindestens 693 Testspnpm type-check— 4 von 4 erfolgreichpnpm lint— 5 von 5 erfolgreichpnpm --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>
- Kein Weg im gesamten Modul veraendert etwas bei Proxmox; der maschinelle Riegel
proxmox-nur-lesen.spec.tsweist nach, dass die einzige nicht-lesende Anfrage die Ticket-Anmeldung ist. - Ein Administrator legt in den Moduleinstellungen Server aller drei Typen an; bei PMG wird die Token-Auswahl gar nicht erst angeboten und serverseitig abgelehnt.
- Zugangsdaten stehen verschluesselt in der Datenbank und verlassen sie auf keinem Weg im Klartext — auch nicht in Fehlermeldungen, Protokollen oder der Rohprobe.
- Die Zertifikats-Ausnahme gilt nur fuer die Server, bei denen sie einzeln eingeschaltet wurde; Voreinstellung ist pruefen.
- Der Hintergrunddienst haengt an
onApplicationBootstrap, faechert je Mandant auf und ueberschreibt den Auftrag eines zweiten Mandanten nicht. - Die Modulseite liest ausschliesslich aus dem Zwischenlager, zeigt fehlende Werte als „unbekannt" und ohne Server einen ruhigen Hinweis.
- Der Knopf „Verbindung testen" nennt die Ursache in Alltagssprache.
- Beide neuen Tabellen tragen
tenantIdmit RLS-Policy;rls-coverage.spec.tsundrls-access-inventory.spec.tssind gruen, die Klassifikationsdoku ist nachgemessen. - Alle Tore mindestens auf Ausgangswert: api-Tests ab 1240, web-Tests ab 693, type-check 4/4,
lint 5/5, Biome-Warnungen in
apps/webexakt 53. - Anwenderhandbuch und Entwicklungsanleitung beschreiben das Modul, einschliesslich der NUR-LESE-Rolle je Produkt. </success_criteria>