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)); }