From dcd59bf11b4194da84e26091fea262f30a77329f Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 17 May 2026 13:59:37 +0100 Subject: [PATCH] Fix Proxmox node version display Read canonical Proxmox pveVersion metadata for host rows and share compact PVE version formatting across the Proxmox page and node headers. Add contract and proof coverage for the new Proxmox metadata fields. --- .../subsystems/frontend-primitives.md | 8 ++++++ .../internal/subsystems/unified-resources.md | 13 +++++----- .../src/components/shared/NodeGroupHeader.tsx | 11 ++------ .../SharedPrimitives.guardrails.test.ts | 11 ++++++++ .../__tests__/proxmoxPageModel.test.ts | 25 +++++++++++++++++++ .../src/features/proxmox/proxmoxPageModel.ts | 10 +++++++- .../src/types/__tests__/resource.test.ts | 4 +++ frontend-modern/src/types/resource.ts | 2 ++ frontend-modern/src/utils/proxmoxVersion.ts | 10 ++++++++ 9 files changed, 78 insertions(+), 16 deletions(-) create mode 100644 frontend-modern/src/utils/proxmoxVersion.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1322b762d..c6da9382d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -730,6 +730,11 @@ AI runtime. `pveVersion` or a Pulse Agent report whose OS identity resolves to Unraid or Proxmox VE. They must omit the version rather than showing unrelated collector OS versions, such as Debian 12, beside an API-backed PVE badge. + Shared row primitives that render Proxmox node identity, including + `frontend-modern/src/components/shared/NodeGroupHeader.tsx`, must route raw + PVE manager payloads through + `frontend-modern/src/utils/proxmoxVersion.ts` rather than inlining + page-local parsing or falling back to unrelated agent OS versions. System title metadata must apply the same identity rule: once the primary system badge names a platform with its version, source/method context may still add collection labels such as Pulse Agent, but it must not repeat the @@ -1389,6 +1394,9 @@ cards or page-local toolbar wrappers inside `TableCard`. Proxmox host grouping also extends the shared `NodeGroupHeader` row pattern: host metrics may align with workload table columns, but the shared primitive owns the header/table shell boundary rather than platform pages copying their own card headers. +Compact PVE version text in that header must come from the shared Proxmox +version formatter so raw `pve-manager/...` payloads and platform-page host +version cells stay consistent. Mobile navigation now recognizes `proxmox` as a first-class platform tab in the shared priority model so app-shell ordering remains centralized. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 684b8283b..4bbf218a9 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -957,9 +957,10 @@ host-profile token projection and must resolve to a governed profile id before they reach platform filters, source IDs, or top-level resource identity. Frontend resource identity presenters may append a runtime version to a displayed system badge only when that version is sourced from the same canonical -platform or host-profile identity, such as PVE `pveVersion`, PBS `version`, or -an agent OS report that resolves to Unraid or Proxmox VE. They must not attach -a collector OS version to a different API-backed platform identity. +platform or host-profile identity, such as PVE `ResourceProxmoxMeta.pveVersion` +or `platformData.proxmox.pveVersion`, PBS `version`, or an agent OS report that +resolves to Unraid or Proxmox VE. They must not attach a collector OS version to +a different API-backed platform identity. Agent-backed storage resources follow the same distinction: `StorageMeta.platform` may carry appliance presentation context such as `unraid` so the operator can see what system owns the array, but realtime `platformType` and source filters must @@ -1958,9 +1959,9 @@ monitoring and API export paths can derive `models.Storage` from unified views without depending on legacy snapshot ownership. Canonical Proxmox node metadata now carries node-only boundary fields such as -guest URL, connection health, temperature details, and pending-update metadata -so monitoring can derive `models.Node` from unified views without depending on -legacy snapshot ownership. +guest URL, connection health, PVE/kernel version identity, temperature details, +and pending-update metadata so monitoring can derive `models.Node` from unified +views without depending on legacy snapshot ownership. Canonical host-agent metadata now carries host-only runtime fields such as CPU count, load average, machine/report identity, command capability, exclude diff --git a/frontend-modern/src/components/shared/NodeGroupHeader.tsx b/frontend-modern/src/components/shared/NodeGroupHeader.tsx index 2bb749c7e..e0bf1a8cf 100644 --- a/frontend-modern/src/components/shared/NodeGroupHeader.tsx +++ b/frontend-modern/src/components/shared/NodeGroupHeader.tsx @@ -5,6 +5,7 @@ import { StatusDot } from '@/components/shared/StatusDot'; import { getNodeStatusIndicator } from '@/utils/status'; import { formatUptime } from '@/utils/format'; import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature'; +import { formatProxmoxVersion } from '@/utils/proxmoxVersion'; import { GROUPED_TABLE_ROW_BADGE_CLASS, getGroupedTableRowCellClass, @@ -31,15 +32,7 @@ export const NodeGroupHeader: Component = (props) => { const nodeUrl = () => props.node.guestURL || props.node.host || `https://${props.node.name}:8006`; const displayName = () => getNodeDisplayName(props.node); const showActualName = () => hasAlternateDisplayName(props.node); - const pveVersion = () => { - const version = (props.node.pveVersion || '').trim(); - if (!version || version.toLowerCase() === 'unknown') return ''; - return ( - version.match(/pve-manager\/([^/\s]+)/i)?.[1] || - version.match(/\d+(?:\.\d+)+/)?.[0] || - version - ); - }; + const pveVersion = () => formatProxmoxVersion(props.node.pveVersion); const cpuTemperature = () => getCpuTemperature(props.node.temperature); const hasNodeFacts = () => Boolean(pveVersion()) || diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 5ec5d7602..58eeb7cfc 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -136,6 +136,7 @@ import swarmServicesDrawerSource from '@/components/Docker/SwarmServicesDrawer.t import k8sDeploymentsDrawerSource from '@/components/Kubernetes/K8sDeploymentsDrawer.tsx?raw'; import k8sNamespacesDrawerSource from '@/components/Kubernetes/K8sNamespacesDrawer.tsx?raw'; import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw'; +import proxmoxVersionSource from '@/utils/proxmoxVersion.ts?raw'; import storageGroupRowSource from '@/components/Storage/StorageGroupRow.tsx?raw'; import storageGroupPresentationSource from '@/features/storageBackups/groupPresentation.ts?raw'; import storagePoolRowSource from '@/components/Storage/StoragePoolRow.tsx?raw'; @@ -547,6 +548,16 @@ describe('shared primitive guardrails', () => { expect(unifiedResourceHostTableCardSource).toContain('data-summary-group-member-active'); }); + it('routes Proxmox node version presentation through the shared formatter', () => { + expect(nodeGroupHeaderSource).toContain("from '@/utils/proxmoxVersion'"); + expect(nodeGroupHeaderSource).toContain('formatProxmoxVersion(props.node.pveVersion)'); + expect(nodeGroupHeaderSource).not.toContain('pve-manager\\/'); + + expect(proxmoxVersionSource).toContain('formatProxmoxVersion'); + expect(proxmoxVersionSource).toContain('pve-manager\\/'); + expect(proxmoxVersionSource).toContain('unknown'); + }); + it('keeps product table scroll frames on the shared table shell', () => { expect(tableSource).toContain('wrapperClass'); expect(tableSource).toContain('w-full overflow-x-auto touch-scroll'); diff --git a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts index 92f55e8db..0e6cad098 100644 --- a/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts @@ -3,6 +3,7 @@ import type { Resource } from '@/types/resource'; import { PROXMOX_TAB_SPECS, buildProxmoxPageModel, + getResourceVersion, resolveProxmoxPlatformScope, } from '../proxmoxPageModel'; @@ -133,4 +134,28 @@ describe('proxmoxPageModel', () => { ), ).toBe('proxmox-pve'); }); + + it('surfaces compact PVE versions from canonical Proxmox resource metadata', () => { + expect( + getResourceVersion( + makeResource({ + id: 'pve-node', + type: 'agent', + proxmox: { pveVersion: 'pve-manager/9.1.9/ee7bad0a3d1546c9' }, + }), + ), + ).toBe('9.1.9'); + + expect( + getResourceVersion( + makeResource({ + id: 'pve-node-platform-data', + type: 'agent', + platformData: { + proxmox: { pveVersion: 'pve-manager/8.3.3/bbba1c53a1a65b24' }, + }, + }), + ), + ).toBe('8.3.3'); + }); }); diff --git a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts index b4dc66497..340f70619 100644 --- a/frontend-modern/src/features/proxmox/proxmoxPageModel.ts +++ b/frontend-modern/src/features/proxmox/proxmoxPageModel.ts @@ -1,5 +1,6 @@ import type { Resource, ResourceMetric, ResourceType } from '@/types/resource'; import type { ResourceChange } from '@/types/resource'; +import { formatProxmoxVersion } from '@/utils/proxmoxVersion'; export type ProxmoxPageTabId = 'overview' | 'storage' | 'replication' | 'backups' | 'ceph' | 'mail'; @@ -228,13 +229,20 @@ function buildReplicationChanges(resources: Resource[]): ProxmoxReplicationChang } export function getResourceVersion(resource: Resource): string { + const pveVersion = formatProxmoxVersion(resource.proxmox?.pveVersion); + if (pveVersion) return pveVersion; + const platformProxmox = getPlatformData(resource).proxmox; + if (isRecord(platformProxmox) && typeof platformProxmox.pveVersion === 'string') { + const version = formatProxmoxVersion(platformProxmox.pveVersion); + if (version) return version; + } if (resource.pbs?.version) return resource.pbs.version; const platformPbs = getPlatformData(resource).pbs; if (isRecord(platformPbs) && typeof platformPbs.version === 'string') return platformPbs.version; const platformPmg = getPlatformData(resource).pmg; if (isRecord(platformPmg) && typeof platformPmg.version === 'string') return platformPmg.version; if (resource.agent?.osName?.toLowerCase().includes('proxmox') && resource.agent.osVersion) { - return resource.agent.osVersion; + return formatProxmoxVersion(resource.agent.osVersion) || resource.agent.osVersion; } return ''; } diff --git a/frontend-modern/src/types/__tests__/resource.test.ts b/frontend-modern/src/types/__tests__/resource.test.ts index 32b4156b9..b58dfaa40 100644 --- a/frontend-modern/src/types/__tests__/resource.test.ts +++ b/frontend-modern/src/types/__tests__/resource.test.ts @@ -182,9 +182,13 @@ describe('Resource Helper Functions', () => { nodeName: 'pve-a', instance: 'cluster-a', pool: 'prod-vms', + pveVersion: 'pve-manager/9.1.9/ee7bad0a3d1546c9', + kernelVersion: 'Linux 6.8.12-10-pve', }; expect(proxmox.pool).toBe('prod-vms'); + expect(proxmox.pveVersion).toBe('pve-manager/9.1.9/ee7bad0a3d1546c9'); + expect(proxmox.kernelVersion).toBe('Linux 6.8.12-10-pve'); }); }); diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts index 0de329360..803e6e3f8 100644 --- a/frontend-modern/src/types/resource.ts +++ b/frontend-modern/src/types/resource.ts @@ -522,6 +522,8 @@ export interface ResourceProxmoxMeta { balloon?: number; isOci?: boolean; osTemplate?: string; + pveVersion?: string; + kernelVersion?: string; } // Docker Swarm service projection emitted by the canonical adapter for diff --git a/frontend-modern/src/utils/proxmoxVersion.ts b/frontend-modern/src/utils/proxmoxVersion.ts new file mode 100644 index 000000000..a01c7b9ed --- /dev/null +++ b/frontend-modern/src/utils/proxmoxVersion.ts @@ -0,0 +1,10 @@ +export const formatProxmoxVersion = (rawVersion: string | null | undefined): string => { + const version = (rawVersion ?? '').trim(); + if (!version || version.toLowerCase() === 'unknown') return ''; + + return ( + version.match(/pve-manager\/([^/\s]+)/i)?.[1] || + version.match(/\d+(?:\.\d+)+(?:[-+][\w.-]+)?/)?.[0] || + version + ); +};