mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
Name sensor setup notice nodes the way the nodes table does
The outdated sensor setup notice named each node by its Proxmox node
name ("pve3") while the nodes table below it, the outdated agent notice
beside it and Infrastructure settings label the same node by its
display name ("West Production C"). A user with display names set could
not tell which row the notice meant.
The notice now leads with the label the nodes table shows for the row
(the resource name, else the id) and adds the Proxmox node name in
parentheses: "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 node, so the exact native name is
what the user needs to find the host.
The node name is left out only when the label already contains it as a
whole word, ignoring ASCII case, or as a domain's host part ("PVE1",
"pve3.lan"). This is deliberately stricter than hasAlternateDisplayName
in the guest table's group header, which normalises punctuation and would
show "PVE3" alone for node "pve-3", hiding the host to act on.
The notice's own test file was never registered as proof for the
frontend-primitives policy that covers the notice, unlike the sibling
agent notice's test, so it is added to that policy's exact_files.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
+13
-2
@@ -29,12 +29,23 @@ describe('PlatformOutdatedSensorSetupNotice', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('names a renamed node by its table label and its Proxmox node', () => {
|
||||
render(() => (
|
||||
<PlatformOutdatedSensorSetupNotice
|
||||
nodes={[{ id: 'node-3', name: 'West Production C', nodeName: 'pve3' }]}
|
||||
/>
|
||||
));
|
||||
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(() => (
|
||||
<PlatformOutdatedSensorSetupNotice
|
||||
nodes={[
|
||||
{ id: 'node-1', name: 'pve1' },
|
||||
{ id: 'node-2', name: 'pve2' },
|
||||
{ id: 'node-3', name: 'West Production C', nodeName: 'pve3' },
|
||||
]}
|
||||
/>
|
||||
));
|
||||
@@ -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).');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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()}.`;
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user