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