+
+
+
+
+
+
+
+
+
+
+
+), document.getElementById('root')!);
diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json
index fb410d4e7a..0c8ed6c767 100644
--- a/frontend-modern/browser-verification.json
+++ b/frontend-modern/browser-verification.json
@@ -1,64 +1,47 @@
{
"version": 1,
- "base_sha": "d25403b8c3e28354d0e62fc3fc35898cca3e8f1c",
- "verified_at": "2026-09-24T19:54:45Z",
+ "base_sha": "27036a244550f3095aa883a74716374107aaa6cd",
+ "verified_at": "2026-09-26T17:36:07Z",
"result": "passed",
"changed_paths": [
- "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx",
- "frontend-modern/src/components/Storage/useStoragePoolsTableWindowing.ts",
- "frontend-modern/src/components/Workloads/useWorkloadsState.ts",
- "frontend-modern/src/components/shared/InlineNotice.tsx",
- "frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx",
- "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx",
- "frontend-modern/src/hooks/useUnifiedResources.ts",
- "frontend-modern/src/hooks/useWorkloads.ts"
+ "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx",
+ "frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx",
+ "frontend-modern/src/hooks/useWorkloads.ts",
+ "frontend-modern/src/types/workloads.ts"
],
"content_sha256": {
- "frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx": "c112078ff621331510ac28490371ee7ba9099f7a1bbb64d0f66ff96400d4f89a",
- "frontend-modern/src/components/Storage/useStoragePoolsTableWindowing.ts": "3678e4b852ae0029760d3295fdd878cec436100f85c945dd46fe760da38887e4",
- "frontend-modern/src/components/Workloads/useWorkloadsState.ts": "7ae6fee729328039f4d1ade064ba5d6e899f0fdf0a72e96a7df7e034b4f464de",
- "frontend-modern/src/components/shared/InlineNotice.tsx": "1a239f4240c57f83383f0d936f2e90ea6cb122627299e17500c20c7236c04e9d",
- "frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.tsx": "7960f323dde72b2b2f9d961792d410db2bfd95bacd87b1ae55c1ed473737994e",
- "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "808ba390e1978c07f08a033bc23d5cf4d62731b6cc8b388c024180b3914f6fce",
- "frontend-modern/src/hooks/useUnifiedResources.ts": "c4d2f3c44b19c89d3cf0d15e033f825c37aaab985abfd963ddb5907cbab76fe2",
- "frontend-modern/src/hooks/useWorkloads.ts": "453b90cfe6df925bd28a8713548c1348767f3c398d08c972b9b1f493e70986ef"
+ "frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx": "f4f8903e442d92a622fa2517f54288fb0dfa97882136e268e8e0f540390127fe",
+ "frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx": "9640df56a51dbf27247613324a4f3fe1b4023e93d4a1efa913e234453acfd270",
+ "frontend-modern/src/hooks/useWorkloads.ts": "0b3ae83f83d2df3e2517d552f51b634e0eedecbfbd1756a2dadd834c81c67bd2",
+ "frontend-modern/src/types/workloads.ts": "4d779f40a56b83b83c8bbcb4b49f88e1db3432d071b8f6326094fa081547c1e6"
},
"routes": [
- "/proxmox/overview",
- "/proxmox/storage",
- "/alerts/overview",
- "/settings/infrastructure/agent-doctor"
+ "/browser-tests/guest-storage-2263.html"
],
"viewports": [
{
"width": 1440,
"height": 900
},
- {
- "width": 768,
- "height": 900
- },
{
"width": 390,
"height": 844
}
],
"states": [
- "50-node, 1,508-resource synthetic snapshot with two-second random metric updates and 49 outdated agents",
- "Proxmox node selection, Workloads search/filter reset, Storage record Overview and History",
- "Collapsed and keyboard-expanded affected-host notice, upgrade link and Agent Doctor guidance",
- "Agent Doctor first and later table rows, badge and Seen values, keyboard row expansion and collapse",
- "Mobile platform switcher open and Escape dismissed; alert Timeline expanded and dismissed",
- "WebSocket reconnect and reload with no extra /api/state request on reconnect"
+ "Production guest Overview mounted with synthetic agent-linked VM, one guest SMART disk and mdadm RAID; fixture also returns an unrelated host disk to check parent isolation",
+ "Guest SMART disclosure collapsed and keyboard-expanded; health, temperature and counters visible",
+ "Uninstrumented VM: no physical-disk card and no child query",
+ "Child-query HTTP 503: visible unavailable status, no misleading empty card",
+ "Desktop and phone screenshots inspected; no horizontal overflow or browser page errors"
],
"interactions": [
- "Hydrate Proxmox Overview, expand a node, filter 929 guests to one named container, clear the filter and navigate to Alerts.",
- "Switch Storage and Overview through live controls, open a storage record, select History and inspect chart placement and mobile scrolling.",
- "Focus and activate Show all 49 hosts by keyboard, inspect the full list, collapse it and use the upgrade link to open Agent Doctor.",
- "Inspect Agent Doctor at 1440, 768 and 390px, including guidance contrast, full badge and Seen text, first and later rows, keyboard expansion and return to Proxmox.",
- "Open the mobile platform switcher, inspect stacking and clipping, dismiss with Escape. Reconnect WebSocket and reload, confirming resource hydration.",
- "Navigate to Alerts and expand Timeline at desktop and phone widths; inspect dismissal and adjacent controls."
+ "Load current Vite source build in Chromium and inspect the linked guest Overview at 1440px and 390px",
+ "Focus the physical-disk summary and press Enter; inspect rendered SMART counters and adjacent RAID card",
+ "Select No agent and verify that no physical-disk request or card is made",
+ "Select Unavailable query and verify the explicit error status with no disk card",
+ "Assert both requests use type=physical_disk and exact canonical parent ID, never the VMID/table ID; reject unrelated host disk in client display"
],
- "command": "Playwright on the final rebuilt 50-node synthetic server at pulse-dev:7703 after the lazy registry index change: qa-agent-doctor-final-20260924.mjs, qa-final-20260924.mjs, qa-adjacent-20260924.mjs, qa-overlays-20260924.mjs, qa-notice-action-20260924.mjs.",
- "notes": "Final frontend content hashes bind the receipt to every changed frontend runtime file. The browser binary used internal/unifiedresources/registry.go SHA-256 bb4fa4a0d9a635971c93d93282663bb8088e25e020c087e75de5a7b196c7222d. Pixel screenshots inspected at desktop, intermediate and phone widths, including the deepest changed states. No page errors or horizontal overflow. The synthetic fixture does not qualify installed-fleet latency or the remaining roughly 4 MB initial resource stream."
+ "command": "pulse-worker-browser frontend-modern/browser-tests/guest-storage-2263.cjs (Playwright 1.56.1, Chromium 141.0.7390.37)",
+ "notes": "Synthetic network fixture mounts production GuestDrawerOverview, GuestPhysicalDisks, RaidCard and useUnifiedResources in Vite; it does not prove installed Proxmox data, full-app navigation, release placement or reporter confirmation. Screenshots and browser command output retained in the lane output directory."
}
diff --git a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx
index 39e66ca4a2..c7d95a9fae 100644
--- a/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx
+++ b/frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx
@@ -24,6 +24,8 @@ import {
} from './guestDrawerModel';
import type { NestedWorkloadContext } from './nestedWorkloadContext';
import { WORKLOAD_ACTION_AGENT_LABEL } from './workloadAgentReadiness';
+import { GuestPhysicalDisks } from './GuestPhysicalDisks';
+import { RaidCard } from '@/components/shared/cards/RaidCard';
import type { GuestDrawerProps } from './guestDrawerModel';
@@ -276,6 +278,18 @@ export function GuestDrawerOverview(props: GuestDrawerOverviewProps) {
dataTestId="guest-technical-details"
sections={[...overviewSections(), ...technicalSections()]}
/>
+
+
+
+
+
+
diff --git a/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx b/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx
new file mode 100644
index 0000000000..e487da5254
--- /dev/null
+++ b/frontend-modern/src/components/Workloads/GuestPhysicalDisks.tsx
@@ -0,0 +1,83 @@
+import { For, Show, createSignal, type Component } from 'solid-js';
+import { DiskDetail } from '@/components/Storage/DiskDetail';
+import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
+import { useUnifiedResources } from '@/hooks/useUnifiedResources';
+import {
+ extractPhysicalDiskPresentationData,
+ getPhysicalDiskHealthStatus,
+} from '@/features/storageBackups/diskPresentation';
+import type { Resource } from '@/types/resource';
+import { formatBytes } from '@/utils/format';
+import { formatTemperature } from '@/utils/temperature';
+
+const GuestPhysicalDiskRow: Component<{ disk: Resource }> = (props) => {
+ const [expanded, setExpanded] = createSignal(false);
+ const data = () => extractPhysicalDiskPresentationData(props.disk);
+ const health = () => getPhysicalDiskHealthStatus(data());
+ const label = () => data().model || data().devPath || props.disk.name;
+
+ return (
+ setExpanded(event.currentTarget.open)}
+ data-testid="guest-physical-disk"
+ >
+
+ {label()}
+
+ {data().devPath}
+
+
+ {health().label}
+
+ 0}>
+ {formatTemperature(data().temperature)}
+
+ 0}>
+ {formatBytes(data().size)}
+
+
+
+
+
+
+
+
+ );
+};
+
+export const GuestPhysicalDisks: Component<{ parentId: string }> = (props) => {
+ // The Proxmox Overview snapshot deliberately excludes physical disks. Query
+ // only the open guest's direct children rather than hydrating every disk in
+ // a large estate (or mistaking the VMID-based table key for the resource ID).
+ const parentId = props.parentId.trim();
+ const source = useUnifiedResources({
+ query: `type=physical_disk&parent=${encodeURIComponent(parentId)}`,
+ cacheKey: `guest-physical-disks:${parentId}`,
+ enabled: () => parentId.length > 0,
+ });
+ const disks = () =>
+ source
+ .resources()
+ .filter((resource) => resource.type === 'physical_disk' && resource.parentId === parentId);
+
+ return (
+ <>
+
+
+ Physical disk details are unavailable.
+
+
+ 0}>
+
+
+ Physical Disks & SMART ({disks().length})
+
+
+ {(disk) => }
+
+
+
+ >
+ );
+};
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.filesystems.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.filesystems.test.tsx
index acaa858c3e..38e5e81bce 100644
--- a/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.filesystems.test.tsx
+++ b/frontend-modern/src/components/Workloads/__tests__/GuestDrawerOverview.filesystems.test.tsx
@@ -55,6 +55,34 @@ const props: ComponentProps = {
describe('GuestDrawerOverview filesystem labels', () => {
afterEach(cleanup);
+ it('shows linked-agent RAID arrays beside the guest storage evidence', () => {
+ render(() => (
+
+ ));
+ expect(screen.getByText('Guest RAID')).toBeInTheDocument();
+ expect(screen.getByText('data')).toBeInTheDocument();
+ expect(screen.getByText('raid1')).toBeInTheDocument();
+ expect(screen.getByText('clean')).toBeInTheDocument();
+ });
+
it('shows shared-prefix paths above usage, including unknown usage, without relying on hover', () => {
render(() => );
for (const path of paths) {
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx
new file mode 100644
index 0000000000..45dfc75287
--- /dev/null
+++ b/frontend-modern/src/components/Workloads/__tests__/GuestPhysicalDisks.test.tsx
@@ -0,0 +1,92 @@
+import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Resource } from '@/types/resource';
+import { GuestPhysicalDisks } from '../GuestPhysicalDisks';
+
+const queryState = vi.hoisted(() => ({
+ resources: [] as Resource[],
+ error: undefined as unknown,
+ query: '',
+ enabled: false,
+}));
+
+vi.mock('@/hooks/useUnifiedResources', () => ({
+ useUnifiedResources: (options: { query: string; enabled: () => boolean }) => {
+ queryState.query = options.query;
+ queryState.enabled = options.enabled();
+ return { resources: () => queryState.resources, error: () => queryState.error };
+ },
+}));
+
+const disk: Resource = {
+ id: 'disk-101-a',
+ parentId: 'vm-resource-101',
+ type: 'physical_disk',
+ name: 'disk-101-a',
+ displayName: 'Guest SATA',
+ platformId: 'cluster-a',
+ platformType: 'proxmox-pve',
+ sourceType: 'agent',
+ status: 'online',
+ lastSeen: Date.now(),
+ physicalDisk: {
+ devPath: '/dev/sda',
+ model: 'Guest SATA',
+ diskType: 'sata',
+ sizeBytes: 1_000_000_000,
+ health: 'PASSED',
+ temperature: 45,
+ smart: { reallocatedSectors: 0, pendingSectors: 0, udmaCrcErrors: 2 },
+ },
+};
+
+describe('GuestPhysicalDisks', () => {
+ beforeEach(() => {
+ queryState.resources = [];
+ queryState.error = undefined;
+ queryState.query = '';
+ queryState.enabled = false;
+ });
+ afterEach(cleanup);
+
+ it('shows the linked guest disk and opens its existing SMART detail', () => {
+ queryState.resources = [disk, { ...disk, id: 'host-disk', parentId: 'pve-node-1' }];
+ render(() => );
+ expect(queryState.query).toBe('type=physical_disk&parent=vm-resource-101');
+ expect(queryState.enabled).toBe(true);
+ const card = screen.getByTestId('guest-physical-disks');
+ expect(within(card).getByText('Physical Disks & SMART (1)')).toBeInTheDocument();
+ expect(within(card).getByText('Guest SATA')).toBeInTheDocument();
+ expect(within(card).getByText('Healthy')).toBeInTheDocument();
+ expect(within(card).getByText('45°C')).toBeInTheDocument();
+ expect(within(card).queryByText('Reallocated Sectors')).toBeNull();
+
+ const row = within(card).getByTestId('guest-physical-disk') as HTMLDetailsElement;
+ row.open = true;
+ fireEvent(row, new Event('toggle'));
+ expect(within(card).getByText('Reallocated Sectors')).toBeInTheDocument();
+ expect(within(card).getByText('Pending Sectors')).toBeInTheDocument();
+ expect(within(card).getByText('CRC Errors')).toBeInTheDocument();
+ expect(within(card).getAllByTestId('guest-physical-disk')).toHaveLength(1);
+ });
+
+ it('does not add an empty storage card to an uninstrumented guest', () => {
+ render(() => );
+ expect(screen.queryByTestId('guest-physical-disks')).toBeNull();
+ });
+
+ it('encodes the parent value and never widens an empty parent to all disks', () => {
+ render(() => );
+ expect(queryState.query).toBe('type=physical_disk&parent=vm%26101');
+ cleanup();
+ render(() => );
+ expect(queryState.enabled).toBe(false);
+ expect(screen.queryByTestId('guest-physical-disks')).toBeNull();
+ });
+
+ it('makes a failed child query visible rather than silently implying no disks', () => {
+ queryState.error = new Error('unavailable');
+ render(() => );
+ expect(screen.getByRole('status')).toHaveTextContent('Physical disk details are unavailable.');
+ });
+});
diff --git a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
index e3a758519c..fc20e938e7 100644
--- a/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
+++ b/frontend-modern/src/hooks/__tests__/useWorkloads.test.ts
@@ -78,6 +78,46 @@ describe('useWorkloads', () => {
resetWorkloadsCacheForTests();
});
+ it('preserves linked-agent RAID arrays from the VM resource for guest detail', async () => {
+ const [snapshot] = createSignal([
+ {
+ ...sampleResource,
+ agent: {
+ agentVersion: '6.4.5',
+ raid: [
+ {
+ device: '/dev/md0',
+ name: 'data',
+ level: 'raid1',
+ state: 'clean',
+ totalDevices: 2,
+ activeDevices: 2,
+ workingDevices: 2,
+ failedDevices: 0,
+ spareDevices: 0,
+ },
+ ],
+ },
+ },
+ ] as any);
+ let dispose = () => {};
+ let result: ReturnType | undefined;
+ createRoot((d) => {
+ dispose = d;
+ result = useWorkloads(() => true, { resourceSnapshot: snapshot });
+ });
+ try {
+ await flushAsync();
+ expect(result!.workloads()[0]?.canonicalResourceId).toBe(sampleResource.id);
+ expect(result!.workloads()[0]?.agentRaid).toMatchObject([
+ { device: '/dev/md0', level: 'raid1', state: 'clean' },
+ ]);
+ expect(apiFetchJSONMock).not.toHaveBeenCalled();
+ } finally {
+ dispose();
+ }
+ });
+
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
diff --git a/frontend-modern/src/hooks/useWorkloads.ts b/frontend-modern/src/hooks/useWorkloads.ts
index ab28ad2c6c..89e6eeb684 100644
--- a/frontend-modern/src/hooks/useWorkloads.ts
+++ b/frontend-modern/src/hooks/useWorkloads.ts
@@ -19,6 +19,7 @@ import {
getPreferredResourceKubernetesContext,
} from '@/utils/resourceIdentity';
import type { WorkloadGuest } from '@/types/workloads';
+import type { HostRAIDArray } from '@/types/api';
import type {
Resource,
ResourceActionReadiness,
@@ -134,6 +135,7 @@ type APIResource = {
agentVersion?: string;
networkInterfaces?: APINetworkInterface[];
disks?: APIDiskInfo[];
+ raid?: HostRAIDArray[];
};
docker?: {
containerId?: string;
@@ -526,6 +528,7 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => {
return {
id: guestId,
+ canonicalResourceId: resource.id,
alertResourceIds: buildAlertResourceIds(resource),
metricsTarget: resource.metricsTarget,
vmid: Number.isFinite(vmid) ? vmid : 0,
@@ -579,6 +582,7 @@ const mapResourceToWorkload = (resource: APIResource): WorkloadGuest | null => {
osVersion: resource.agent?.osVersion ?? resource.proxmox?.osVersion,
agentVersion: resource.agent?.agentVersion,
agentKind: resource.agent?.agentVersion ? 'pulse' : undefined,
+ agentRaid: resource.agent?.raid,
networkInterfaces: mapNetworkInterfaces(resource.agent?.networkInterfaces),
networkIn: finiteMetricNumber(resource.metrics?.netIn?.value) ?? 0,
networkOut: finiteMetricNumber(resource.metrics?.netOut?.value) ?? 0,
diff --git a/frontend-modern/src/types/workloads.ts b/frontend-modern/src/types/workloads.ts
index bf079ab599..12005d1459 100644
--- a/frontend-modern/src/types/workloads.ts
+++ b/frontend-modern/src/types/workloads.ts
@@ -1,4 +1,4 @@
-import type { VM, Container, DockerContainerUpdateStatus } from './api';
+import type { VM, Container, DockerContainerUpdateStatus, HostRAIDArray } from './api';
import type {
ResourceActionReadiness,
ResourceAvailabilityMeta,
@@ -24,6 +24,8 @@ export type WorkloadGuest = (VM | Container) & {
workloadType?: WorkloadType;
/** Backend identities that may own active alerts for this canonical workload. */
alertResourceIds?: string[];
+ /** Exact unified-resource ID; a Proxmox row ID can be a different VMID-based display key. */
+ canonicalResourceId?: string;
/** Backend-owned coordinates for metrics history; distinct from the stable workload row id. */
metricsTarget?: ResourceMetricsTarget;
/** Canonical unified-resource health, kept separate from runtime power state in `status`. */
@@ -52,6 +54,8 @@ export type WorkloadGuest = (VM | Container) & {
containerRuntime?: string;
/** Identifies the producer of agentVersion instead of inferring it from VM type. */
agentKind?: 'pulse' | 'qemu-guest';
+ /** RAID arrays reported by the linked Pulse agent, not Proxmox virtual disks. */
+ agentRaid?: HostRAIDArray[];
updateStatus?: DockerContainerUpdateStatus;
// Server-evaluated capability refusals from the unified resource. The
// update button reads this to disable itself with the refusal reason