From c9d4cdebf04194662d5b367a5cda6cc253533008 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:06:37 +0100 Subject: [PATCH] Read a silent in-guest agent's guest drawer readings as last known MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Pulse agent linked into a VM keeps its last report on the VM resource after it stops reporting: the registry copies the agent's whole facet, agent.stale included, onto the guest. The guest drawer never read that flag, so a quiet agent's RAID kept its live colours and "Rebuild: 42% · 118M/sec", its network rows and version read as current, and nothing said the agent had gone. - useWorkloads maps agent.stale onto the row as agentStale, and sets disksFromAgent when Proxmox reported no guest filesystems and the agent's own disks filled in. - The guest drawer passes the stopped-reporting reason to RaidCard, puts a status line over muted network rows, marks the Pulse Agent row as stopped reporting, and reads agent-supplied filesystem rows as last known. - Agent-supplied filesystem rows follow the agent's freshness instead of the Proxmox guest-read reason. A VM with a live linked agent but no qemu-guest-agent showed the agent's filesystems as "Usage unavailable" with advice to install qemu-guest-agent. A stopped VM keeps the Proxmox reason, and Proxmox's own guest filesystems keep their read state. The workload table's disk cell, the drawer History, and the agent's OS, addresses and physical disk health still carry no stale cue. --- .../subsystems/performance-and-scalability.md | 18 +++ .../d4fbbd15ad939e02.json | 47 ++++++ .../components/Workloads/GuestDrawer.test.tsx | 151 ++++++++++++++++++ .../Workloads/GuestDrawerOverview.tsx | 71 ++++++-- .../src/hooks/__tests__/useWorkloads.test.ts | 48 ++++++ frontend-modern/src/hooks/useWorkloads.ts | 5 + frontend-modern/src/types/workloads.ts | 10 ++ 7 files changed, 337 insertions(+), 13 deletions(-) create mode 100644 frontend-modern/browser-verification/d4fbbd15ad939e02.json diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 55a9ba06fa..b339107a3e 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -2732,6 +2732,24 @@ must remain distinguishable without hover. Workloads supplies this row metadata, not local table CSS or a duplicate renderer; non-filesystem rows keep the compact default. Verification covers the opt-in at this composition boundary and actual long-path wrapping in the shared renderer's browser proof. +A guest's linked Pulse Agent can stop reporting while Proxmox keeps polling the +VM. `useWorkloads` copies `resource.agent.stale` onto the row as `agentStale`, +and sets `disksFromAgent` when Proxmox reported no guest filesystems and the +agent's own `disks` filled in. When that agent is stale, `GuestDrawerOverview.tsx` +reads the Guest RAID card as last known through the shared `RaidCard` +`lastKnownReason` prop with `HOST_AGENT_STOPPED_REPORTING_REASON`, puts a status +line over muted Network rows, reads agent-supplied Filesystems rows through the +existing `last-known` disk read state, and marks the Pulse Agent row as stopped +reporting. Agent-supplied filesystem rows follow the agent's freshness rather +than the Proxmox `diskStatusReason`, which describes Proxmox's own guest reads, +so a live agent's rows keep their usage and bars. Those rows read as +unavailable once the guest is stopped, whether its status or the Proxmox +`vm-stopped` reason says so, and Proxmox's own guest filesystems keep their +Proxmox read state. The two flags ride the existing row mapping and +add no query or poll; `GuestDrawer.test.tsx` and `useWorkloads.test.ts` pin +both sources. The workload table's disk cell and the drawer History still read +the aggregate disk metric and the Proxmox reason, and the agent's OS, addresses +and physical disk health carry no stale cue yet. Guest, node, and Docker-host drawer headers follow the same frontend-primitives dependency boundary for collapse: Workloads owns which inline row is selected and the close handler, while diff --git a/frontend-modern/browser-verification/d4fbbd15ad939e02.json b/frontend-modern/browser-verification/d4fbbd15ad939e02.json new file mode 100644 index 0000000000..28d656c133 --- /dev/null +++ b/frontend-modern/browser-verification/d4fbbd15ad939e02.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "base_sha": "43a80dafd91e92d3079a039cdb08929281812eef", + "verified_at": "2026-10-07T13:06:25Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx", + "frontend-modern/src/hooks/useWorkloads.ts", + "frontend-modern/src/types/workloads.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "f984547722c304cd3c80610a3e5ef743b3e6ad268493016ddc649c858e54e6de", + "frontend-modern/src/hooks/useWorkloads.ts": "1439f5bc51ad5e362eb21b65b9d95b870f84cdae823770f4ae41dd848905703d", + "frontend-modern/src/types/workloads.ts": "4eebb6b801a056900e7fc309679cdca891b48bdd2672e40dbb58eaffce4ca0c6" + }, + "routes": [ + "/proxmox?q=analytics-batch-310", + "/proxmox?q=analytics-batch-118", + "/proxmox?q=analytics-batch-58", + "/proxmox?q=analytics-batch-262" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Private mock stack built from origin/main 43a80dafd9 plus this change, with a local-only go build -overlay of internal/mock/generator.go: host agents apollo-114 and meridian-731 held past their reporting lease (offline, LastSeen frozen 14 minutes back, readings retained); all four standalone agents seeded with md0 raid1 clean and md1 raid5 'clean, degraded, recovering' at 42.4% / 118M/sec (sdd1 faulty, sde1 spare rebuilding); analytics-batch-310 and analytics-batch-58 forced to report no Proxmox guest filesystems with diskStatusReason agent-not-running", + "Agents linked into VMs through the real POST /api/resources/{vm}/link: apollo-114 -> analytics-batch-118, meridian-731 -> analytics-batch-310 (both silent), spectrum-709 -> analytics-batch-262 and meridian-848 -> analytics-batch-58 (both live). /api/resources serves agent.stale true with the agent's raid, disks and networkInterfaces on the two silent VMs and no stale flag on the two live ones", + "Before this change (same stack, PR 2652 head, real browser run): silent VMs showed Guest RAID with emerald 'clean', amber 'clean, degraded, recovering', 'Rebuild: 42% \u00b7 118M/sec' and coloured member badges, network rows and 'Pulse 0.2.0-alpha' as current; the live analytics-batch-58 showed its agent filesystems as 'Usage unavailable \u00b7 ?/328 GB' under 'Guest agent not running. Install and start qemu-guest-agent in the VM.'", + "After, silent analytics-batch-310 (agent filesystems) at 1440 and 390: Pulse Agent row 'Pulse 0.2.0-alpha \u00b7 stopped reporting' in amber with title 'Pulse Agent 0.2.0-alpha has stopped reporting. Its readings here are its last report.'; Filesystems status 'Using last known disk stats. The Pulse Agent in this guest stopped reporting.' with 'Last known 75%' / 'Last known 42%' rows and no bars; Network status 'Using last known interface details. The Pulse Agent in this guest stopped reporting.' over a muted eth0 row; both RAID arrays data-raid-reading=last-known, '(last known)' states in muted text titled 'Last known reading, not current: host agent stopped reporting', 'Rebuild was at 42%', neutral badges 'sdd1 \u00b7 faulty' and 'sde1 \u00b7 spare rebuilding'", + "After, silent analytics-batch-118 with Proxmox guest filesystems (earlier run of the same change): RAID, network and agent row read as above while the Proxmox filesystem rows kept their usage and bars", + "After, live analytics-batch-58 (agent filesystems, Proxmox agent-not-running): '/ 65%' and '/data 28%' with progress bars, no status line, no qemu-guest-agent advice; RAID current with emerald/amber states and 'Rebuild: 42% \u00b7 118M/sec'", + "After, live control analytics-batch-262 (Proxmox filesystems): unchanged at both widths", + "No horizontal page overflow at either width; no page errors. Stopped guests were not exercised in the browser: their agent filesystems read 'Usage unavailable' under the stopped-VM message whether the guest status or the Proxmox vm-stopped reason says stopped, and the stale flip on an open drawer, are covered by GuestDrawer.test.tsx. The final stopped-status gate changes only stopped guests, so the running-guest states above were observed on this code path" + ], + "interactions": [ + "Opened each guest's inline drawer from the Proxmox workloads table filtered by name (name cell click at 1440, first visible cell tap at 390)", + "Scrolled the phone drawer to the Network section and Guest RAID card and captured both", + "Read computed text colour, titles and data-raid-reading of every Filesystems, Network, System and RAID row via Playwright" + ] +} diff --git a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx index 5f094a8215..2e4f080e38 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawer.test.tsx @@ -1602,6 +1602,157 @@ describe('GuestDrawer', () => { }); }); + // ── In-guest Pulse Agent readings ── + + describe('In-guest Pulse Agent readings', () => { + // Proxmox read no guest filesystems, so these rows came from the agent. + const makeAgentGuest = (overrides: Partial = {}) => + makeGuest({ + agentVersion: '6.4.5', + agentKind: 'pulse', + disksFromAgent: true, + diskStatusReason: 'agent-not-running', + disks: [{ total: 100, used: 75, free: 25, usage: 75, mountpoint: '/' }], + networkInterfaces: [{ name: 'eth0', addresses: ['192.168.1.5'], rxBytes: 1024 }], + agentRaid: [ + { + device: '/dev/md1', + name: 'data', + level: 'raid5', + state: 'clean, degraded, recovering', + totalDevices: 3, + activeDevices: 2, + workingDevices: 3, + failedDevices: 1, + spareDevices: 0, + devices: [{ device: 'sdd1', state: 'faulty', slot: 1 }], + rebuildPercent: 42, + rebuildSpeed: '118M/sec', + }, + ], + ...overrides, + }); + + it("reads a silent in-guest agent's RAID, network and filesystems as last known", () => { + render(() => ); + + expect(technicalDetails().getByText('Pulse 6.4.5 · stopped reporting')).toHaveAttribute( + 'title', + 'Pulse Agent 6.4.5 has stopped reporting. Its readings here are its last report.', + ); + expect( + technicalDetails().getByText( + 'Using last known disk stats. The Pulse Agent in this guest stopped reporting.', + ), + ).toBeInTheDocument(); + expect(technicalDetails().getByText(/^Last known 75%/)).toBeInTheDocument(); + expect( + technicalDetails().queryByRole('progressbar', { name: 'Filesystem / utilization' }), + ).toBeNull(); + expect( + technicalDetails().getByText( + 'Using last known interface details. The Pulse Agent in this guest stopped reporting.', + ), + ).toBeInTheDocument(); + expect( + technicalDetails() + .getByText(/^192\.168\.1\.5/) + .closest('td'), + ).toHaveClass('text-muted'); + // Proxmox's guest-read reason does not describe rows the agent supplied. + expect(technicalDetails().queryByText(/qemu-guest-agent/)).toBeNull(); + + const array = screen.getByTestId('raid-card-array'); + expect(array).toHaveAttribute('data-raid-reading', 'last-known'); + expect(within(array).getByTestId('raid-card-state')).toHaveAttribute( + 'title', + 'Last known reading, not current: host agent stopped reporting', + ); + expect(within(array).getByTestId('raid-card-rebuild')).toHaveTextContent( + 'Rebuild was at 42%', + ); + }); + + it('keeps a live in-guest agent current when Proxmox has no guest reads', () => { + render(() => ); + + expect(technicalDetails().getByText('Pulse 6.4.5')).toBeInTheDocument(); + expect( + technicalDetails().getByRole('progressbar', { name: 'Filesystem / utilization' }), + ).toHaveAttribute('aria-valuenow', '75'); + expect(technicalDetails().queryByText(/Last known|qemu-guest-agent/)).toBeNull(); + expect(screen.getByTestId('raid-card-array')).toHaveAttribute('data-raid-reading', 'current'); + expect(screen.getByTestId('raid-card-rebuild')).toHaveTextContent('Rebuild: 42% · 118M/sec'); + }); + + it("keeps Proxmox's own guest filesystems current under a silent agent", () => { + render(() => ( + + )); + + expect( + technicalDetails().getByRole('progressbar', { name: 'Filesystem / utilization' }), + ).toHaveAttribute('aria-valuenow', '75'); + expect(technicalDetails().queryByText(/Using last known disk stats/)).toBeNull(); + expect(screen.getByTestId('raid-card-array')).toHaveAttribute( + 'data-raid-reading', + 'last-known', + ); + }); + + // Proxmox gives vm-stopped only for a guest with allocated disk, so the + // guest's own stopped status must hold the rows too. + it.each([ + ['with the Proxmox reason', 'vm-stopped'], + ['without a Proxmox reason', undefined], + ])( + "reads a stopped guest's agent filesystems as unavailable before the lease lapses, %s", + (_label, diskStatusReason) => { + render(() => ( + + )); + + expect( + technicalDetails().getByText( + 'Guest filesystem stats unavailable while the VM is stopped.', + ), + ).toBeInTheDocument(); + expect(technicalDetails().getByText(/^Usage unavailable/)).toBeInTheDocument(); + expect( + technicalDetails().queryByRole('progressbar', { name: 'Filesystem / utilization' }), + ).toBeNull(); + }, + ); + + it('follows the agent between silent and reporting while the drawer stays open', () => { + const [guest, setGuest] = createSignal(makeAgentGuest()); + render(() => ); + expect(screen.getByTestId('raid-card-array')).toHaveAttribute('data-raid-reading', 'current'); + + setGuest({ ...guest(), agentStale: true }); + expect(screen.getByTestId('raid-card-array')).toHaveAttribute( + 'data-raid-reading', + 'last-known', + ); + expect(technicalDetails().getByText('Pulse 6.4.5 · stopped reporting')).toBeInTheDocument(); + expect(technicalDetails().getByText(/^Last known 75%/)).toBeInTheDocument(); + + setGuest({ ...guest(), agentStale: undefined }); + expect(screen.getByTestId('raid-card-array')).toHaveAttribute('data-raid-reading', 'current'); + expect(technicalDetails().queryByText(/stopped reporting|Last known/)).toBeNull(); + }); + }); + // ── WebInterfaceUrlField ── describe('WebInterfaceUrlField', () => { diff --git a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx index 072c4c3e4a..7218755137 100644 --- a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx +++ b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx @@ -10,6 +10,7 @@ import { TechnicalDetailsSection } from '@/components/shared/TechnicalDetailsDis import { DrawerAttentionSection } from '@/components/shared/DrawerAttentionSection'; import type { Alert } from '@/types/api'; import { AvailabilityProbeStatusCards } from '@/components/Infrastructure/AvailabilityProbeStatusCard'; +import { HOST_AGENT_STOPPED_REPORTING_REASON } from '@/components/Infrastructure/resourceDetailMappers'; import type { DiscoveryIdentifiedSummary } from '@/utils/discoveryPresentation'; import { formatBytes } from '@/utils/format'; import type { MetricDisplayThresholds } from '@/utils/metricThresholds'; @@ -79,6 +80,10 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { ageClass: props.backupPresentation?.ageClass, }); const guestReadPresentation = () => getGuestDrawerGuestReadPresentation(props.guest); + // A linked Pulse agent past its reporting lease leaves its last report on + // the guest, so its RAID, network and filesystem rows read as last known. + const agentLastKnownReason = () => + props.guest.agentStale ? HOST_AGENT_STOPPED_REPORTING_REASON : undefined; const coverageLabel = () => { if (props.hasWorkloadActionAgent) return WORKLOAD_ACTION_AGENT_LABEL; if (props.hasAgentInfo) return `${props.agentHeading} observed`; @@ -147,9 +152,22 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { props.guest.vmware?.datacenterName || props.guest.vmware?.clusterName, ); - const diskReason = isGuestDrawerVM(props.guest) ? props.guest.diskStatusReason : undefined; - const diskReadState = - props.guest.telemetryAvailability?.disk === false + // Rows the linked Pulse agent supplied follow that agent's freshness, not + // the Proxmox reason for its own guest reads. A stopped guest has no + // current filesystems from either source, with or without the Proxmox + // vm-stopped reason, which it gives only for a guest with allocated disk. + const vmDiskReason = isGuestDrawerVM(props.guest) ? props.guest.diskStatusReason : undefined; + const agentDisks = props.guest.disksFromAgent === true; + const agentDisksStopped = + agentDisks && (props.guest.status === 'stopped' || vmDiskReason === 'vm-stopped'); + const diskReason = agentDisks ? undefined : vmDiskReason; + const diskReadState = agentDisks + ? agentDisksStopped + ? 'unavailable' + : agentLastKnownReason() + ? 'last-known' + : 'current' + : props.guest.telemetryAvailability?.disk === false ? 'unavailable' : diskReason ? diskReason.startsWith('prev-') @@ -159,11 +177,19 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { const diskRows = (props.guest.disks ?? []).map((disk, index) => buildWorkloadsDiskPresentation(disk, index, props.diskThresholds, diskReadState), ); - const diskStatusMessage = diskReason - ? getWorkloadGuestDiskStatusMessage(diskReason) - : diskReadState === 'unavailable' - ? 'Filesystem usage is unavailable.' - : null; + const diskStatusMessage = agentDisks + ? agentDisksStopped + ? isGuestDrawerVM(props.guest) + ? getWorkloadGuestDiskStatusMessage('vm-stopped') + : 'Filesystem usage is unavailable.' + : agentLastKnownReason() + ? 'Using last known disk stats. The Pulse Agent in this guest stopped reporting.' + : null + : diskReason + ? getWorkloadGuestDiskStatusMessage(diskReason) + : diskReadState === 'unavailable' + ? 'Filesystem usage is unavailable.' + : null; return compactDetailSections([ discovery @@ -186,7 +212,13 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { rows: compactDetailRows([ props.guest.cpus ? makeDetailRow('CPUs', `${props.guest.cpus}`) : null, props.hasAgentInfo - ? makeDetailRow(props.agentHeading, props.agentLabel, { title: props.agentTitle }) + ? agentLastKnownReason() + ? makeDetailRow(props.agentHeading, `${props.agentLabel} · stopped reporting`, { + title: `${props.agentTitle} has stopped reporting. Its readings here are its last report.`, + tone: 'warning', + wrap: true, + }) + : makeDetailRow(props.agentHeading, props.agentLabel, { title: props.agentTitle }) : null, ]), }, @@ -272,8 +304,16 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { props.hasNetworkInterfaces ? { label: 'Network', - rows: compactDetailRows( - props.networkInterfaces.slice(0, 4).map((iface, index) => { + rows: compactDetailRows([ + // Only the linked Pulse agent reports guest interfaces. + makeDetailRow( + 'Status', + agentLastKnownReason() + ? 'Using last known interface details. The Pulse Agent in this guest stopped reporting.' + : null, + { layout: 'stacked', wrap: true }, + ), + ...props.networkInterfaces.slice(0, 4).map((iface, index) => { const addresses = iface.addresses ?? []; const hasTraffic = (iface.rxBytes ?? 0) > 0 || (iface.txBytes ?? 0) > 0; const detail = [ @@ -291,11 +331,12 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { iface.name || `Interface ${index + 1}`, detail || 'No details', { + tone: agentLastKnownReason() ? 'muted' : undefined, wrap: true, }, ); }), - ), + ]), } : null, ]); @@ -338,7 +379,11 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) { - +
diff --git a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts index 8a19338644..1549c6c930 100644 --- a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts +++ b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts @@ -142,6 +142,54 @@ describe('useWorkloads', () => { } }); + it('marks a silent linked agent and the guest filesystems it supplied', async () => { + const disk = (mountpoint: string) => ({ + mountpoint, + type: 'ext4', + total: 100, + used: 40, + free: 60, + usage: 40, + }); + const [snapshot] = createSignal([ + // Proxmox read no guest filesystems, so the agent's own report fills in. + { + ...sampleResource, + id: 'vm-agent-disks', + vmid: 102, + sources: ['proxmox', 'agent'], + agent: { agentVersion: '6.4.5', stale: true, disks: [disk('/agent')] }, + }, + // Proxmox guest filesystems win over a live agent's. + { + ...sampleResource, + id: 'vm-platform-disks', + vmid: 103, + sources: ['proxmox', 'agent'], + proxmox: { disks: [disk('/platform')] }, + agent: { agentVersion: '6.4.5', stale: false, disks: [disk('/agent')] }, + }, + ] as any); + let dispose = () => {}; + let result: ReturnType | undefined; + createRoot((d) => { + dispose = d; + result = useWorkloads(() => true, { resourceSnapshot: snapshot }); + }); + try { + await flushAsync(); + const byId = (id: string) => + result!.workloads().find((workload) => workload.canonicalResourceId === id); + expect(byId('vm-agent-disks')).toMatchObject({ agentStale: true, disksFromAgent: true }); + expect(byId('vm-agent-disks')?.disks?.map((d) => d.mountpoint)).toEqual(['/agent']); + expect(byId('vm-platform-disks')?.agentStale).toBeUndefined(); + expect(byId('vm-platform-disks')?.disksFromAgent).toBeUndefined(); + expect(byId('vm-platform-disks')?.disks?.map((d) => d.mountpoint)).toEqual(['/platform']); + } finally { + dispose(); + } + }); + afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); diff --git a/frontend-modern/src/hooks/useWorkloads.ts b/frontend-modern/src/hooks/useWorkloads.ts index 35f238e8cb..f8d45872eb 100644 --- a/frontend-modern/src/hooks/useWorkloads.ts +++ b/frontend-modern/src/hooks/useWorkloads.ts @@ -146,6 +146,7 @@ type APIResource = { networkInterfaces?: APINetworkInterface[]; disks?: APIDiskInfo[]; raid?: HostRAIDArray[]; + stale?: boolean; }; docker?: { containerId?: string; @@ -592,6 +593,7 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => { })(), disk: buildMetric(resource.metrics?.disk), disks: normalizeDiskArray(resource.proxmox?.disks ?? resource.agent?.disks), + disksFromAgent: (resource.proxmox?.disks == null && resource.agent?.disks != null) || undefined, // Both the API and owning platform snapshot pass through this mapper. // A retained numeric metric must not lose the provider's read deferral. diskStatusReason: resource.proxmox?.diskStatusReason, @@ -612,6 +614,9 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => { agentId: resource.agent?.agentId, agentRaid: resource.agent?.raid, networkInterfaces: mapNetworkInterfaces(resource.agent?.networkInterfaces), + // A linked agent past its reporting lease keeps its last report on the + // resource, so its RAID, network and filesystem rows are not current. + agentStale: resource.agent?.stale === true || undefined, networkIn: finiteMetricNumber(resource.metrics?.netIn?.value) ?? 0, networkOut: finiteMetricNumber(resource.metrics?.netOut?.value) ?? 0, diskRead: finiteMetricNumber(resource.metrics?.diskRead?.value) ?? 0, diff --git a/frontend-modern/src/types/workloads.ts b/frontend-modern/src/types/workloads.ts index 411d3c3777..b4bb88c54e 100644 --- a/frontend-modern/src/types/workloads.ts +++ b/frontend-modern/src/types/workloads.ts @@ -60,6 +60,16 @@ export type WorkloadGuest = (VM | Container) & { agentId?: string; /** RAID arrays reported by the linked Pulse agent, not Proxmox virtual disks. */ agentRaid?: HostRAIDArray[]; + /** + * Set when the linked Pulse agent is past its reporting lease. Its RAID, + * network and filesystem readings are then its last report, not current. + */ + agentStale?: boolean; + /** + * Set when `disks` came from the linked Pulse agent because the platform + * reported no guest filesystems, so the rows follow that agent's freshness. + */ + disksFromAgent?: boolean; updateStatus?: DockerContainerUpdateStatus; // Server-evaluated capability refusals from the unified resource. The // update button reads this to disable itself with the refusal reason