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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user