Converge standalone metrics on platform table helper

This commit is contained in:
rcourtman
2026-06-13 20:50:06 +01:00
parent 6c2a085bb8
commit c48dfa8261
4 changed files with 124 additions and 27 deletions
@@ -2801,9 +2801,10 @@ Platform table metric fallback rendering is also shared.
metric bar cells plus optional caller-owned fallback label/title text, and
`getPlatformTableFiniteMetric` owns finite-number normalization for CPU and
memory, disk, and capacity values. Docker / Podman, Kubernetes, Proxmox,
Standalone, TrueNAS, and vSphere platform tables must compose those helpers
instead of declaring local `metricFallback` / `finiteMetric` helpers or
inlining centered muted dash fallback markup in metric cells.
Standalone, TrueNAS, and vSphere platform tables and their table-model helpers
must compose those helpers instead of declaring local `metricFallback` /
`finiteMetric` helpers or inlining centered muted dash fallback markup in
metric cells.
Platform load-failure states are registry-backed as well.
`PlatformErrorState` owns the repeated table-card error shell, warning icon,
and Refresh action for platform page and table load failures; platform
@@ -2613,6 +2613,26 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-finite-metric-normalization",
"category": "platform-table-metric",
"summary": "Platform table models must use the shared finite-metric normalization helper instead of carrying model-local Number.isFinite wrappers.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "getPlatformTableFiniteMetric"
},
"requiredConsumers": [{ "path": "src/features/standalone/agentMachineTableModel.ts" }],
"forbiddenPatterns": [
{
"path": "src/features/standalone/agentMachineTableModel.ts",
"patterns": ["const finiteMetric"]
}
],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-error-state",
"category": "platform-error-state",
@@ -3983,6 +4003,25 @@
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-table-model-local-finite-metric-helper",
"category": "platform-table-metric",
"summary": "Platform table models must not recreate local finiteMetric helpers for metric normalization.",
"canonical": {
"path": "src/features/platformPage/sharedPlatformPage.tsx",
"export": "getPlatformTableFiniteMetric"
},
"scopes": ["src/features/standalone/agentMachineTableModel.ts"],
"extensions": [".ts"],
"allPatterns": ["const finiteMetric"],
"legacyReason": "Retired migration debt. Platform table model-side metric normalization must use getPlatformTableFiniteMetric so table presentation validity remains shared.",
"allowedPaths": [],
"ignoredPaths": [],
"proof": [
"src/components/shared/SharedPrimitives.guardrails.test.ts",
"scripts/shared-template-audit.mjs"
]
},
{
"id": "platform-section-tabs-local-nav",
"category": "platform-section-tabs",
@@ -6097,6 +6097,62 @@ describe('shared primitive guardrails', () => {
}
});
it('keeps platform table model metric normalization on the shared helper', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
id: string;
canonical?: { path?: string; export?: string };
requiredConsumers?: Array<{ path?: string }>;
forbiddenPatterns?: Array<{ path?: string; patterns?: string[] }>;
}>;
patternGuards?: Array<{
id: string;
canonical?: { path?: string; export?: string };
allPatterns?: string[];
scopes?: string[];
extensions?: string[];
allowedPaths?: string[];
ignoredPaths?: string[];
}>;
};
const registeredRule = registry.rules?.find(
(rule) => rule.id === 'platform-table-finite-metric-normalization',
);
const localHelperGuard = registry.patternGuards?.find(
(guard) => guard.id === 'platform-table-model-local-finite-metric-helper',
);
expect(registeredRule?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(registeredRule?.canonical?.export).toBe('getPlatformTableFiniteMetric');
expect(registeredRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([
'src/features/standalone/agentMachineTableModel.ts',
]);
expect(registeredRule?.forbiddenPatterns).toEqual([
{
path: 'src/features/standalone/agentMachineTableModel.ts',
patterns: ['const finiteMetric'],
},
]);
expect(localHelperGuard?.canonical?.path).toBe(
'src/features/platformPage/sharedPlatformPage.tsx',
);
expect(localHelperGuard?.canonical?.export).toBe('getPlatformTableFiniteMetric');
expect(localHelperGuard?.allPatterns).toEqual(['const finiteMetric']);
expect(localHelperGuard?.scopes).toEqual(['src/features/standalone/agentMachineTableModel.ts']);
expect(localHelperGuard?.extensions).toEqual(['.ts']);
expect(localHelperGuard?.allowedPaths ?? []).toHaveLength(0);
expect(localHelperGuard?.ignoredPaths ?? []).toHaveLength(0);
expect(sharedPlatformPageSource).toContain('export const getPlatformTableFiniteMetric');
expect(agentMachineTableModelSource).toContain('getPlatformTableFiniteMetric');
expect(agentMachineTableModelSource).not.toContain('const finiteMetric');
expect(agentMachineTableModelSource).not.toContain(
"typeof value === 'number' && Number.isFinite(value) ? value : undefined",
);
});
it('keeps platform section navigation on the shared tabs template', () => {
const registry = JSON.parse(sharedTemplateRegistrySource) as {
rules?: Array<{
@@ -1,6 +1,7 @@
import type { ColumnDef } from '@/hooks/useColumnVisibility';
import type { HostDiskIO, HostRAIDArray, HostRAIDDevice } from '@/types/api';
import type { Resource } from '@/types/resource';
import { getPlatformTableFiniteMetric } from '@/features/platformPage/sharedPlatformPage';
import { normalizeDiskArray } from '@/utils/format';
import { asTrimmedString } from '@/utils/stringUtils';
@@ -114,9 +115,6 @@ const AGENT_MACHINE_SORT_DESC_DEFAULTS = new Set<AgentMachineSortKey>([
'lastSeen',
]);
const finiteMetric = (value: number | undefined): number | undefined =>
typeof value === 'number' && Number.isFinite(value) ? value : undefined;
type TemperatureReading = {
label: string;
value: number;
@@ -151,7 +149,7 @@ export type AgentMachineDiskIODetail = HostDiskIO;
export type AgentMachineRaidArrayDetail = HostRAIDArray;
const positiveTemperature = (value: number | undefined): number | undefined => {
const metric = finiteMetric(value);
const metric = getPlatformTableFiniteMetric(value);
return metric !== undefined && metric > 0 ? metric : undefined;
};
@@ -163,13 +161,13 @@ const getDiskUsagePercent = (disk: {
used?: number;
usage?: number;
}): number | undefined => {
const total = finiteMetric(disk.total);
const used = finiteMetric(disk.used);
const total = getPlatformTableFiniteMetric(disk.total);
const used = getPlatformTableFiniteMetric(disk.used);
if (total && total > 0 && typeof used === 'number') {
return (used / total) * 100;
}
const usage = finiteMetric(disk.usage);
const usage = getPlatformTableFiniteMetric(disk.usage);
if (usage === undefined) return undefined;
return usage <= 1 ? usage * 100 : usage;
};
@@ -267,51 +265,54 @@ const flattenTemperatureSections = (
.join('\n');
const getMetricPercent = (metric: Resource['cpu'] | undefined): number | undefined =>
finiteMetric(metric?.current);
getPlatformTableFiniteMetric(metric?.current);
export const getAgentMachineCpuPercent = (machine: Resource): number | undefined =>
getMetricPercent(machine.cpu);
export const getAgentMachineMemoryPercent = (machine: Resource): number | undefined => {
const total = finiteMetric(machine.memory?.total);
const used = finiteMetric(machine.memory?.used);
const total = getPlatformTableFiniteMetric(machine.memory?.total);
const used = getPlatformTableFiniteMetric(machine.memory?.used);
if (total && total > 0 && typeof used === 'number') {
return (used / total) * 100;
}
return finiteMetric(machine.memory?.current) ?? finiteMetric(machine.agent?.memory?.usage);
return (
getPlatformTableFiniteMetric(machine.memory?.current) ??
getPlatformTableFiniteMetric(machine.agent?.memory?.usage)
);
};
export const getAgentMachineDiskPercent = (machine: Resource): number | undefined => {
const maxDiskPercent = getMaxOperationalDiskPercent(machine);
if (maxDiskPercent !== undefined) return maxDiskPercent;
const total = finiteMetric(machine.disk?.total);
const used = finiteMetric(machine.disk?.used);
const total = getPlatformTableFiniteMetric(machine.disk?.total);
const used = getPlatformTableFiniteMetric(machine.disk?.used);
if (total && total > 0 && typeof used === 'number') {
return (used / total) * 100;
}
return finiteMetric(machine.disk?.current);
return getPlatformTableFiniteMetric(machine.disk?.current);
};
export const getAgentMachineNetworkTotal = (machine: Resource): number | undefined => {
const rx = finiteMetric(machine.network?.rxBytes);
const tx = finiteMetric(machine.network?.txBytes);
const rx = getPlatformTableFiniteMetric(machine.network?.rxBytes);
const tx = getPlatformTableFiniteMetric(machine.network?.txBytes);
if (rx === undefined && tx === undefined) return undefined;
return (rx ?? 0) + (tx ?? 0);
};
const positiveMetric = (value: number | undefined): number | undefined => {
const metric = finiteMetric(value);
const metric = getPlatformTableFiniteMetric(value);
return metric !== undefined && metric > 0 ? metric : undefined;
};
const nonNegativeMetric = (value: number | undefined): number => {
const metric = finiteMetric(value);
const metric = getPlatformTableFiniteMetric(value);
return metric !== undefined && metric > 0 ? metric : 0;
};
const nonNegativeFiniteMetric = (value: number | undefined): number | undefined => {
const metric = finiteMetric(value);
const metric = getPlatformTableFiniteMetric(value);
return metric !== undefined && metric >= 0 ? metric : undefined;
};
@@ -367,8 +368,8 @@ export const getAgentMachineNetworkInterfaceDetails = (
const name = asTrimmedString(iface.name);
const mac = asTrimmedString(iface.mac);
const addresses = uniqueTrimmedValues(iface.addresses);
const rxBytes = finiteMetric(iface.rxBytes);
const txBytes = finiteMetric(iface.txBytes);
const rxBytes = getPlatformTableFiniteMetric(iface.rxBytes);
const txBytes = getPlatformTableFiniteMetric(iface.txBytes);
const speedMbps = positiveMetric(iface.speedMbps);
if (
@@ -396,8 +397,8 @@ export const getAgentMachineNetworkInterfaceDetails = (
);
export const getAgentMachineDiskIOTotal = (machine: Resource): number | undefined => {
const read = finiteMetric(machine.diskIO?.readRate);
const write = finiteMetric(machine.diskIO?.writeRate);
const read = getPlatformTableFiniteMetric(machine.diskIO?.readRate);
const write = getPlatformTableFiniteMetric(machine.diskIO?.writeRate);
if (read === undefined && write === undefined) return undefined;
return (read ?? 0) + (write ?? 0);
};
@@ -532,7 +533,7 @@ const getAgentMachineRaidDeviceDetails = (
(devices ?? []).reduce<HostRAIDDevice[]>((details, device, index) => {
const deviceName = asTrimmedString(device.device);
const state = asTrimmedString(device.state);
const slot = finiteMetric(device.slot);
const slot = getPlatformTableFiniteMetric(device.slot);
if (!deviceName && !state && slot === undefined) return details;