feat(quick-261005-bt1): Proxmox warnt bei gestopptem Gast mit aktivem Autostart

Fuer jeden nicht laufenden qemu/lxc (ohne Vorlage) liest der PVE-Durchlauf
onboot aus /nodes/{node}/{type}/{vmid}/config (nur GET, Deckel 40).
Befund in den Metriken (autostartStopped, autostartUnchecked); Karte wird
Warnung mit einer Zeile je Gast, Kachel zeigt 'gestoppt trotz Autostart'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-05 08:30:20 +02:00
parent 781bc9f9ee
commit 873d15d25e
21 changed files with 455 additions and 21 deletions
+3 -2
View File
@@ -31,7 +31,7 @@ See: .planning/PROJECT.md (updated 2026-07-17)
Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden)
Plan: 6 of 6
Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen
Last activity: 2026-10-05 - Quick 261005-blw Notiz-Links in neuem Tab
Last activity: 2026-10-05 - Quick 261005-bt1 Proxmox Autostart-Warnung
Progress: [██████████] 99%
@@ -492,7 +492,8 @@ Gerettet aus `.continue-here.md`. Relevant fuer die noch offenen Live-Tests.
| 261002-k67 | Modul Nextcloud-Status mit Ampel-Kacheln und Dashboard-Uebersicht | 2026-10-02 | 5ef7b0c..87a7b7c | [261002-k67-modul-nextcloud-status-mit-ampel-kacheln](.planning/quick/261002-k67-modul-nextcloud-status-mit-ampel-kacheln/) |
| 261002-kxc | Nextcloud-Status: Benachrichtigung bei Rot je Benutzer (Mail + Desktop-Hinweis), Klartext-Fehler | 2026-10-02 | faed0d7..6c4bff6 | [261002-kxc-nextcloud-status-benachrichtigung-bei-ro](.planning/quick/261002-kxc-nextcloud-status-benachrichtigung-bei-ro/) |
| 261003-387 | Kategorien durch Admins bearbeitbar (anlegen, umbenennen, sortieren, loeschen mit Verschieben, Module zuordnen) | 2026-10-03 | 8ec116c..f2c0a89 | [261003-387-kategorien-durch-admins-bearbeitbar-umbe](.planning/quick/261003-387-kategorien-durch-admins-bearbeitbar-umbe/) |
| 261005-blw | Links in Notiz-Kacheln oeffnen in neuem Tab | 2026-10-05 | (dieser Commit) | [261005-blw-notiz-links-in-neuem-tab](.planning/quick/261005-blw-notiz-links-in-neuem-tab/) |
| 261005-blw | Links in Notiz-Kacheln oeffnen in neuem Tab | 2026-10-05 | 781bc9f | [261005-blw-notiz-links-in-neuem-tab](.planning/quick/261005-blw-notiz-links-in-neuem-tab/) |
| 261005-bt1 | Proxmox: Warnung je gestopptem Gast mit aktivem Autostart (Karte, Kachel) | 2026-10-05 | (dieser Commit) | [261005-bt1-proxmox-autostart-warnung](.planning/quick/261005-bt1-proxmox-autostart-warnung/) |
## Deferred Items
@@ -0,0 +1,20 @@
---
quick_id: 261005-bt1
slug: proxmox-autostart-warnung
date: 2026-10-05
---
# Proxmox: Warnung bei gestopptem Gast mit Autostart
User 05.10.: Warnung fuer jede VM oder jeden LXC, der nicht laeuft, fuer den Autostart aber aktiv ist.
## Task 1 — API
- `/cluster/resources` liefert `onboot` nicht. Fuer jeden nicht laufenden qemu/lxc (ohne Vorlage) `GET /nodes/{node}/{type}/{vmid}/config` lesen, Deckel 40 je Durchlauf.
- Metriken PVE: `autostartStopped[]` (vmid, name, type, node, status) + `autostartUnchecked` (Abruf fehlgeschlagen/Deckel). Kein Schema-Umbau (metrics ist JSON).
- Nur GET, Lese-Riegel (`proxmox-nur-lesen.spec.ts`) bleibt gruen.
## Task 2 — Web
- `serverHealth`: Autostart-Befund -> warn.
- ServerCard: Warnblock mit einer Zeile je Gast, Hinweis bei ungeprueften.
- Dashboard-Kachel: Kennzahl „N gestoppt trotz Autostart“ (Vorrang vor Auslastung).
- de/en-Texte, Handbuch, CHANGELOG.
@@ -0,0 +1,13 @@
---
quick_id: 261005-bt1
status: complete
date: 2026-10-05
---
# 261005-bt1: Proxmox Autostart-Warnung — SUMMARY
- API: `listStoppedPveGuests` + `readPveOnboot` (proxmox-normalize.ts), `checkPveAutostart` im PVE-Durchlauf (proxmox.service.ts), Deckel `PVE_AUTOSTART_QUERY_CAP = 40`. Fehlendes `onboot` = aus; fehlgeschlagener Config-Abruf (z. B. 403 ohne VM.Audit) macht den Server NICHT unerreichbar, sondern zaehlt in `autostartUnchecked`.
- Web: `autostartAlerts` + Warnstufe in proxmox-status.ts, `AutostartNotice` in ServerCard, Kennzahl `autostart` in der Kachel. Alte Zwischenlagerstaende ohne Feld -> keine Warnung.
- Tests: api 2208/2208, web 1370/1370 gruen (neu: Service-Durchlauf mit onboot/ohne/403/Vorlage, Normalisierer, serverHealth, Kennzahl, Kachel-Render, ServerCard-Render). tsc beider Apps sauber; Biome-Befunde der geaenderten Dateien unveraendert gegenueber HEAD.
- Nicht gegen echten PVE geprueft (lokaler Stack aus, Zugangsdaten nur verschluesselt auf alpha). alpha hat PVE01 mit 2 gestoppten Gaesten -> nach Deploy dort pruefen.
- Folgen fuer den Abfrageweg: je PVE-Durchlauf eine zusaetzliche GET-Anfrage pro gestopptem Gast.
+1
View File
@@ -14,6 +14,7 @@ Diese Liste beschreibt in einfachen Worten, was sich von Version zu Version an T
- Kantinenabrechnung: Die CSV-Datei der Kantine hochladen (Excel-Export mit UTF-8 oder Windows-1252 ist beides in Ordnung). Tessera zeigt Zeilenzahl, Abrechnungsmonat und Gesamtbetrag, nennt fehlerhafte Zeilen mit Zeilennummer und weist auf unterschiedliche Abrechnungsmonate hin. Ist alles in Ordnung, laden Sie mit einem Klick die DATEV-Lohndatei herunter. Beraternummer, Mandantennummer und Lohnart trägt ein Administrator einmalig ein; bis dahin ist der Download gesperrt. Die hochgeladenen Daten werden nicht gespeichert.
- Handelsware: Eine Excel-Liste mit Handelswaren-Umsätzen hochladen. Tessera ordnet jedem Produkt sein Konto zu, markiert neue Produkte mit „neu“ und vergibt ihnen das nächste freie Gegenkonto. Das Buchungsdatum wird aus dem Dateinamen abgeleitet (letzter Tag des Monats) und lässt sich ändern. Neue Konten werden erst beim Herunterladen der Buchungsdatei gespeichert. Im Reiter „Konten“ pflegen Sie die Kontenliste, lesen sie aus einer CSV-Datei ein (ersetzt alle vorhandenen Konten, nach Rückfrage) und exportieren sie als CSV. Standard-Erlöskonto und Startwert für das Gegenkonto trägt ein Administrator einmalig ein.
- Modul-Freigaben haben jetzt zwei Stufen: „Benutzen“ (wie bisher) und „Verwalten“. Wer ein Modul verwalten darf, ändert dessen Einstellungen selbst, ohne Administrator zu sein, zum Beispiel in der Kantinenabrechnung, bei Handelsware und bei den Proxmox-Servern. Ein Administrator wählt die Stufe je Gruppe in der Freigaben-Matrix oder je Benutzer in den Benutzerdetails; bestehende Freigaben bleiben „Benutzen“. Freigaben vergeben und Module aktivieren dürfen weiterhin nur Administratoren. Das Modul DKV-Rechnung steht Administratoren und Benutzern mit „Verwalten“ zur Verfügung.
- Proxmox: Für jede virtuelle Maschine und jeden Container auf einem PVE-Server, der nicht läuft, obwohl der Autostart eingeschaltet ist, zeigt die Serverkarte jetzt eine Warnung mit Nummer, Name, Knoten und Zustand. Die Karte steht dann auf „Warnung“, die Dashboard-Kachel zeigt „gestoppt trotz Autostart“. Vorlagen zählen nicht. Kann Tessera die Autostart-Einstellung nicht lesen, nennt die Karte, bei wie vielen Gästen das so war (dem Zugang fehlt dann meist das Recht VM.Audit aus der Rolle PVEAuditor).
### Behoben
@@ -1,12 +1,14 @@
import { describe, expect, it } from 'vitest';
import {
listPbsDatastoreNames,
listStoppedPveGuests,
normalizePbs,
normalizePmg,
normalizePve,
readBool,
readList,
readNumber,
readPveOnboot,
readText,
} from './proxmox-normalize';
@@ -285,3 +287,32 @@ describe('normalizePmg (Aufgabe 3, <behavior>)', () => {
expect(metrics.countOut).toBe(50);
});
});
describe('quick-261005: Autostart-Hilfen', () => {
it('listStoppedPveGuests nimmt nur nicht laufende qemu/lxc ohne Vorlage, sortiert nach vmid', () => {
const list = listStoppedPveGuests({
data: [
{ type: 'lxc', node: 'b', vmid: '205', status: 'stopped' },
{ type: 'qemu', node: 'a', vmid: 101, status: 'paused', name: 'p' },
{ type: 'qemu', node: 'a', vmid: 100, status: 'running' },
{ type: 'qemu', node: 'a', vmid: 9000, status: 'stopped', template: 1 },
{ type: 'storage', node: 'a', status: 'available' },
{ type: 'qemu', vmid: 300, status: 'stopped' },
],
});
expect(list).toEqual([
{ vmid: 101, name: 'p', type: 'qemu', node: 'a', status: 'paused' },
{ vmid: 205, name: null, type: 'lxc', node: 'b', status: 'stopped' },
]);
expect(listStoppedPveGuests('kaputt')).toEqual([]);
});
it('readPveOnboot: fehlendes Feld heisst aus, kein data-Objekt heisst unbekannt', () => {
expect(readPveOnboot({ data: { onboot: 1 } })).toBe(true);
expect(readPveOnboot({ data: { onboot: '1' } })).toBe(true);
expect(readPveOnboot({ data: { onboot: 0 } })).toBe(false);
expect(readPveOnboot({ data: {} })).toBe(false);
expect(readPveOnboot({ data: null })).toBeNull();
expect(readPveOnboot(null)).toBeNull();
});
});
+41
View File
@@ -3,6 +3,7 @@ import type {
ProxmoxPbsDatastoreMetric,
ProxmoxPbsMetrics,
ProxmoxPmgMetrics,
ProxmoxPveAutostartGuest,
ProxmoxPveMetrics,
ProxmoxPveNodeMetric,
} from './proxmox.types';
@@ -121,6 +122,46 @@ export function normalizePve(body: unknown): NormalizeResult<ProxmoxPveMetrics>
};
}
/**
* Gaeste aus `/cluster/resources`, die NICHT laufen und keine Vorlage sind
* (quick-261005) — Kandidaten fuer die Autostart-Pruefung. Fuer jeden
* fragt der Dienst danach `/nodes/{node}/{type}/{vmid}/config` ab, denn
* `cluster/resources` liefert `onboot` nicht mit. Eintraege ohne gueltige
* vmid oder Node werden still uebergangen.
*/
export function listStoppedPveGuests(body: unknown): ProxmoxPveAutostartGuest[] {
if (!isRecord(body)) return [];
const result: ProxmoxPveAutostartGuest[] = [];
for (const entry of readList(body.data)) {
if (!isRecord(entry)) continue;
if (entry.type !== 'qemu' && entry.type !== 'lxc') continue;
if (entry.status === 'running') continue;
if (readBool(entry.template) === true) continue;
const vmid = readNumber(entry.vmid);
const node = readText(entry.node);
if (vmid === null || node === null) continue;
result.push({
vmid,
name: readText(entry.name),
type: entry.type,
node,
status: readText(entry.status) ?? 'unknown',
});
}
return result.sort((a, b) => a.vmid - b.vmid);
}
/**
* `onboot` aus der Antwort von `/nodes/{node}/{type}/{vmid}/config`.
* Proxmox laesst das Feld weg, wenn es nie gesetzt wurde — das heisst
* „aus“ (Vorgabe 0). `null` nur bei einer Antwort ohne `data`-Objekt.
*/
export function readPveOnboot(body: unknown): boolean | null {
if (!isRecord(body) || !isRecord(body.data)) return null;
if (!('onboot' in body.data)) return false;
return readBool(body.data.onboot) ?? false;
}
// ---------------------------------------------------------------------------
// PBS — /api2/json/status/datastore-usage + je Datenspeicher .../snapshots
// ---------------------------------------------------------------------------
+54 -8
View File
@@ -188,8 +188,9 @@ describe('ProxmoxService — Aufgabe 1 (PVE per Token, durchgehender Weg)', () =
const service = new ProxmoxService(prisma as any, crypto as any);
const created = await service.createServer('tenant-a', TOKEN_DTO);
const urls: string[] = [];
const fetchSpy = vi.fn(async (url: string) => {
expect(url).toBe('https://pve.intern:8006/api2/json/cluster/resources');
urls.push(url);
return new Response(JSON.stringify(pveResourcesBody()), { status: 200 });
});
vi.stubGlobal('fetch', fetchSpy);
@@ -208,6 +209,51 @@ describe('ProxmoxService — Aufgabe 1 (PVE per Token, durchgehender Weg)', () =
expect(status.reachable).toBe(true);
expect(status.metrics).toMatchObject({ nodeCount: 1 });
expect(status.rawSample).toContain('"node":"pve1"');
expect(urls[0]).toBe('https://pve.intern:8006/api2/json/cluster/resources');
});
it('quick-261005: meldet gestoppte Gaeste mit aktivem Autostart, Vorlagen nie, unlesbare als nicht geprueft', async () => {
const prisma = makeFakePrisma();
const service = new ProxmoxService(prisma as any, crypto as any);
const created = await service.createServer('tenant-a', TOKEN_DTO);
const resources = {
data: [
{ type: 'node', node: 'pve1', cpu: 0.1, maxcpu: 8, mem: 1, maxmem: 2 },
{ type: 'qemu', node: 'pve1', vmid: 100, status: 'running', name: 'laeuft' },
{ type: 'qemu', node: 'pve1', vmid: 101, status: 'stopped', name: 'dc01' },
{ type: 'lxc', node: 'pve2', vmid: 201, status: 'stopped', name: 'ohne-autostart' },
{ type: 'qemu', node: 'pve1', vmid: 102, status: 'stopped', name: 'kein-recht' },
{ type: 'qemu', node: 'pve1', vmid: 9000, status: 'stopped', template: 1 },
],
};
const urls: string[] = [];
vi.stubGlobal(
'fetch',
vi.fn(async (url: string) => {
urls.push(url);
if (url.endsWith('/cluster/resources')) {
return new Response(JSON.stringify(resources), { status: 200 });
}
if (url.endsWith('/nodes/pve1/qemu/101/config')) {
return new Response(JSON.stringify({ data: { onboot: 1, name: 'dc01' } }), { status: 200 });
}
if (url.endsWith('/nodes/pve2/lxc/201/config')) {
return new Response(JSON.stringify({ data: { hostname: 'x' } }), { status: 200 });
}
return new Response('{"data":null}', { status: 403 });
}),
);
const result = await service.pollServer('tenant-a', (created as any).id);
expect(result?.reachable).toBe(true);
expect(result?.metrics).toMatchObject({
autostartStopped: [{ vmid: 101, name: 'dc01', type: 'qemu', node: 'pve1', status: 'stopped' }],
autostartUnchecked: 1,
});
expect(urls.some((u) => u.includes('/100/config'))).toBe(false);
expect(urls.some((u) => u.includes('/9000/config'))).toBe(false);
});
it('sendet die Token-Kopfzeile im PVE-Schema (Gleichheitszeichen vor dem Geheimnis)', async () => {
@@ -241,8 +287,8 @@ describe('ProxmoxService — Aufgabe 1 (PVE per Token, durchgehender Weg)', () =
});
const dispatchers: unknown[] = [];
const fetchSpy = vi.fn(async (_url: string, options: RequestInit & { dispatcher?: unknown }) => {
dispatchers.push(options.dispatcher);
const fetchSpy = vi.fn(async (url: string, options: RequestInit & { dispatcher?: unknown }) => {
if (url.endsWith('/cluster/resources')) dispatchers.push(options.dispatcher);
return new Response(JSON.stringify(pveResourcesBody()), { status: 200 });
});
vi.stubGlobal('fetch', fetchSpy);
@@ -479,7 +525,7 @@ describe('ProxmoxService — Aufgabe 4 (Verbindungstest, Zehn-Sekunden-Sperre)',
vi.stubGlobal(
'fetch',
vi.fn(async (url: string) => {
capturedUrl = url;
capturedUrl ??= url;
return new Response(JSON.stringify(pveResourcesBody()), { status: 200 });
}),
);
@@ -519,8 +565,8 @@ describe('ProxmoxService — Aufgabe 4 (Verbindungstest, Zehn-Sekunden-Sperre)',
let fetchCalls = 0;
vi.stubGlobal(
'fetch',
vi.fn(async () => {
fetchCalls++;
vi.fn(async (url: string) => {
if (url.endsWith('/cluster/resources')) fetchCalls++;
return new Response(JSON.stringify(pveResourcesBody()), { status: 200 });
}),
);
@@ -540,8 +586,8 @@ describe('ProxmoxService — Aufgabe 4 (Verbindungstest, Zehn-Sekunden-Sperre)',
let fetchCalls = 0;
vi.stubGlobal(
'fetch',
vi.fn(async () => {
fetchCalls++;
vi.fn(async (url: string) => {
if (url.endsWith('/cluster/resources')) fetchCalls++;
return new Response(JSON.stringify(pveResourcesBody()), { status: 200 });
}),
);
+51 -3
View File
@@ -5,13 +5,25 @@ import { PrismaService } from '../prisma/prisma.service';
import { forSystem, forTenant } from '../prisma/prisma-tenant.extension';
import { buildTicketCookieHeader, buildTokenAuthHeader, loginTicket } from './proxmox-auth';
import { proxmoxGet, type ProxmoxGetResult } from './proxmox-client.service';
import { listPbsDatastoreNames, normalizePbs, normalizePmg, normalizePve } from './proxmox-normalize';
import {
listPbsDatastoreNames,
listStoppedPveGuests,
normalizePbs,
normalizePmg,
normalizePve,
readPveOnboot,
} from './proxmox-normalize';
import type {
CreateProxmoxServerDto,
TestProxmoxServerDto,
UpdateProxmoxServerDto,
} from './dto/proxmox-server.dto';
import type { ProxmoxErrorKind, ProxmoxPollResult, ProxmoxProductType } from './proxmox.types';
import type {
ProxmoxErrorKind,
ProxmoxPollResult,
ProxmoxProductType,
ProxmoxPveAutostartGuest,
} from './proxmox.types';
/**
* Nur die Felder, die eine Abfrage tatsaechlich braucht (Nachbesserung
@@ -77,6 +89,13 @@ const SAFE_SERVER_SELECT = {
*/
const PBS_SNAPSHOT_QUERY_CAP = 10;
/**
* Deckel der Autostart-Pruefung je PVE-Durchlauf (quick-261005): hoechstens
* so viele `/config`-Abfragen fuer gestoppte Gaeste. Gaeste darueber hinaus
* zaehlen als „nicht geprueft“ (`autostartUnchecked`), nie als „in Ordnung“.
*/
const PVE_AUTOSTART_QUERY_CAP = 40;
/**
* Zehn-Sekunden-Sperre fuer `pollServer` (Aufgabe 4, T-DHH-06): ein Klick
* auf "Jetzt aktualisieren" darf nicht zu ungebremsten Anfragen gegen die
@@ -325,6 +344,34 @@ export class ProxmoxService {
return result;
}
/**
* Autostart-Pruefung (quick-261005): fuer jeden gestoppten Gast (keine
* Vorlage) die Einstellung `onboot` aus seiner Konfiguration lesen.
* `cluster/resources` liefert sie nicht mit. Ein fehlgeschlagener Abruf
* (z. B. 403, weil dem Token VM.Audit fehlt) macht den Server NICHT
* unerreichbar, sondern zaehlt als „nicht geprueft“.
*/
private async checkPveAutostart(
server: ProxmoxCredentialSource,
session: { headers: Record<string, string>; retried: boolean },
resourcesBody: unknown,
): Promise<{ autostartStopped: ProxmoxPveAutostartGuest[]; autostartUnchecked: number }> {
const stopped = listStoppedPveGuests(resourcesBody);
const autostartStopped: ProxmoxPveAutostartGuest[] = [];
let autostartUnchecked = Math.max(0, stopped.length - PVE_AUTOSTART_QUERY_CAP);
for (const guest of stopped.slice(0, PVE_AUTOSTART_QUERY_CAP)) {
const configResult = await this.getWithRetry(
server,
session,
`/api2/json/nodes/${encodeURIComponent(guest.node)}/${guest.type}/${guest.vmid}/config`,
);
const onboot = configResult.ok ? readPveOnboot(configResult.body) : null;
if (onboot === null) autostartUnchecked++;
else if (onboot) autostartStopped.push(guest);
}
return { autostartStopped, autostartUnchecked };
}
/**
* EIN Abfragedurchlauf gegen genau diesen Server — `pve` (Aufgabe 1),
* `pbs` und `pmg` (Aufgabe 3), jeweils mit Token ODER Benutzer/Passwort
@@ -366,11 +413,12 @@ export class ProxmoxService {
rawSample: truncateRaw(result.body),
};
}
const autostart = await this.checkPveAutostart(server, session, result.body);
return {
reachable: true,
errorKind: null,
errorDetail: null,
metrics: normalized.metrics,
metrics: { ...normalized.metrics, ...autostart },
rawSample: truncateRaw(result.body),
};
}
+20
View File
@@ -59,6 +59,15 @@ export interface ProxmoxPveStorageMetric {
maxdisk: number | null;
}
/** Eine VM oder ein LXC, der nicht laeuft, obwohl Autostart (`onboot`) aktiv ist (quick-261005). */
export interface ProxmoxPveAutostartGuest {
vmid: number;
name: string | null;
type: 'qemu' | 'lxc';
node: string;
status: string;
}
export interface ProxmoxPveMetrics {
productType: 'pve';
nodeCount: number;
@@ -66,6 +75,17 @@ export interface ProxmoxPveMetrics {
guestsStopped: number;
nodes: ProxmoxPveNodeMetric[];
storages: ProxmoxPveStorageMetric[];
/**
* Gestoppte Gaeste mit aktivem Autostart (quick-261005). Fehlt in
* Zwischenlagerstaenden von vor dieser Erweiterung.
*/
autostartStopped?: ProxmoxPveAutostartGuest[];
/**
* Gestoppte Gaeste, deren Autostart-Einstellung NICHT gelesen werden
* konnte (fehlendes Recht VM.Audit, Deckel erreicht, Abruf fehlgeschlagen)
* — ehrlich „unbekannt“ statt still „kein Problem“.
*/
autostartUnchecked?: number;
}
export interface ProxmoxPbsDatastoreMetric {
@@ -170,6 +170,47 @@ describe('ServerCard — PVE', () => {
expect(mem).toHaveAttribute('data-level', 'crit');
});
it('quick-261005: je gestopptem Gast mit Autostart eine Zeile, Karte wird „Warnung“; ungepruefte als Hinweis', async () => {
const el = await card(
makeServer({
status: makeStatus({
metrics: {
productType: 'pve',
nodeCount: 2,
guestsRunning: 1,
guestsStopped: 3,
nodes: [{ node: 'pve1', cpu: 0.1, maxcpu: 8, mem: 1, maxmem: 100 }],
storages: [],
autostartStopped: [
{ vmid: 101, name: 'dc01', type: 'qemu', node: 'pve1', status: 'stopped' },
{ vmid: 201, name: null, type: 'lxc', node: 'pve2', status: 'paused' },
],
autostartUnchecked: 1,
},
}),
}),
);
expect(el).toHaveAttribute('data-health', 'warn');
const notice = within(el).getByTestId('autostart-notice');
expect(notice).toHaveTextContent('2 Gäste laufen nicht, obwohl Autostart aktiv ist');
expect(within(notice).getByTestId('autostart-guest-101')).toHaveTextContent(
'VM 101 „dc01“auf pve1 · gestoppt',
);
expect(within(notice).getByTestId('autostart-guest-201')).toHaveTextContent(
'LXC 201auf pve2 · angehalten',
);
expect(within(el).getByTestId('autostart-unchecked')).toHaveTextContent(
'Bei 1 gestopptem Gast konnte der Autostart nicht geprüft werden.',
);
});
it('quick-261005: ohne Autostart-Befund keine Warnzeile (auch bei altem Zwischenlagerstand)', async () => {
const el = await card(makeServer());
expect(within(el).queryByTestId('autostart-notice')).toBeNull();
expect(within(el).queryByTestId('autostart-unchecked')).toBeNull();
});
it('unbekannte Knotenwerte erscheinen als „unbekannt“ — nie 0 %, nie NaN', async () => {
const el = await card(
makeServer({
@@ -3,6 +3,7 @@
import Link from 'next/link';
import { useLocale, useTranslations } from 'next-intl';
import {
autostartAlerts,
formatAge,
formatCount,
formatPercent,
@@ -162,6 +163,62 @@ function Meter({
);
}
/**
* quick-261005: Warnung fuer jede VM bzw. jeden LXC, der nicht laeuft,
* obwohl Autostart aktiv ist. Darunter, falls vorhanden, wie viele
* gestoppte Gaeste nicht geprueft werden konnten — ehrlich statt still.
*/
function AutostartNotice({ metrics, t }: { metrics: ProxmoxPveMetrics; t: Translator }) {
const alerts = autostartAlerts(metrics);
const unchecked = metrics.autostartUnchecked ?? 0;
if (alerts.length === 0 && unchecked === 0) return null;
return (
<div className="space-y-2">
{alerts.length > 0 && (
<div
data-testid="autostart-notice"
className="space-y-1.5 rounded-lg border-l-4 border-status-warn bg-status-warn/8 px-4 py-3"
>
<p className={`text-sm font-medium ${HEALTH_STYLE.warn.text}`}>
{t('card.pve.autostartTitle', { count: alerts.length })}
</p>
<ul className="space-y-1">
{alerts.map((guest) => (
<li
key={`${guest.node}-${guest.vmid}`}
data-testid={`autostart-guest-${guest.vmid}`}
className="flex flex-wrap items-baseline gap-x-2 text-sm"
>
<span className="font-medium">
{t(`card.pve.guestType.${guest.type}`)} {guest.vmid}
{guest.name ? ` „${guest.name}“` : ''}
</span>
<span className="text-xs text-muted-foreground">
{t('card.pve.autostartGuestDetail', {
node: guest.node,
status: t(`card.pve.guestStatus.${guestStatusKey(guest.status)}`),
})}
</span>
</li>
))}
</ul>
</div>
)}
{unchecked > 0 && (
<p data-testid="autostart-unchecked" className="text-xs text-muted-foreground">
{t('card.pve.autostartUnchecked', { count: unchecked })}
</p>
)}
</div>
);
}
const KNOWN_GUEST_STATUS = new Set(['stopped', 'paused', 'suspended']);
function guestStatusKey(status: string): string {
return KNOWN_GUEST_STATUS.has(status) ? status : 'other';
}
function PveMetricsView({
metrics,
t,
@@ -202,6 +259,8 @@ function PveMetricsView({
</div>
</div>
<AutostartNotice metrics={metrics} t={t} />
{metrics.nodes.map((node) => (
<div
key={node.node}
@@ -188,6 +188,15 @@ describe('widgetKeyFigure', () => {
});
});
it('quick-261005: gestoppte Gaeste mit Autostart -> autostart mit Anzahl, Vorrang vor Auslastung', () => {
const guest = { vmid: 101, name: null, type: 'lxc' as const, node: 'n1', status: 'stopped' };
const metrics = pve({
nodes: [{ node: 'n1', cpu: 0.95, maxcpu: 8, mem: 1, maxmem: 10 }],
autostartStopped: [guest, { ...guest, vmid: 102 }],
});
expect(widgetKeyFigure(server({ metrics }), NOW)).toEqual({ kind: 'autostart', count: 2 });
});
it('PVE ok ohne Gaeste -> noGuests', () => {
expect(
widgetKeyFigure(server({ metrics: pve({ guestsRunning: 0, guestsStopped: 0 }) }), NOW),
@@ -1,4 +1,5 @@
import {
autostartAlerts,
HEALTH_ORDER,
isBackupStale,
type MeterLevel,
@@ -84,6 +85,7 @@ export type KeyFigure =
| { kind: 'status'; health: 'down' | 'idle' | 'orphan' }
| { kind: 'guests'; running: number; total: number }
| { kind: 'noGuests' }
| { kind: 'autostart'; count: number }
| { kind: 'load'; fraction: number; level: MeterLevel }
| { kind: 'backup'; at: number; stale: boolean }
| { kind: 'noBackup' }
@@ -124,6 +126,10 @@ export function widgetKeyFigure(server: ProxmoxServer, now: number = Date.now())
if (metrics === null) return { kind: 'unknown' };
if (metrics.productType === 'pve') {
// quick-261005: ein gestoppter Gast mit Autostart wiegt schwerer als
// hohe Auslastung — er ist vermutlich ungewollt aus.
const stoppedWithAutostart = autostartAlerts(metrics).length;
if (stoppedWithAutostart > 0) return { kind: 'autostart', count: stoppedWithAutostart };
if (health === 'warn') {
const fraction = maxKnown([
...metrics.nodes.map((n) => (isKnownNumber(n.cpu) ? n.cpu : null)),
@@ -167,6 +167,24 @@ describe('ProxmoxWidget: Balken und Zusammenfassung (quick-260924-i8v)', () => {
expect(summary).toHaveClass('text-status-ok-fg');
});
it('quick-261005: gestoppter Gast mit Autostart -> Warnung und Kennzahl in Warnfarbe', async () => {
mockListServers.mockResolvedValue([
makeServer('ok', {
metrics: pveMetrics({
autostartStopped: [
{ vmid: 101, name: 'dc01', type: 'qemu', node: 'n1', status: 'stopped' },
],
}),
}),
]);
await renderWidget();
const figure = await screen.findByTestId('proxmox-key-figure');
expect(figure).toHaveTextContent('1 gestoppt trotz Autostart');
expect(figure).toHaveClass('text-status-warn-fg');
expect(screen.getByTestId('proxmox-summary')).not.toHaveTextContent('Alles in Ordnung');
});
it('je ein down, warn, ok: „1 nicht erreichbar, 1 mit Warnung“ in der Farbe von down', async () => {
mockListServers.mockResolvedValue([makeServer('ok'), makeServer('warn'), makeServer('down')]);
await renderWidget();
@@ -282,6 +282,8 @@ export function ProxmoxWidget({ instanceId, config, isEditMode }: WidgetProps) {
return t('proxmox.guests', { running: figure.running, total: figure.total });
case 'noGuests':
return t('proxmox.noGuests');
case 'autostart':
return t('proxmox.autostartStopped', { count: figure.count });
case 'load':
return t('proxmox.load', { percent: formatPercent(figure.fraction, locale) });
case 'backup': {
@@ -300,6 +302,7 @@ export function ProxmoxWidget({ instanceId, config, isEditMode }: WidgetProps) {
function keyFigureColor(figure: KeyFigure): string {
if (figure.kind === 'status') return HEALTH_STYLE[figure.health].text;
if (figure.kind === 'load') return METER_TEXT[figure.level];
if (figure.kind === 'autostart') return HEALTH_STYLE.warn.text;
if (figure.kind === 'backup' && figure.stale) return HEALTH_STYLE.warn.text;
return 'text-muted-foreground';
}
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest';
import type { ProxmoxMetrics, ProxmoxServer, ProxmoxServerStatus } from '@/lib/proxmox-api';
import {
autostartAlerts,
formatAge,
formatCount,
formatPercent,
@@ -320,3 +321,25 @@ describe('formatPercent / formatCount', () => {
expect(formatCount(12904, 'de')).toBe('12.904');
});
});
describe('Autostart-Warnung (quick-261005)', () => {
const guest = { vmid: 101, name: 'dc01', type: 'qemu' as const, node: 'n0', status: 'stopped' };
it('gestoppter Gast mit Autostart macht einen sonst gesunden PVE-Server zu warn', () => {
const metrics = { ...pve([{ cpu: 0.1, mem: 1, maxmem: 10 }]), autostartStopped: [guest] };
expect(serverHealth(withMetrics(metrics), NOW)).toBe('warn');
});
it('leere Liste, fehlendes Feld (alter Zwischenlagerstand) und nur ungepruefte Gaeste bleiben ok', () => {
const base = pve([{ cpu: 0.1, mem: 1, maxmem: 10 }]);
expect(serverHealth(withMetrics({ ...base, autostartStopped: [] }), NOW)).toBe('ok');
expect(serverHealth(withMetrics(base), NOW)).toBe('ok');
expect(serverHealth(withMetrics({ ...base, autostartUnchecked: 3 }), NOW)).toBe('ok');
});
it('autostartAlerts liefert ohne Feld eine leere Liste', () => {
const base = pve([]) as Extract<ProxmoxMetrics, { productType: 'pve' }>;
expect(autostartAlerts(base)).toEqual([]);
expect(autostartAlerts({ ...base, autostartStopped: [guest] })).toEqual([guest]);
});
});
@@ -1,4 +1,9 @@
import type { ProxmoxMetrics, ProxmoxServer } from '@/lib/proxmox-api';
import type {
ProxmoxMetrics,
ProxmoxPveAutostartGuest,
ProxmoxPveMetrics,
ProxmoxServer,
} from '@/lib/proxmox-api';
/**
* Statuslogik der Proxmox-Seite (quick-260924-h7x) — reine Funktionen ohne
@@ -77,6 +82,14 @@ function isHigh(fraction: number | null): boolean {
return fraction !== null && fraction >= THRESHOLDS.usageWarn;
}
/**
* Gaeste, die nicht laufen, obwohl Autostart aktiv ist (quick-261005).
* Aeltere Zwischenlagerstaende ohne das Feld liefern eine leere Liste.
*/
export function autostartAlerts(metrics: ProxmoxPveMetrics): ProxmoxPveAutostartGuest[] {
return Array.isArray(metrics.autostartStopped) ? metrics.autostartStopped : [];
}
/** Liegt irgendein Messwert ueber seiner Warnschwelle? */
function metricsNeedAttention(metrics: ProxmoxMetrics, now: number): boolean {
if (metrics.productType === 'pve') {
@@ -84,7 +97,7 @@ function metricsNeedAttention(metrics: ProxmoxMetrics, now: number): boolean {
(n) => isHigh(isKnownNumber(n.cpu) ? n.cpu : null) || isHigh(ratio(n.mem, n.maxmem)),
);
const storageWarn = metrics.storages.some((s) => isHigh(ratio(s.disk, s.maxdisk)));
return nodeWarn || storageWarn;
return nodeWarn || storageWarn || autostartAlerts(metrics).length > 0;
}
if (metrics.productType === 'pbs') {
return metrics.datastores.some(
+13
View File
@@ -36,6 +36,15 @@ export interface ProxmoxPveStorageMetric {
maxdisk: number | null;
}
/** Gast, der nicht laeuft, obwohl Autostart aktiv ist (quick-261005). */
export interface ProxmoxPveAutostartGuest {
vmid: number;
name: string | null;
type: 'qemu' | 'lxc';
node: string;
status: string;
}
export interface ProxmoxPveMetrics {
productType: 'pve';
nodeCount: number;
@@ -43,6 +52,10 @@ export interface ProxmoxPveMetrics {
guestsStopped: number;
nodes: ProxmoxPveNodeMetric[];
storages: ProxmoxPveStorageMetric[];
/** Fehlt in Zwischenlagerstaenden von vor quick-261005. */
autostartStopped?: ProxmoxPveAutostartGuest[];
/** Gestoppte Gaeste, deren Autostart nicht gelesen werden konnte. */
autostartUnchecked?: number;
}
export interface ProxmoxPbsDatastoreMetric {
+16 -2
View File
@@ -473,7 +473,8 @@
"titlePlaceholder": "Titel (optional)",
"serversLabel": "Angezeigte Server",
"serversHint": "Ohne Auswahl zeigt die Kachel alle Server.",
"chooseServers": "Server auswählen"
"chooseServers": "Server auswählen",
"autostartStopped": "{count} gestoppt trotz Autostart"
},
"reminder": {
"name": "Erinnerungen",
@@ -1013,7 +1014,20 @@
"stopped": "gestoppt",
"nodes": "{count, plural, one {# Knoten} other {# Knoten}}",
"cpu": "Prozessor",
"mem": "Arbeitsspeicher"
"mem": "Arbeitsspeicher",
"autostartTitle": "{count, plural, one {# Gast läuft nicht, obwohl Autostart aktiv ist} other {# Gäste laufen nicht, obwohl Autostart aktiv ist}}",
"autostartGuestDetail": "auf {node} · {status}",
"autostartUnchecked": "Bei {count, plural, one {# gestopptem Gast} other {# gestoppten Gästen}} konnte der Autostart nicht geprüft werden. Dem Zugang fehlt vermutlich das Recht VM.Audit.",
"guestType": {
"qemu": "VM",
"lxc": "LXC"
},
"guestStatus": {
"stopped": "gestoppt",
"paused": "angehalten",
"suspended": "im Ruhezustand",
"other": "läuft nicht"
}
},
"pbs": {
"used": "Belegt",
+16 -2
View File
@@ -473,7 +473,8 @@
"titlePlaceholder": "Title (optional)",
"serversLabel": "Servers shown",
"serversHint": "With nothing selected, the tile shows all servers.",
"chooseServers": "Choose servers"
"chooseServers": "Choose servers",
"autostartStopped": "{count} stopped despite autostart"
},
"reminder": {
"name": "Reminders",
@@ -1013,7 +1014,20 @@
"stopped": "stopped",
"nodes": "{count, plural, one {# node} other {# nodes}}",
"cpu": "CPU",
"mem": "Memory"
"mem": "Memory",
"autostartTitle": "{count, plural, one {# guest is not running although autostart is enabled} other {# guests are not running although autostart is enabled}}",
"autostartGuestDetail": "on {node} · {status}",
"autostartUnchecked": "Autostart could not be checked for {count, plural, one {# stopped guest} other {# stopped guests}}. The access probably lacks the VM.Audit permission.",
"guestType": {
"qemu": "VM",
"lxc": "LXC"
},
"guestStatus": {
"stopped": "stopped",
"paused": "paused",
"suspended": "suspended",
"other": "not running"
}
},
"pbs": {
"used": "Used",
+2 -2
View File
@@ -94,7 +94,7 @@ Ihre Änderungen werden über **„Fertig"** übernommen. Verlassen Sie den Bear
| Stoppuhr | Zeitmessung mit Rundenzeiten |
| Bilderrahmen | Diashow aus eigenen Bildern: Sie laden Bilder hoch (PNG, JPEG, GIF oder WebP, höchstens 5 MB je Bild, bis zu 30 Bilder) oder binden Bilder über eine https-Adresse ein. Ob das Bild ganz sichtbar oder formatfüllend erscheint, wie oft gewechselt wird (oder gar nicht), ob in Reihenfolge oder zufällig, und welche Bildunterschrift jedes Bild trägt, stellen Sie unter Einstellungen > Dashboard ein. Ein Klick auf das Bild zeigt es groß; Escape oder ein Klick daneben schließt die Großansicht wieder |
| XFrame | Zeigt eine Webseite als Rahmen in der Kachel. Die https-Adresse, einen optionalen Titel und ob die Seite automatisch neu geladen wird (nie, 1 Minute bis 1 Stunde), stellen Sie unter Einstellungen > Dashboard ein. Die eingebettete Seite kann Tessera nicht verlassen; über „In neuem Tab öffnen“ erreichen Sie die Seite jederzeit direkt. Manche Webseiten erlauben das Einbetten nicht — der Rahmen bleibt dann leer, der Knopf funktioniert trotzdem. Wahlweise zeigen Sie nur einen Ausschnitt der Seite: den Rahmen in der Vorschau verschieben oder an den Ecken ziehen (oder Links, Oben, Breite und Höhe eintippen) – die Kachel zeigt dann genau diesen Ausschnitt, passend zu ihrer Größe; für die ganze Seite gibt es eine Vergrößerung (50 bis 150 %), und „Nur anzeigen“ sperrt Klicken und Scrollen im Rahmen. |
| Proxmox | Zeigt den Zustand Ihrer Proxmox-Server: oben ein farbiger Balken mit „Alles in Ordnung“ oder zum Beispiel „1 nicht erreichbar, 1 mit Warnung“, darunter die Server, auffällige zuerst, jeweils mit einer Kennzahl (laufende Gäste, Auslastung bei einer Warnung, letzte Sicherung oder eingehende E-Mails). Ein Klick auf einen Server öffnet die Proxmox-Seite. Die Kachel steht nur Benutzern zur Verfügung, die das Proxmox-Modul nutzen dürfen. Sie aktualisiert sich jede Minute aus dem zuletzt gespeicherten Stand und fragt die Server dabei nicht neu ab. Einen Titel und die Auswahl der angezeigten Server (ohne Auswahl: alle) legen Sie im Bearbeitungsmodus direkt an der Kachel über das Titelfeld und „Server auswählen“ fest oder unter Einstellungen > Dashboard. In einer schmalen Kachel stehen nur Punkte und Namen, in einer sehr kleinen nur Balken und Zusammenfassung |
| Proxmox | Zeigt den Zustand Ihrer Proxmox-Server: oben ein farbiger Balken mit „Alles in Ordnung“ oder zum Beispiel „1 nicht erreichbar, 1 mit Warnung“, darunter die Server, auffällige zuerst, jeweils mit einer Kennzahl (laufende Gäste, gestoppte Gäste mit Autostart, Auslastung bei einer Warnung, letzte Sicherung oder eingehende E-Mails). Ein Klick auf einen Server öffnet die Proxmox-Seite. Die Kachel steht nur Benutzern zur Verfügung, die das Proxmox-Modul nutzen dürfen. Sie aktualisiert sich jede Minute aus dem zuletzt gespeicherten Stand und fragt die Server dabei nicht neu ab. Einen Titel und die Auswahl der angezeigten Server (ohne Auswahl: alle) legen Sie im Bearbeitungsmodus direkt an der Kachel über das Titelfeld und „Server auswählen“ fest oder unter Einstellungen > Dashboard. In einer schmalen Kachel stehen nur Punkte und Namen, in einer sehr kleinen nur Balken und Zusammenfassung |
| Erinnerungen | Persönliche Erinnerungen mit Datum, Uhrzeit, Titel und Beschreibung. Über „Neue Erinnerung“ legen Sie eine an; die Liste zeigt Ihre Erinnerungen nach Zeit sortiert. Zur gewählten Zeit meldet sich Tessera mit einer Benachrichtigung – im Browser nach einer einmaligen Erlaubnis (der Browser fragt beim ersten Anlegen), in der Desktop-App als Windows-Benachrichtigung, auch wenn das Fenster im Infobereich liegt. Blockiert Ihr Browser Benachrichtigungen, sehen Sie die fällige Erinnerung nur hier in der Kachel; ein Hinweis in der Kachel sagt das. Wer möchte, setzt beim Anlegen den Haken „Zusätzlich per E-Mail erinnern“: Tessera schickt dann zur gewählten Zeit eine E-Mail an Ihre Adresse, auch wenn Tessera nirgends geöffnet ist. Der Haken ist ausgegraut, wenn Ihr Administrator noch keinen E-Mail-Versand eingerichtet hat oder in Ihrem Konto keine E-Mail-Adresse hinterlegt ist; die Kachel nennt den Grund. Eine fällige Erinnerung bleibt hervorgehoben mit „Fällig“ stehen. „Erledigt“ entfernt sie; „Später erinnern“ verschiebt sie auf in 10 Minuten, in 1 Stunde oder morgen zur gleichen Uhrzeit — dann melden sich Benachrichtigung und (falls gewählt) E-Mail noch einmal. Bearbeiten und Löschen sind nur möglich, solange die Erinnerung noch nicht fällig ist. Erinnerungen sind persönlich: nur Sie sehen und ändern Ihre |
Für Uhr, Suchleiste, Kalender, Notizen, Favoriten, Bilderrahmen, XFrame und Proxmox gibt es zusätzliche Einstellungen (z. B. Zeitzone und Schriftgröße der Uhr, eigene Suchanbieter, Kalenderquellen, Überschrift der Notiz- und Favoriten-Kachel, Bilder und Wechselintervall des Bilderrahmens, Adresse, Titel und Neuladen des XFrame, Titel und angezeigte Server der Proxmox-Kachel) — diese finden Sie unter **Einstellungen > Dashboard**, siehe [Persönliche Einstellungen](#persönliche-einstellungen).
@@ -164,7 +164,7 @@ Ein einfaches Werkzeug, um zu prüfen, ob eine Internet-Domain verfügbar ist. G
Das Modul zeigt den Zustand Ihrer Proxmox-Server auf einen Blick — für die drei Proxmox-Produkte PVE (Virtualisierung), PBS (Backup) und PMG (Mail-Gateway). Wichtig zu wissen: **Tessera verändert bei Proxmox nichts, es schaut nur zu.** Es gibt in diesem Modul keinen Weg, einen Server zu starten, zu stoppen, eine Sicherung auszulösen oder irgendeine Proxmox-Einstellung zu ändern.
**Die Modulseite** zeigt eine Karte je eingetragenem Server: Name, Typ und Adresse, dazu die zuletzt gemessenen Werte — bei PVE Anzahl Knoten sowie laufende/gestoppte virtuelle Maschinen und Container samt Auslastung je Knoten, bei PBS die Belegung je Datenspeicher mit dem Zeitpunkt der letzten Sicherung und deren Prüfergebnis, bei PMG die Tageszahlen eingehender und ausgehender E-Mails sowie Spam- und Virenfunde. Ein Wert, der „unbekannt" anzeigt, bedeutet nicht, dass etwas kaputt ist — er bedeutet, dass dieser eine Messwert beim letzten Abruf nicht in der erwarteten Form geliefert wurde. Der Knopf **„Jetzt aktualisieren"** fragt alle eingetragenen Server neu ab; regulär geschieht das automatisch im Hintergrund, in dem Abstand, den Sie je Server eingestellt haben.
**Die Modulseite** zeigt eine Karte je eingetragenem Server: Name, Typ und Adresse, dazu die zuletzt gemessenen Werte — bei PVE Anzahl Knoten sowie laufende/gestoppte virtuelle Maschinen und Container samt Auslastung je Knoten, bei PBS die Belegung je Datenspeicher mit dem Zeitpunkt der letzten Sicherung und deren Prüfergebnis, bei PMG die Tageszahlen eingehender und ausgehender E-Mails sowie Spam- und Virenfunde. Bei PVE warnt die Karte außerdem für jede virtuelle Maschine und jeden Container, der nicht läuft, obwohl in Proxmox der Autostart („Start at boot“) eingeschaltet ist — mit Nummer, Name, Knoten und Zustand. Ein solcher Gast ist vermutlich ungewollt aus; die Karte steht dann auf „Warnung“, und die Dashboard-Kachel zeigt „gestoppt trotz Autostart“. Vorlagen (Templates) zählen nicht. Kann Tessera die Autostart-Einstellung eines gestoppten Gasts nicht lesen, steht darunter ein Hinweis, bei wie vielen das so war; meist fehlt dem Zugang dann das Recht VM.Audit, das in der Rolle PVEAuditor enthalten ist. Ein Wert, der „unbekannt" anzeigt, bedeutet nicht, dass etwas kaputt ist — er bedeutet, dass dieser eine Messwert beim letzten Abruf nicht in der erwarteten Form geliefert wurde. Der Knopf **„Jetzt aktualisieren"** fragt alle eingetragenen Server neu ab; regulär geschieht das automatisch im Hintergrund, in dem Abstand, den Sie je Server eingestellt haben.
**Einen Server anlegen** (für Administratoren und Benutzer mit der Freigabestufe „Verwalten“, über den Link „Zu den Einstellungen"): Name, Typ (PVE/PBS/PMG), Adresse (z. B. `https://pve.intern:8006`) und Zugang. Beim Zugang wählen Sie zwischen einem API-Token oder Benutzername/Passwort — bei PMG bietet Tessera von vornherein nur Benutzername/Passwort an, weil dieses Proxmox-Produkt keine API-Token kennt. Ein einmal gespeichertes Geheimnis (Token oder Passwort) wird nie wieder im Klartext angezeigt; lassen Sie das Feld beim Bearbeiten leer, um es unverändert zu lassen, oder tragen Sie ein neues ein, um es zu ersetzen.