From c48dfa826176282aadb6e2afd827823ff8957e99 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 13 Jun 2026 20:50:06 +0100 Subject: [PATCH] Converge standalone metrics on platform table helper --- .../subsystems/frontend-primitives.md | 7 ++- .../scripts/shared-template-registry.json | 39 +++++++++++++ .../SharedPrimitives.guardrails.test.ts | 56 +++++++++++++++++++ .../standalone/agentMachineTableModel.ts | 49 ++++++++-------- 4 files changed, 124 insertions(+), 27 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e8a2b84a5..ca47f2a5a 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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 diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index ab3938578..734db0f3c 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -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", diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 18214416a..01ef20c14 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -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<{ diff --git a/frontend-modern/src/features/standalone/agentMachineTableModel.ts b/frontend-modern/src/features/standalone/agentMachineTableModel.ts index 76f1cb6ff..adea29e52 100644 --- a/frontend-modern/src/features/standalone/agentMachineTableModel.ts +++ b/frontend-modern/src/features/standalone/agentMachineTableModel.ts @@ -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([ '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((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;