diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 0d45e31828..7c19ed515a 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -6664,6 +6664,16 @@ notice renders on. `collectOutdatedSensorSetupNodes` lists the PVE nodes that
carry the registry's `proxmox.sensorSetupOutdated` verdict and takes no disk
rows, because only the Proxmox Storage tab loads them; a predicate over them
left the notice silent on Overview.
+A notice names each affected resource by the label its page's table shows for
+the row. The sensor-setup notice leads with the node's trimmed resource name
+(the operator-set display name when one exists, else the Proxmox node name),
+falling back to the id as the nodes table does, and adds the Proxmox node name
+in parentheses unless the label already contains it as a whole word, ignoring
+ASCII case, or as the host part of a domain name, as in
+`West Production C (pve3)`. Both names are needed: the nodes table switches to
+the Proxmox node name in its narrowest layout, and the setup script is re-run
+on that Proxmox node, so the rule is stricter than `hasAlternateDisplayName` in
+the guest table's node group header and keeps `pve-3` beside a `PVE3` label.
Alert incident-event filter containers, labels, and chips must now route
through the shared presentation helpers in
diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json
index 95466d4ebd..78a73168b9 100644
--- a/docs/release-control/v6/internal/subsystems/registry.json
+++ b/docs/release-control/v6/internal/subsystems/registry.json
@@ -5836,6 +5836,7 @@
"frontend-modern/src/features/platformPage/__tests__/platformEstateOverviewModel.test.ts",
"frontend-modern/src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
"frontend-modern/src/features/platformPage/PlatformOutdatedAgentNotice.test.tsx",
+ "frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx",
"frontend-modern/src/features/platformPage/platformSearchSuggestions.test.ts"
]
},
diff --git a/frontend-modern/browser-verification/07000f92ba692351.json b/frontend-modern/browser-verification/07000f92ba692351.json
new file mode 100644
index 0000000000..5a82e7bbd2
--- /dev/null
+++ b/frontend-modern/browser-verification/07000f92ba692351.json
@@ -0,0 +1,50 @@
+{
+ "version": 1,
+ "base_sha": "c5416b6800add376e674ab64292df3e4d8e88f7f",
+ "verified_at": "2026-10-07T22:31:44Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx",
+ "frontend-modern/src/features/platformPage/sensorSetup.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx": "404d9a23881683e94ec74400120a33c432ffa4b8f24238be3d127a6a7e243abc",
+ "frontend-modern/src/features/platformPage/sensorSetup.ts": "153a28a603520156129738bd09d3d5dedab03ab8f5de2075edeb57b777aed778"
+ },
+ "routes": [
+ "/proxmox",
+ "/proxmox/storage"
+ ],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 900
+ },
+ {
+ "width": 768,
+ "height": 1024
+ },
+ {
+ "width": 414,
+ "height": 896
+ },
+ {
+ "width": 375,
+ "height": 812
+ }
+ ],
+ "states": [
+ "Private mock stack (8-node estate; every node carries an operator-style display name such as West Production C for pve3) with a local-only generator overlay: pve1, pve3 and standalone2 report the legacy sensors -j payload with a SATA disk lacking a current temperature, so the registry sets sensorSetupOutdated on those three nodes. Frontend served by Vite from this tree; backend binary built from dace9295c6 with the overlay (main's later commits do not touch this data path).",
+ "Three affected nodes: 'Affected: Automation Lab Standalone (standalone2), West Production A (pve1), West Production C (pve3).', sorted by table label, on Overview and Storage at all four viewports, with no notice or page overflow and no console errors; the action link targets /settings/infrastructure.",
+ "The nodes table container measures 1320, 695 and 385px at 1440, 768 and 414 wide and shows display names; at 375 wide it measures 346px (its narrow layout) and labels rows standalone2, standalone1, pve1. The notice carries both names, so each layout can match it to a row.",
+ "Single affected node (pve3 only, earlier revision of this change with the same label output): 'West Production C (pve3) is using an older temperature monitoring setup ...'.",
+ "Before control on the same stack with origin/main's sensorSetup.ts and PlatformOutdatedSensorSetupNotice.tsx swapped in: 'Affected: pve1, pve3, standalone2.' above rows labelled Automation Lab Standalone, West Production A and West Production C.",
+ "Labels that already contain the node name (pve2.lan, 'Rack 4 (pve3)', quoted, case-only) and labels that only resemble it (PVE3 for pve-3, pve5-backup, pve10 for pve1, a Kelvin-sign lookalike) are covered by sensorSetup.test.ts only; every mock node carries a distinct display name. The last edit after the pass was a code comment.",
+ "Mock data only; not a real pre-rc.6 SSH node or a real operator-renamed cluster."
+ ],
+ "interactions": [
+ "Loaded /proxmox and /proxmox/storage by URL in headless Chromium at each viewport and read the notice text, node row labels, table container width and overflow from the DOM; screenshots kept locally.",
+ "Restarted the backend with two more legacy nodes to move from the single-node sentence to the multi-node list.",
+ "Swapped origin/main's two source files in and out through Vite HMR for the before control, then restored this change byte-for-byte and re-read the notice."
+ ]
+}
diff --git a/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx b/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx
index 1fe4c929df..e6447986c7 100644
--- a/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx
+++ b/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.test.tsx
@@ -29,12 +29,23 @@ describe('PlatformOutdatedSensorSetupNotice', () => {
);
});
+ it('names a renamed node by its table label and its Proxmox node', () => {
+ render(() => (
+
+ ));
+ expect(screen.getByTestId('platform-outdated-sensor-setup-notice')).toHaveTextContent(
+ 'West Production C (pve3) is using an older temperature monitoring setup that cannot read SATA/SAS disk temperatures.',
+ );
+ });
+
it('summarises multiple affected nodes and lists them', () => {
render(() => (
));
@@ -42,6 +53,6 @@ describe('PlatformOutdatedSensorSetupNotice', () => {
expect(notice).toHaveTextContent(
'2 nodes are using an older temperature monitoring setup that cannot read SATA/SAS disk temperatures.',
);
- expect(notice).toHaveTextContent('Affected: pve1, pve2.');
+ expect(notice).toHaveTextContent('Affected: pve1, West Production C (pve3).');
});
});
diff --git a/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx b/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx
index aada988edd..0f50af8a0d 100644
--- a/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx
+++ b/frontend-modern/src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx
@@ -10,6 +10,12 @@ type PlatformOutdatedSensorSetupNoticeProps = {
actionLabel?: string;
};
+// Leads with the label the nodes table shows so the user can match the row,
+// then names the Proxmox node the setup script is re-run on when that label
+// does not already contain it.
+const formatNodeLabel = (node: OutdatedSensorSetupNode): string =>
+ node.nodeName ? `${node.name} (${node.nodeName})` : node.name;
+
// Inline notice shown on the Proxmox page when a node's SSH temperature
// monitoring was set up by a pre-v6.0.0-rc.6 setup script. That setup locks
// the SSH key to `sensors -j`, so SATA/SAS disk temperatures silently never
@@ -18,12 +24,12 @@ type PlatformOutdatedSensorSetupNoticeProps = {
// clean in the healthy case. Sibling of PlatformOutdatedAgentNotice.
export function PlatformOutdatedSensorSetupNotice(props: PlatformOutdatedSensorSetupNoticeProps) {
const count = createMemo(() => props.nodes.length);
- const names = createMemo(() => props.nodes.map((node) => node.name).join(', '));
+ const names = createMemo(() => props.nodes.map(formatNodeLabel).join(', '));
const actionLabel = createMemo(() => props.actionLabel || 'Open Infrastructure settings');
const message = createMemo(() => {
if (count() === 1) {
- return `${props.nodes[0].name} is using an older temperature monitoring setup that cannot read SATA/SAS disk temperatures. Re-run the node setup script to upgrade it.`;
+ return `${formatNodeLabel(props.nodes[0])} is using an older temperature monitoring setup that cannot read SATA/SAS disk temperatures. Re-run the node setup script to upgrade it.`;
}
return `${count()} nodes are using an older temperature monitoring setup that cannot read SATA/SAS disk temperatures. Re-run the node setup script on each to upgrade them. Affected: ${names()}.`;
});
diff --git a/frontend-modern/src/features/platformPage/sensorSetup.test.ts b/frontend-modern/src/features/platformPage/sensorSetup.test.ts
index 949c649186..2480648e4c 100644
--- a/frontend-modern/src/features/platformPage/sensorSetup.test.ts
+++ b/frontend-modern/src/features/platformPage/sensorSetup.test.ts
@@ -34,24 +34,108 @@ describe('collectOutdatedSensorSetupNodes', () => {
expect(collectOutdatedSensorSetupNodes([reading])).toEqual([]);
});
- it('names a node by its Proxmox node, then node name, then resource name, trimmed', () => {
- const nodes = [
- node({ id: 'by-node', proxmox: { node: ' alpha ', sensorSetupOutdated: true } }),
- node({ id: 'by-node-name', proxmox: { nodeName: 'bravo', sensorSetupOutdated: true } }),
- node({ id: 'by-name', name: 'charlie', proxmox: { sensorSetupOutdated: true } }),
- ];
- expect(collectOutdatedSensorSetupNodes(nodes)).toEqual([
- { id: 'by-node', name: 'alpha' },
- { id: 'by-node-name', name: 'bravo' },
- { id: 'by-name', name: 'charlie' },
+ it('names a node by its table label and adds the Proxmox node name when they differ', () => {
+ // The nodes table labels the row by the operator-set display name, while
+ // the setup script is re-run on the Proxmox node itself.
+ const renamed = node({
+ id: 'node-3',
+ name: 'West Production C',
+ proxmox: { node: 'pve3', nodeName: 'pve3', sensorSetupOutdated: true },
+ });
+ expect(collectOutdatedSensorSetupNodes([renamed])).toEqual([
+ { id: 'node-3', name: 'West Production C', nodeName: 'pve3' },
]);
});
- it('sorts multiple affected nodes by name', () => {
+ it('omits the Proxmox node name when the table label contains it as a word', () => {
+ const flagged = (id: string, name: string, nodeName?: string) =>
+ node({ id, name, proxmox: { nodeName, sensorSetupOutdated: true } });
+ expect(
+ collectOutdatedSensorSetupNodes([
+ flagged('same', ' PVE1 ', 'pve1'),
+ flagged('fqdn', 'pve2.lan', 'pve2'),
+ flagged('embedded', 'Rack 4 (pve3)', 'pve3'),
+ flagged('punctuated', '"pve5": primary', 'pve5'),
+ flagged('unknown', 'pve4'),
+ ]),
+ ).toEqual([
+ { id: 'punctuated', name: '"pve5": primary' },
+ { id: 'same', name: 'PVE1' },
+ { id: 'fqdn', name: 'pve2.lan' },
+ { id: 'unknown', name: 'pve4' },
+ { id: 'embedded', name: 'Rack 4 (pve3)' },
+ ]);
+ });
+
+ it('keeps the exact Proxmox node name when the label only resembles it', () => {
+ // The setup script is re-run on this host, so a label that differs inside
+ // the name (punctuation, spacing, a suffix or a non-ASCII lookalike
+ // letter) must not stand in for it.
+ const flagged = (id: string, name: string, nodeName: string) =>
+ node({ id, name, proxmox: { nodeName, sensorSetupOutdated: true } });
+ expect(
+ collectOutdatedSensorSetupNodes([
+ flagged('punctuation', 'PVE3', 'pve-3'),
+ flagged('spacing', 'PVE 4', 'pve-4'),
+ flagged('suffix', 'pve5-backup', 'pve5'),
+ flagged('prefix', 'pve10', 'pve1'),
+ flagged('lookalike', 'PVE-\u212A', 'pve-k'),
+ ]),
+ ).toEqual([
+ { id: 'spacing', name: 'PVE 4', nodeName: 'pve-4' },
+ { id: 'lookalike', name: 'PVE-\u212A', nodeName: 'pve-k' },
+ { id: 'prefix', name: 'pve10', nodeName: 'pve1' },
+ { id: 'punctuation', name: 'PVE3', nodeName: 'pve-3' },
+ { id: 'suffix', name: 'pve5-backup', nodeName: 'pve5' },
+ ]);
+ });
+
+ it('labels a node without a resource name by its id, as the table does', () => {
const nodes = [
- flaggedNode({ id: 'node-b', proxmox: { node: 'pve-b', sensorSetupOutdated: true } }),
- flaggedNode({ id: 'node-a', proxmox: { node: 'pve-a', sensorSetupOutdated: true } }),
+ node({ id: 'by-id', name: ' ', proxmox: { node: ' alpha ', sensorSetupOutdated: true } }),
+ node({ id: 'bare', name: '', proxmox: { sensorSetupOutdated: true } }),
];
- expect(collectOutdatedSensorSetupNodes(nodes).map((n) => n.name)).toEqual(['pve-a', 'pve-b']);
+ expect(collectOutdatedSensorSetupNodes(nodes)).toEqual([
+ { id: 'bare', name: 'bare' },
+ { id: 'by-id', name: 'by-id', nodeName: 'alpha' },
+ ]);
+ });
+
+ it('reads the Proxmox node name from nodeName before node', () => {
+ const nodes = [
+ node({
+ id: 'both',
+ name: 'West Production C',
+ proxmox: { node: 'stale', nodeName: ' pve3 ', sensorSetupOutdated: true },
+ }),
+ node({
+ id: 'blank',
+ name: 'West Production D',
+ proxmox: { node: 'pve4', nodeName: ' ', sensorSetupOutdated: true },
+ }),
+ ];
+ expect(collectOutdatedSensorSetupNodes(nodes)).toEqual([
+ { id: 'both', name: 'West Production C', nodeName: 'pve3' },
+ { id: 'blank', name: 'West Production D', nodeName: 'pve4' },
+ ]);
+ });
+
+ it('sorts multiple affected nodes by their table label', () => {
+ const nodes = [
+ flaggedNode({
+ id: 'node-a',
+ name: 'West Production C',
+ proxmox: { nodeName: 'pve-a', sensorSetupOutdated: true },
+ }),
+ flaggedNode({
+ id: 'node-b',
+ name: 'East Backup',
+ proxmox: { nodeName: 'pve-b', sensorSetupOutdated: true },
+ }),
+ ];
+ expect(collectOutdatedSensorSetupNodes(nodes).map((n) => n.name)).toEqual([
+ 'East Backup',
+ 'West Production C',
+ ]);
});
});
diff --git a/frontend-modern/src/features/platformPage/sensorSetup.ts b/frontend-modern/src/features/platformPage/sensorSetup.ts
index 2e27d70d5a..31f0dc9964 100644
--- a/frontend-modern/src/features/platformPage/sensorSetup.ts
+++ b/frontend-modern/src/features/platformPage/sensorSetup.ts
@@ -2,11 +2,50 @@ import type { Resource } from '@/types/resource';
export type OutdatedSensorSetupNode = {
id: string;
+ // The label the Proxmox nodes table shows for the node's row outside its
+ // narrowest layout, which shows the Proxmox node name instead.
name: string;
+ // The Proxmox node name, which the setup script is re-run on, when `name`
+ // does not already contain it as a whole word or a domain's host part.
+ nodeName?: string;
};
-const getNodeName = (node: Resource): string =>
- (node.proxmox?.node || node.proxmox?.nodeName || node.name || '').trim();
+const trimmed = (value: string | undefined): string => (value ?? '').trim();
+
+const getProxmoxNodeName = (node: Resource): string =>
+ trimmed(node.proxmox?.nodeName) || trimmed(node.proxmox?.node);
+
+// Mirrors the nodes table's row label: the resource name, which carries the
+// operator-set display name when one exists and the Proxmox node name
+// otherwise, falling back to the id.
+const getNodeLabel = (node: Resource): string => trimmed(node.name) || node.id;
+
+// Folds only ASCII letters, the alphabet of Proxmox node names, so a
+// non-ASCII lookalike (the Kelvin sign for K) cannot pass for one.
+const foldAscii = (value: string): string => value.replace(/[A-Z]/g, (c) => c.toLowerCase());
+
+// True when the label already contains the node name as a whole word, ignoring
+// ASCII case (`PVE1`, `Rack 4 (pve3)`), or as the host part of a domain name
+// (`pve3.lan`). Words are runs of ASCII letters, digits, dots, hyphens and
+// underscores, so a label that differs inside the name (`PVE3` or `PVE 3` for
+// `pve-3`, `pve3-backup` for `pve3`) keeps the exact name. Stricter than the
+// guest table's decorative node group header (`hasAlternateDisplayName`),
+// because the notice names the host the user must act on.
+const labelShowsNodeName = (label: string, nodeName: string): boolean => {
+ const target = foldAscii(nodeName);
+ return foldAscii(label)
+ .split(/[^a-z0-9._-]+/)
+ .some((word) => word === target || word.startsWith(`${target}.`));
+};
+
+const toOutdatedSensorSetupNode = (node: Resource): OutdatedSensorSetupNode => {
+ const name = getNodeLabel(node);
+ const nodeName = getProxmoxNodeName(node);
+ if (!nodeName || labelShowsNodeName(name, nodeName)) {
+ return { id: node.id, name };
+ }
+ return { id: node.id, name, nodeName };
+};
// Lists PVE nodes whose SSH temperature monitoring still runs the pre-rc.6
// setup (authorized_keys locked to `sensors -j`). Such a payload parses fine
@@ -18,6 +57,6 @@ const getNodeName = (node: Resource): string =>
export function collectOutdatedSensorSetupNodes(nodes: Resource[]): OutdatedSensorSetupNode[] {
return nodes
.filter((node) => node.proxmox?.sensorSetupOutdated === true)
- .map((node) => ({ id: node.id, name: getNodeName(node) }))
+ .map(toOutdatedSensorSetupNode)
.sort((a, b) => a.name.localeCompare(b.name));
}