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.
This commit is contained in:
rcourtman
2026-05-17 13:59:37 +01:00
parent b746838b97
commit dcd59bf11b
9 changed files with 78 additions and 16 deletions
@@ -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.
@@ -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
@@ -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<NodeGroupHeaderProps> = (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()) ||
@@ -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');
@@ -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');
});
});
@@ -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 '';
}
@@ -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');
});
});
+2
View File
@@ -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
@@ -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
);
};