docs(quick-260923-le6): Proxmox-Abnahmebefunde behoben (PMG-Teilsumme, Aktualisieren nur fuer Admins)
Tessera CI/CD / Lint & Type Check (push) Successful in 57s
Tessera CI/CD / Tests (push) Successful in 1m54s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 20s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m22s

Uebergabe-Notizen der pausierten Sitzung entfernt, die Arbeit ist wieder aufgenommen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-23 15:35:30 +02:00
parent e1b191bf0b
commit c294bfddf2
5 changed files with 397 additions and 199 deletions
@@ -0,0 +1,221 @@
---
phase: quick
plan: 260923-le6
type: execute
wave: 1
depends_on: []
files_modified:
- apps/api/src/proxmox/proxmox-normalize.ts
- apps/api/src/proxmox/proxmox-normalize.spec.ts
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- apps/web/src/app/(portal)/modules/proxmox/page.tsx
- apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx
autonomous: true
requirements: []
estimate:
tokens: 45000
raw_tokens: 45000
tasks: 3
confidence: low
must_haves:
truths:
- "PMG: fehlt von einem Paar (spamcount_in/spamcount_out bzw. viruscount_in/viruscount_out) genau eine Haelfte, ist spamCount bzw. virusCount null (Anzeige 'unbekannt') — in beide Richtungen, nie eine Teilsumme"
- "PMG: sind beide Haelften vorhanden, bleibt die Summe wie bisher (10+2 -> 12); fehlen beide, bleibt null"
- "Auf der Proxmox-Modulseite sehen nur ADMIN und SUPER_ADMIN den Knopf 'Jetzt aktualisieren'; USER (und ein noch nicht geladener Benutzer) sehen ihn nicht"
- "Ein noch nie abgefragter Server zeigt Admins weiterhin 'Noch keine Abfrage gelaufen. Klicken Sie oben auf „Jetzt aktualisieren“.'; Nicht-Admins sehen stattdessen einen Text ohne Verweis auf den Knopf"
- "Sonst aendert sich an der Modulseite nichts (Festlegung: kein Umbau, keine zusaetzlichen Details/Statusfarben)"
artifacts:
- path: apps/api/src/proxmox/proxmox-normalize.ts
provides: "sumOrNull liefert null, sobald ein Teilwert null ist"
- path: apps/api/src/proxmox/proxmox-normalize.spec.ts
provides: "Testfaelle 'nur eine Haelfte vorhanden -> null' fuer Spam und Viren, beide Richtungen"
- path: apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
provides: "optionale Eigenschaft isAdmin (Vorgabe false), waehlt den Hinweistext fuer noch nie abgefragte Server"
- path: apps/web/src/app/(portal)/modules/proxmox/page.tsx
provides: "Aktualisieren-Knopf nur fuer Admins, reicht isAdmin an ServerCard weiter"
- path: apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx
provides: "Seitentest: Knopf sichtbar fuer ADMIN/SUPER_ADMIN, unsichtbar fuer USER/null"
key_links:
- from: "apps/web/src/app/(portal)/modules/proxmox/page.tsx"
to: "ServerCard"
via: "isAdmin={isAdmin}"
pattern: "isAdmin=\\{isAdmin\\}"
- from: "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx"
to: "apps/web/src/messages/de.json proxmox.card.notPolledYetAutomatic"
via: "t('card.notPolledYetAutomatic')"
pattern: "card\\.notPolledYetAutomatic"
---
<objective>
Zwei Befunde aus der Abnahme des Proxmox-Moduls beheben, sonst nichts:
1. **PMG-Teilsumme (API):** `sumOrNull(a, b)` in `apps/api/src/proxmox/proxmox-normalize.ts` addiert heute einen fehlenden Teilwert als 0, sobald nur EINE Haelfte null ist. Dadurch zeigt die Seite z. B. „Spam: 10“ als vollstaendige Tageszahl, obwohl `spamcount_out` fehlte. Das ist der in `.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-VERIFICATION.md` (Wahrheit 7, Blocker) belegte Fehler. Kuenftig ist die Summe null, sobald ein Teilwert null ist.
2. **Aktualisieren-Knopf nur fuer Admins (Web):** Der Knopf „Jetzt aktualisieren“ erscheint heute bei allen, die Zugriff auf das Modul haben. Der Endpunkt `POST servers/:id/poll` verlangt aber `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` (`apps/api/src/proxmox/proxmox.controller.ts:89-90`), deshalb passiert beim Klick fuer alle anderen nichts. Kuenftig sehen nur ADMIN/SUPER_ADMIN den Knopf. Der Hinweis „Noch keine Abfrage gelaufen. Klicken Sie oben auf …“ darf Nicht-Admins nicht mehr auf einen Knopf verweisen, den sie nicht sehen.
**Festlegung (locked, vom Nutzer):** KEIN Umbau der Proxmox-Modulseite. Der Nutzer hat seinen Wunsch nach mehr Details bzw. Statusfarben ausdruecklich zurueckgezogen. Nur diese zwei Korrekturen, keine weiteren Anzeige-, Layout- oder Textaenderungen.
Hinweis zum Zuschnitt: Tracer-first entfaellt (wie `--no-tracer`). Es handelt sich um zwei voneinander unabhaengige Fehlerkorrekturen, jede in genau einer Schicht, ohne neue Architektur, die ein Durchstich absichern muesste. Aufgabe 1 (API) und Aufgabe 2/3 (Web) beruehren keine gemeinsamen Dateien. Aufgabe 3 braucht die Eigenschaft `isAdmin` aus Aufgabe 2.
Purpose: Das Modul soll keinen still falschen, plausibel aussehenden Wert zeigen (eigener Anspruch in `proxmox-normalize.ts` und `ServerCard.tsx`: „ein still falscher Wert waere schlimmer als ein ehrliches unbekannt“). Ausserdem soll kein Knopf erscheinen, der fuer den Betrachter wirkungslos ist.
Output: korrigierte `sumOrNull` samt Tests; `ServerCard` mit `isAdmin`-Eigenschaft und einem zweiten Hinweistext in de/en; Modulseite, die den Knopf nur Admins zeigt, samt neuem Seitentest.
</objective>
<execution_context>
@~/.claude/gsd-core/workflows/execute-plan.md
@~/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/STATE.md
@./CLAUDE.md
@.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-VERIFICATION.md
@apps/api/src/proxmox/proxmox-normalize.ts
@apps/api/src/proxmox/proxmox-normalize.spec.ts
@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/tender-radar/settings/settings-roles.test.tsx
<interfaces>
Bereits im Code vorhanden (vom Planer gelesen, nicht erneut suchen):
- `apps/web/src/lib/stores/auth-store.ts`: `useAuthStore((s) => s.user)`, `user.role` ist `'SUPER_ADMIN' | 'ADMIN' | 'USER'`.
- `page.tsx` berechnet BEREITS `const isAdmin = user?.role === 'ADMIN' || user?.role === 'SUPER_ADMIN';` (Zeile 18-19) und nutzt es fuer den Einstellungs-Link. Das ist das etablierte Muster; es wird kein neuer Mechanismus eingefuehrt.
- `apps/web/src/lib/proxmox-api.ts`: `listServers(): Promise<ProxmoxServer[]>`, `pollServer(id: string): Promise<ProxmoxTestResult>`, Typ `ProxmoxServer` (inkl. `isActive`, `pollIntervalMin`, `status: ProxmoxServerStatus | null`).
- `ServerCard` wird ausschliesslich in `page.tsx:99` verwendet (per grep geprueft).
- Test-Muster fuer Rollen: `settings-roles.test.tsx` mockt `@/lib/stores/auth-store` mit `useAuthStore: (selector) => mockAuthStore(selector)` und setzt je Fall `mockAuthStore.mockImplementation((sel) => sel({ user }))`, ausserdem `next/link` als `<a>` und `next-intl` mit handgeschriebener Uebersetzungstabelle.
- Nachrichtendateien: nur `apps/web/src/messages/de.json` und `apps/web/src/messages/en.json`. Namensraum `proxmox.card` (de.json ab Zeile 712). `umlaut-guard.spec.ts` prueft de.json auf Ersatzschreibungen (ae/oe/ue/ss) — neue deutsche Texte brauchen echte Umlaute.
- Abfragetakt: `ProxmoxServer.pollIntervalMin` Vorgabe 5, erlaubt 1–1440 (`dto/proxmox-server.dto.ts` `@Min(1) @Max(1440)`). Der Planer laeuft je Mandant im kleinsten Intervall der aktiven Server (`proxmox-scheduler.service.ts`). Inaktive Server (`isActive: false`) werden nicht automatisch abgefragt.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Aufgabe 1: PMG-Summe wird null, sobald eine Haelfte fehlt (sumOrNull)</name>
<files>apps/api/src/proxmox/proxmox-normalize.ts, apps/api/src/proxmox/proxmox-normalize.spec.ts</files>
<read_first>apps/api/src/proxmox/proxmox-normalize.ts (Zeilen 222-270), apps/api/src/proxmox/proxmox-normalize.spec.ts (Zeilen 185-240)</read_first>
<behavior>
- Nur `spamcount_in: 10` vorhanden, `spamcount_out` fehlt -> `spamCount` ist `null` (heute faelschlich 10)
- Nur `spamcount_out: 2` vorhanden, `spamcount_in` fehlt -> `spamCount` ist `null`
- Nur `viruscount_in: 1` vorhanden, `viruscount_out` fehlt -> `virusCount` ist `null`
- Nur `viruscount_out: 3` vorhanden, `viruscount_in` fehlt -> `virusCount` ist `null`
- Eine Haelfte vorhanden, die andere ist nicht lesbar (z. B. `spamcount_out: 'abc'`, `readNumber` liefert null) -> `spamCount` ist `null`
- Unabhaengigkeit der Paare: Spam unvollstaendig, Viren vollstaendig (`viruscount_in: 1, viruscount_out: 0`) -> `spamCount` null, `virusCount` 1; `countIn`/`countOut` bleiben unberuehrt
- Unveraendert gruen: die bestehenden Tests „beide vorhanden -> 12/1“, „beide fehlen -> null“, „HTML -> antwortform“
</behavior>
<action>
RED: Im bestehenden `describe('normalizePmg (Aufgabe 3, <behavior>)', ...)`-Block von `proxmox-normalize.spec.ts` neue Faelle fuer jede Zeile aus `<behavior>` ergaenzen. Das geht als einzelne `it` oder als `it.each` ueber eine Tabelle {Beschreibung, data, erwartetes spamCount, erwartetes virusCount}. Jeder Testname nennt „nur eine Haelfte vorhanden -> null“ und die Richtung (in bzw. out) sowie Spam bzw. Viren. Vorhandene Tests unveraendert lassen. Der Planer hat geprueft, dass keiner das alte Verhalten festschreibt: Die vorhandenen PMG-Tests decken nur „beide vorhanden“ und „beide fehlen“ ab, und `proxmox.service.spec.ts:350-360` liefert beide Haelften (`spamcount_in: 1, spamcount_out: 0`). Test ausfuehren, die neuen Faelle muessen ROT sein. Commit `test(260923-le6): PMG-Teilsumme ohne Haelfte muss null sein`.
GREEN: `sumOrNull(a, b)` so aendern, dass es `null` zurueckgibt, sobald `a` ODER `b` `null` ist. Nur wenn beide Zahlen sind, wird ihre Summe zurueckgegeben. Die bisherige Ersatz-durch-Null-Addition entfaellt vollstaendig, ein fehlender Teilwert wird nie mehr als 0 behandelt. Ueber der Funktion einen kurzen deutschen Kommentar ergaenzen (Stil der Datei, ASCII-Umschreibungen wie im Rest der Datei): Eine Tageszahl aus zwei Teilwerten ist nur dann bekannt, wenn beide Teilwerte bekannt sind; eine Teilsumme saehe vollstaendig aus, waere aber still falsch (Abnahmebefund 260923-dhh, Wahrheit 7; PMG-Feldnamen sind nur Annahme A5). `normalizePmg` selbst und die Feldtabelle `PMG_STATS_FIELDS` bleiben unveraendert. Tests muessen GRUEN sein. Commit `fix(260923-le6): PMG-Summe null bei fehlendem Teilwert`.
</action>
<verify>
<automated>pnpm --filter api exec vitest run src/proxmox</automated>
<automated>test "$(grep -v '^\s*//' apps/api/src/proxmox/proxmox-normalize.ts | grep -c '?? 0) + (')" -eq 0</automated>
<automated>pnpm --filter api type-check</automated>
</verify>
<done>Alle Tests unter `apps/api/src/proxmox` gruen, darunter mindestens 5 neue Faelle „nur eine Haelfte vorhanden -> null“ (Spam in/out, Viren in/out, nicht lesbare Haelfte). Die Ersatz-durch-Null-Addition steht nicht mehr in `proxmox-normalize.ts`. API-Typpruefung ohne Fehler.</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 2: ServerCard waehlt den Hinweistext nach Rolle (isAdmin) und zweiter Text in de/en</name>
<files>apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx, apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx, apps/web/src/messages/de.json, apps/web/src/messages/en.json</files>
<read_first>apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx (Zeilen 150-215), apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx, apps/web/src/messages/de.json (Zeilen 706-720), apps/web/src/messages/en.json (Zeilen 706-720)</read_first>
<behavior>
- `isAdmin` gesetzt, Server nie abgefragt (`status.lastPolledAt === null`) -> Text „Noch keine Abfrage gelaufen. Klicken Sie oben auf „Jetzt aktualisieren“.“ (wie heute)
- `isAdmin={false}`, Server nie abgefragt -> Text „Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.“, und nirgends in der Karte steht „Jetzt aktualisieren“
- `isAdmin` weggelassen -> verhaelt sich wie `isAdmin={false}` (sichere Vorgabe)
- Weiterhin in keinem der Faelle die Sammelmeldung „Unerwarteter Fehler.“
</behavior>
<action>
Umsetzung der zweiten Korrektur, Teil Karte.
(a) Nachrichten: In `apps/web/src/messages/de.json` unter `proxmox.card`, direkt nach `notPolledYet`, den neuen Schluessel `notPolledYetAutomatic` mit dem Wert „Noch keine Abfrage gelaufen. Die Werte erscheinen nach der nächsten automatischen Abfrage.“ anlegen. Echtes „ä“ verwenden (umlaut-guard), Sie-Form bzw. unpersoenlich wie die uebrigen App-Texte. In `apps/web/src/messages/en.json` an derselben Stelle `notPolledYetAutomatic`: „No poll has run yet. The values will appear after the next automatic poll.“ Andere Schluessel nicht anfassen; `notPolledYet`, `refresh` und `refreshing` bleiben unveraendert. Begruendung der Wortwahl (Planer-Ermessen, Vorschlag aus dem Auftrag angepasst): Ein „in Kürze“ waere nicht immer wahr, denn das Intervall ist je Server von 1 bis 1440 Minuten einstellbar (`@Max(1440)`). „Nach der nächsten automatischen Abfrage“ stimmt bei jedem Intervall und verweist auf keinen Knopf.
(b) `ServerCard.tsx`: `ServerCardProps` um die optionale Eigenschaft `isAdmin?: boolean` erweitern und in der Funktionssignatur mit Vorgabe `false` entgegennehmen. Die sichere Vorgabe bedeutet: Wer die Eigenschaft vergisst, zeigt keinen Verweis auf einen Knopf. Im vorhandenen Zweig fuer nie abgefragte Server (`status && !status.lastPolledAt`) den Text nach `isAdmin` waehlen. Ist `isAdmin` wahr, bleibt der heutige Aufruf `t('card.notPolledYet', { refreshLabel: t('card.refresh') })` unveraendert, sonst `t('card.notPolledYetAutomatic')`. Den Kommentar „Nachbesserung Befund 2“ um einen Satz ergaenzen: Nicht-Admins sehen den Knopf nicht (der Poll-Endpunkt verlangt ADMIN/SUPER_ADMIN) und bekommen deshalb den Text ohne Knopfverweis (260923-le6). Sonst NICHTS an der Karte aendern, auch keine Formatierung unbeteiligter Zeilen (Festlegung: kein Umbau). Insbesondere kein `biome format --write` auf die ganze Datei, das wuerde unbeteiligte Zeilen umbrechen.
(c) `ServerCard.test.tsx`: In die `next-intl`-Mock-Tabelle `'card.notPolledYetAutomatic'` mit dem deutschen Text aus (a) aufnehmen. Den bestehenden Test „Nachbesserung Befund 2: …“ auf `render(<ServerCard server={server} isAdmin />)` umstellen; seine Erwartungen bleiben. Neue Tests fuer die Faelle aus `<behavior>` ergaenzen: `isAdmin={false}` sowie weggelassenes `isAdmin` jeweils mit Erwartung des automatischen Textes, `queryByText(/Jetzt aktualisieren/)` ist `null` und `queryByText('Unerwarteter Fehler.')` ist `null`. Zuerst die Tests schreiben und ROT sehen, dann (a)+(b) umsetzen und GRUEN sehen. Ein Commit genuegt: `fix(260923-le6): Proxmox-Karte verweist Nicht-Admins nicht auf den Aktualisieren-Knopf`.
</action>
<verify>
<automated>pnpm --filter web exec vitest run "src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx" src/messages</automated>
<automated>node -e "const d=require('./apps/web/src/messages/de.json'),e=require('./apps/web/src/messages/en.json');const a=d.proxmox.card.notPolledYetAutomatic,b=e.proxmox.card.notPolledYetAutomatic;if(!a||!b||/aktualisieren/i.test(a)||/refresh/i.test(b)||!a.includes('nächsten'))process.exit(1);if(d.proxmox.card.notPolledYet!=='Noch keine Abfrage gelaufen. Klicken Sie oben auf „{refreshLabel}“.')process.exit(2)"</automated>
</verify>
<done>ServerCard-Tests gruen (bestehende und neue Admin-/Nicht-Admin-Faelle); `src/messages`-Tests (umlaut-guard, Paritaet) gruen. `notPolledYetAutomatic` existiert in de und en und erwaehnt keinen Aktualisieren-Knopf. `notPolledYet` ist unveraendert.</done>
</task>
<task type="auto" tdd="true">
<name>Aufgabe 3: Modulseite zeigt „Jetzt aktualisieren“ nur ADMIN/SUPER_ADMIN, mit Seitentest</name>
<files>apps/web/src/app/(portal)/modules/proxmox/page.tsx, apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx</files>
<read_first>apps/web/src/app/(portal)/modules/proxmox/page.tsx, apps/web/src/app/(portal)/modules/tender-radar/settings/settings-roles.test.tsx (Zeilen 1-100, nur das Mock-Muster)</read_first>
<behavior>
- Rolle USER, eine Serverliste mit einem nie abgefragten Server -> kein Knopf mit Namen „Jetzt aktualisieren“; der Karten-Hinweis ist der automatische Text
- Kein Benutzer geladen (`user: null`) -> kein Knopf
- Rolle ADMIN -> Knopf „Jetzt aktualisieren“ sichtbar; der Karten-Hinweis ist der Admin-Text mit Knopfverweis
- Rolle SUPER_ADMIN -> Knopf sichtbar
- Leere Serverliste bei ADMIN -> weiterhin kein Knopf (bestehende Bedingung `servers.length > 0` bleibt)
</behavior>
<action>
Umsetzung der zweiten Korrektur, Teil Seite.
(a) Neue Testdatei `apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx` nach dem Muster von `settings-roles.test.tsx` anlegen. Gemockt werden `@/lib/proxmox-api` (`listServers` als `vi.fn()`, der je Fall eine Liste aufloest, und `pollServer` als `vi.fn()`), `@/lib/stores/auth-store` (Selektor-Durchreichung ueber `mockAuthStore`), `next/link` (als `<a>`) und `next-intl`. Die handgeschriebene Uebersetzungstabelle enthaelt mindestens `title`, `description`, `loading`, `loadError`, `emptyState`, `card.refresh`, `card.refreshing`, `card.settingsLink`, `card.unknownValue`, `card.lastPolledLabel`, `card.notPolledYet` (mit `{refreshLabel}`-Ersetzung wie in `ServerCard.test.tsx`) und `card.notPolledYetAutomatic`. Die Seite ueber `import ProxmoxPage from './page'` rendern. Mit `waitFor`/`findByText` auf den Servernamen warten, weil `listServers` asynchron ist. Danach Knopf per `queryByRole('button', { name: 'Jetzt aktualisieren' })` bzw. `getByRole` pruefen. Ein Testfall je Zeile aus `<behavior>`; der Server im Test ist ein nie abgefragter Server (Status wie im Befund-2-Test von `ServerCard.test.tsx`: `lastPolledAt: null`, `reachable: false`, `errorKind: null`). `afterEach` mit `cleanup()` und `vi.clearAllMocks()`. Test ausfuehren, die USER- und null-Faelle muessen ROT sein.
(b) `page.tsx`: Die bestehende Bedingung des Aktualisieren-Knopfs (`servers !== null && servers.length > 0`) zusaetzlich an `isAdmin` knuepfen, sodass der Knopf nur fuer ADMIN/SUPER_ADMIN gerendert wird. Die vorhandene Variable `isAdmin` wiederverwenden, keinen neuen Rollen-Mechanismus einfuehren. `handleRefresh` bleibt unveraendert. An der Render-Stelle `<ServerCard server={server} />` die Eigenschaft `isAdmin={isAdmin}` weiterreichen. Den Kopfkommentar der Komponente um einen Satz ergaenzen: Der Knopf erscheint nur fuer Admins, weil `POST servers/:id/poll` `@Roles(ADMIN, SUPER_ADMIN)` verlangt; fuer andere waere er wirkungslos (260923-le6). Sonst nichts an der Seite aendern: keine neuen Texte, kein Layout, keine Import-Umsortierung. Das vorbestehende organizeImports-Signal von biome in dieser Datei bleibt unangetastet.
Tests GRUEN sehen. Commit `fix(260923-le6): Aktualisieren-Knopf der Proxmox-Seite nur fuer Admins`.
</action>
<verify>
<automated>pnpm --filter web exec vitest run "src/app/(portal)/modules/proxmox"</automated>
<automated>grep -c 'isAdmin={isAdmin}' "apps/web/src/app/(portal)/modules/proxmox/page.tsx"</automated>
<automated>pnpm --filter web type-check</automated>
</verify>
<done>Alle Web-Tests im Proxmox-Verzeichnis gruen (ServerCard, ServerForm, neuer Seitentest mit mindestens 5 Faellen: USER, null, ADMIN, SUPER_ADMIN, leere Liste). `page.tsx` reicht `isAdmin={isAdmin}` an `ServerCard` weiter. Web-Typpruefung ohne Fehler.</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser -> API `POST /proxmox/servers/:id/poll` | Nicht-Admin koennte die Abfrage manuell ausloesen; die Berechtigung prueft ausschliesslich der Server (`@Roles(ADMIN, SUPER_ADMIN)`) |
| PMG-Server -> `normalizePmg` | Fremde, nur angenommene Antwortform (Annahme A5); unvollstaendige Felder duerfen keinen falschen Wert erzeugen |
## STRIDE Threat Register
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|-----------|----------|-----------|----------|-------------|-----------------|
| T-le6-01 | Elevation of Privilege | `POST servers/:id/poll` | low | accept | Das Ausblenden des Knopfes ist reine Oberflaeche, keine Sicherheitsgrenze. Die Durchsetzung bleibt unveraendert serverseitig per `@Roles(Role.ADMIN, Role.SUPER_ADMIN)` in `proxmox.controller.ts:89-90`. Dieser Plan aendert den Controller nicht. |
| T-le6-02 | Tampering (Integritaet der Anzeige) | `sumOrNull` in `normalizePmg` | medium | mitigate | Aufgabe 1: Summe null, sobald ein Teilwert fehlt oder unlesbar ist. Die neuen Tests decken beide Richtungen fuer Spam und Viren ab. |
| T-le6-03 | Information Disclosure | `ServerCard` Hinweistext | low | accept | Der neue Text enthaelt keine Server- oder Zugangsdaten, nur einen statischen Hinweis. |
</threat_model>
<verification>
Nach allen drei Aufgaben (vom Planer an der Ausgangslage 6530ae5 geprueft: alles gruen, `biome lint` sauber):
- `pnpm --filter api exec vitest run src/proxmox` gruen
- `pnpm --filter web exec vitest run "src/app/(portal)/modules/proxmox" src/messages` gruen
- `pnpm --filter api type-check` und `pnpm --filter web type-check` ohne Fehler
- `pnpm exec biome lint apps/api/src/proxmox/proxmox-normalize.ts apps/api/src/proxmox/proxmox-normalize.spec.ts "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/proxmox-page-roles.test.tsx"` ohne Befund
- `pnpm exec biome check "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx"` ohne Befund (neue Datei, voll konform)
- `biome check` auf den fuenf VORHANDENEN Dateien: vorher 6 Befunde, alle vorbestehend (Formatierung je Datei, dazu organizeImports in `page.tsx`). Deren Anzahl darf nicht steigen. Die vorbestehenden Befunde werden nicht mit behoben, das waere fremder Diff (Festlegung: kein Umbau).
- Keine Container-Neubauten, kein Deploy, keine Browserpruefung in diesem Plan
</verification>
<success_criteria>
- Eine PMG-Antwort mit nur einer Haelfte eines Spam- oder Viren-Paares ergibt `null`, die Seite zeigt dort also „unbekannt“ statt einer Teilsumme.
- Auf der Proxmox-Modulseite sehen nur ADMIN und SUPER_ADMIN „Jetzt aktualisieren“. Der Hinweis fuer nie abgefragte Server verweist Nicht-Admins auf die automatische Abfrage statt auf den Knopf.
- Sonst keine sichtbare Aenderung an der Modulseite.
- In der SUMMARY als Beobachtung vermerken, nicht beheben: Inaktive Server (`isActive: false`) werden nicht automatisch abgefragt. Fuer einen inaktiven, nie abgefragten Server stimmt der neue Nicht-Admin-Text deshalb nicht ganz. Das ist ein vorbestehender Randfall, denn auch die Karte fuer Admins beachtet `isActive` heute nicht. Er liegt ausserhalb dieses Auftrags (Festlegung: kein Umbau) und wird dem Nutzer zur Entscheidung vorgelegt.
- In der SUMMARY vermerken, dass damit die offene Luecke (Wahrheit 7) aus `260923-dhh-VERIFICATION.md` geschlossen ist.
</success_criteria>
<output>
Create `.planning/quick/260923-le6-proxmox-abnahmebefunde-sumornull-null-be/260923-le6-SUMMARY.md` when done
</output>
@@ -0,0 +1,174 @@
---
phase: quick
plan: 260923-le6
subsystem: proxmox-modul
tags: [nestjs, next-intl, vitest, tdd, proxmox]
requires:
- phase: quick-260923-dhh
provides: "Proxmox-Modul (PVE/PBS/PMG) inklusive normalizePmg und ServerCard; Abnahmebefund Wahrheit 7 (PMG-Teilsumme) blieb offen"
provides:
- "sumOrNull liefert null, sobald ein Teilwert einer PMG-Summe (Spam/Viren) fehlt oder unlesbar ist — nie mehr eine Teilsumme"
- "ServerCard zeigt Nicht-Admins fuer nie abgefragte Server einen Hinweis ohne Knopfverweis (isAdmin-Eigenschaft, Vorgabe false)"
- "Proxmox-Modulseite zeigt den Knopf 'Jetzt aktualisieren' nur ADMIN/SUPER_ADMIN"
affects: [proxmox-modul, dashboard-kachel-proxmox]
actuals:
tokens: 4581
tasks: 3
commits: 4
plan_head_before: 6530ae5
tech-stack:
added: []
patterns:
- "isAdmin?: boolean (Vorgabe false) als sichere Eigenschaft fuer UI-Elemente, deren serverseitige Aktion rollenbeschraenkt ist (uebernimmt das bestehende Muster aus page.tsx, kein neuer Mechanismus)"
key-files:
created:
- apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx
modified:
- apps/api/src/proxmox/proxmox-normalize.ts
- apps/api/src/proxmox/proxmox-normalize.spec.ts
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx
- apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx
- apps/web/src/messages/de.json
- apps/web/src/messages/en.json
- apps/web/src/app/(portal)/modules/proxmox/page.tsx
key-decisions:
- "Wortwahl fuer notPolledYetAutomatic: 'nach der naechsten automatischen Abfrage' statt 'in Kuerze', weil das Poll-Intervall je Server 1-1440 Minuten einstellbar ist und 'in Kuerze' nicht immer zutraefe"
patterns-established:
- "sumOrNull(a, b): null wenn a ODER b null ist (statt Ersatz-durch-Null) — Muster fuer jede zukuenftige Tageszahl aus zwei Teilwerten"
requirements-completed: []
coverage:
- id: D1
description: "PMG-Summe (Spam/Viren) ist null, sobald genau eine Haelfte fehlt oder unlesbar ist — in beide Richtungen (in/out), Paare unabhaengig voneinander"
verification:
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#nur eine Haelfte vorhanden -> null (Spam, nur spamcount_in)"
status: pass
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#nur eine Haelfte vorhanden -> null (Spam, nur spamcount_out)"
status: pass
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#nur eine Haelfte vorhanden -> null (Viren, nur viruscount_in)"
status: pass
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#nur eine Haelfte vorhanden -> null (Viren, nur viruscount_out)"
status: pass
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#eine Haelfte ist nicht lesbar -> null (Spam, spamcount_out ist Text)"
status: pass
- kind: unit
ref: "apps/api/src/proxmox/proxmox-normalize.spec.ts#Unabhaengigkeit der Paare: Spam unvollstaendig, Viren vollstaendig"
status: pass
human_judgment: false
- id: D2
description: "Auf der Proxmox-Modulseite sehen nur ADMIN/SUPER_ADMIN den Knopf 'Jetzt aktualisieren'; USER und ein noch nicht geladener Benutzer sehen ihn nicht; ServerCard verweist Nicht-Admins nicht auf den Knopf"
verification:
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx#Rolle USER: kein Knopf"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx#kein Benutzer geladen (user: null): kein Knopf"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx#Rolle ADMIN: Knopf sichtbar"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx#Rolle SUPER_ADMIN: Knopf sichtbar"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx#leere Serverliste bei ADMIN: weiterhin kein Knopf"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx#260923-le6: isAdmin={false}, noch nie abgefragt -> automatischer Hinweis ohne Knopfverweis"
status: pass
- kind: unit
ref: "apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx#260923-le6: isAdmin weggelassen -> verhaelt sich wie isAdmin={false}"
status: pass
human_judgment: false
duration: 21min
completed: 2026-09-23
status: complete
---
# Quick Task 260923-le6: Zwei Abnahmebefunde des Proxmox-Moduls behoben Summary
**PMG-Teilsumme wird null statt still falsch (sumOrNull), Aktualisieren-Knopf der Proxmox-Modulseite nur noch fuer ADMIN/SUPER_ADMIN sichtbar**
## Performance
- **Duration:** 21 min
- **Started:** 2026-09-23T13:12:00Z
- **Completed:** 2026-09-23T13:33:50Z
- **Tasks:** 3
- **Files modified:** 8 (7 geaendert, 1 neu)
## Accomplishments
- `sumOrNull(a, b)` in `proxmox-normalize.ts` liefert `null`, sobald ein Teilwert (Spam oder Viren, je Richtung in/out) fehlt oder nicht lesbar ist — die bisherige stille Ersatz-durch-0-Addition ist vollstaendig entfernt. Damit ist Wahrheit 7 (Blocker) aus `260923-dhh-VERIFICATION.md` geschlossen.
- `ServerCard` bekommt die optionale Eigenschaft `isAdmin` (Vorgabe `false`) und zeigt Nicht-Admins fuer einen nie abgefragten Server einen neuen Hinweistext (`proxmox.card.notPolledYetAutomatic`, de/en), der auf keinen Knopf verweist.
- Die Proxmox-Modulseite zeigt den Knopf "Jetzt aktualisieren" nur noch, wenn `isAdmin` wahr ist (bestehende Variable wiederverwendet, kein neuer Rollen-Mechanismus), und reicht `isAdmin` an `ServerCard` weiter.
## Task Commits
Alle Aufgaben wurden per TDD (RED -> GREEN) umgesetzt und einzeln committet:
1. **Aufgabe 1 (RED): PMG-Teilsumme-Tests** - `c13d657` (test)
2. **Aufgabe 1 (GREEN): sumOrNull korrigiert** - `2eb86e1` (fix)
3. **Aufgabe 2: ServerCard mit isAdmin und zweitem Hinweistext** - `2f8dd14` (fix)
4. **Aufgabe 3: Aktualisieren-Knopf nur fuer Admins** - `e1b191b` (fix)
_Hinweis: Aufgabe 1 hatte planmaessig zwei Commits (RED/GREEN); Aufgaben 2 und 3 wurden je in einem Commit umgesetzt, wie im Plan vorgesehen (Tests zuerst rot gesehen, dann implementiert, ein Commit je Aufgabe)._
## Files Created/Modified
- `apps/api/src/proxmox/proxmox-normalize.ts` - `sumOrNull` liefert `null` bei fehlendem Teilwert statt Ersatz-durch-0
- `apps/api/src/proxmox/proxmox-normalize.spec.ts` - 6 neue Testfaelle fuer beide Richtungen (Spam/Viren), unlesbare Haelfte, Unabhaengigkeit der Paare
- `apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.tsx` - neue `isAdmin`-Eigenschaft (Vorgabe `false`), waehlt den Hinweistext fuer nie abgefragte Server
- `apps/web/src/app/(portal)/modules/proxmox/components/ServerCard.test.tsx` - bestehenden Test auf `isAdmin` umgestellt, zwei neue Faelle (`isAdmin={false}`, weggelassen)
- `apps/web/src/messages/de.json` / `en.json` - neuer Schluessel `proxmox.card.notPolledYetAutomatic`
- `apps/web/src/app/(portal)/modules/proxmox/page.tsx` - Knopf nur bei `isAdmin`, reicht `isAdmin={isAdmin}` an `ServerCard` weiter
- `apps/web/src/app/(portal)/modules/proxmox/proxmox-page-roles.test.tsx` (neu) - Seitentest mit 5 Faellen (USER, `user: null`, ADMIN, SUPER_ADMIN, leere Liste bei ADMIN)
## Decisions Made
- Formulierung "Noch keine Abfrage gelaufen. Die Werte erscheinen nach der naechsten automatischen Abfrage." statt eines "in Kuerze"-Hinweises, weil das Poll-Intervall je Server zwischen 1 und 1440 Minuten liegen kann (`@Max(1440)`) — die gewaehlte Formulierung stimmt bei jedem Intervall und verweist auf keinen Knopf.
- Keine weiteren Aenderungen an der Modulseite (Festlegung des Nutzers: kein Umbau, keine zusaetzlichen Details oder Statusfarben) — bestaetigt eingehalten.
## Deviations from Plan
None - plan genau wie geschrieben ausgefuehrt.
## Issues Encountered
None.
## Beobachtungen (nicht behoben, dem Nutzer zur Entscheidung vorgelegt)
- **Inaktive Server:** Ein inaktiver, nie abgefragter Server (`isActive: false`) wird nicht automatisch abgefragt (`proxmox-scheduler.service.ts` fragt nur aktive Server ab). Der neue Nicht-Admin-Hinweistext "...erscheinen nach der naechsten automatischen Abfrage" trifft fuer diesen Randfall nicht ganz zu. Das ist ein vorbestehender Randfall — auch die Admin-Karte beachtet `isActive` heute nicht — und liegt ausserhalb dieses Auftrags (Festlegung: kein Umbau). Wird hier nur vermerkt, nicht behoben.
## Verifikation (alle gruen, wie im Plan verlangt)
- `pnpm --filter api exec vitest run src/proxmox` - 82 Tests gruen (26 in `proxmox-normalize.spec.ts`, davon 6 neu)
- `pnpm --filter web exec vitest run "src/app/(portal)/modules/proxmox" src/messages` - 32 Tests gruen
- `pnpm --filter api type-check` und `pnpm --filter web type-check` - ohne Fehler
- `biome lint` auf den 6 Plan-Dateien - ohne Befund
- `biome check` auf der neuen Datei `proxmox-page-roles.test.tsx` - ohne Befund (nach `biome check --write` fuer Formatierung)
- `biome check` auf den 5 vorbestehenden Dateien - weiterhin genau 6 Befunde (vorbestehende Formatierung + `organizeImports` in `page.tsx`), keine neuen Befunde — wie im Plan festgelegt nicht behoben (fremder Diff)
- Keine Container-Neubauten, kein Deploy, keine Browserpruefung — wie im Plan vorgesehen
## User Setup Required
None - keine externe Konfiguration noetig.
## Next Phase Readiness
- Die offene Luecke (Wahrheit 7) aus `260923-dhh-VERIFICATION.md` ist geschlossen; das Proxmox-Modul hat keine bekannten offenen Abnahmebefunde mehr.
- Offen beim Nutzer (keine Entscheidung noetig, nur zur Kenntnis): der oben vermerkte Randfall bei inaktiven, nie abgefragten Servern.
## Self-Check: PASSED
Alle im Plan genannten Dateien wurden gefunden, alle vier Commits sind im Log nachweisbar.
---
*Plan: 260923-le6*
*Completed: 2026-09-23*