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
@@ -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}