From 8e1a564d7e229bda51c021c7e6bde920e0ca0596 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:11:44 +0100 Subject: [PATCH] Judge TrueNAS disk temperature alerts by disk type Every disk heat judge follows the per-type disk temperature policy (NVMe 70C, SAS 65C, SATA 55C, under the agent Disk Temp default) except TrueNAS disk temperature alerts, which used a flat 55/50 TrueNAS Disks default. A TrueNAS NVMe at 60C raised a warning while the TrueNAS storage table, Physical Disks and Patrol called it fine, the false NVMe alert #1388 removed for agent disks. TrueNAS disks now resolve their trigger per disk type unless the user saved a TrueNAS-wide value, which still applies to every TrueNAS disk; a per-disk override beats both. Saved configs keep a chosen value: a stored 55/50 is dropped only from configs written before the new truenasDiskTemperatureByType marker, because the thresholds page wrote that factory value back on every save. Alerts record their disk type so a config save re-judges them per type, and a retained reading no longer raises one. The TrueNAS Disks Global Defaults cell reads "By type" while unset (Off when the agent default switches the policy off), each disk row shows its type's trigger, and Off is written as trigger 0. The shared alert table now reads its props reactively, so the Custom badge and reset control follow a config that loads after the table mounts. The TrueNAS disk drawer tones a current reading by the same per-type trigger instead of a fixed 55C. --- .../v6/internal/subsystems/alerts.md | 44 ++++- .../subsystems/frontend-primitives.md | 12 ++ .../internal/subsystems/unified-resources.md | 7 + .../26f085dd13bc21bf.json | 69 ++++++++ .../Alerts/AlertResourceTableDesktop.tsx | 38 ++-- .../Alerts/AlertResourceTableMobile.tsx | 25 ++- .../components/Alerts/ResourceTable.test.tsx | 139 +++++++++++++++ .../src/components/Alerts/ResourceTable.tsx | 23 ++- .../ThresholdsTableAgentsResourcesSection.tsx | 6 +- .../Alerts/ThresholdsTableTrueNASTab.tsx | 53 +++++- .../Alerts/alertResourceTableModel.ts | 43 +++++ ...DetailDrawerTrueNASModel.coverage2.test.ts | 35 +++- .../resourceDetailDrawerTrueNASModel.test.ts | 3 +- .../resourceDetailDrawerTrueNASModel.ts | 31 +++- .../useResourceDetailDrawerDerivedState.ts | 6 +- .../alertsConfigurationModel.snapshot.test.ts | 51 +++++- .../alerts/alertsConfigurationModel.ts | 20 ++- .../__tests__/trueNASDiskTemperature.test.ts | 44 +++++ .../hooks/useThresholdsPlatformData.ts | 14 +- .../thresholds/trueNASDiskTemperature.ts | 51 ++++++ frontend-modern/src/types/alerts.ts | 3 + .../__tests__/alertThresholdDefaults.test.ts | 3 +- .../utils/__tests__/metricThresholds.test.ts | 2 +- .../src/utils/alertThresholdDefaults.ts | 8 +- internal/alerts/alert_policy.go | 9 + internal/alerts/alerts_test.go | 59 +++++++ ...g_normalize_defaults_branchcov0716_test.go | 6 +- internal/alerts/config/normalize.go | 28 ++- .../config/normalize_truenas_disk_test.go | 58 ++++++ internal/alerts/config/types.go | 7 + internal/alerts/config_runtime.go | 12 +- internal/alerts/default_config.go | 8 +- internal/alerts/truenas_disk_temperature.go | 18 ++ internal/alerts/unified_eval.go | 18 +- internal/alerts/unified_eval_test.go | 165 ++++++++++++++++++ 35 files changed, 1051 insertions(+), 67 deletions(-) create mode 100644 frontend-modern/browser-verification/26f085dd13bc21bf.json create mode 100644 frontend-modern/src/features/alerts/thresholds/__tests__/trueNASDiskTemperature.test.ts create mode 100644 frontend-modern/src/features/alerts/thresholds/trueNASDiskTemperature.ts create mode 100644 internal/alerts/config/normalize_truenas_disk_test.go create mode 100644 internal/alerts/truenas_disk_temperature.go diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 9d7946179e..c7ab193445 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -1830,14 +1830,52 @@ A disk is hot from its trigger. Alerts and Patrol findings stay open until the reading falls to the clear value, or under the trigger when there is no band below it. The Physical Disks verdict and the TrueNAS Health cell judge only the current reading against the trigger. PDF reports colour disk temperatures by -the same thresholds. Judges that still differ: TrueNAS disk temperature alerts -use `TrueNASDiskDefaults.Temperature` (a flat 55/50), and the TrueNAS disk -drawer tones the reading from a fixed 55C. +the same thresholds. `TestDiskTemperatureThresholdMatchesCheckHostPolicy` in `internal/alerts/threshold_resolution_shared_test.go` pins per-type resolution, a raised NVMe trigger that `CheckHost` also honours, the copy, the disabled default and the nil manager. +TrueNAS disk temperature alerts follow the policy too: +`trueNASDiskTemperatureDefaultNoLock` +(`internal/alerts/truenas_disk_temperature.go`) is the type-default tier of +`effectiveAlertPolicyNoLock` for `truenas-disk`, so an unset +`TrueNASDiskDefaults.Temperature` resolves per disk type (the +`alertPolicyQuery.DiskType` the unified input carries from +`PhysicalDisk.DiskType`). A TrueNAS-wide value the user saved under TrueNAS +Disks replaces it for every TrueNAS disk, as a host Disk Temp override does for +an agent's disks, 0 meaning off, and a per-disk override beats both. The alert +records `diskType` so a config save re-judges it per type; one without it is +held to the lowest per-type trigger. A retained reading +(`diskinventory.TemperatureCollected` false) is no evidence and raises nothing. +The factory value is unset. `normalizeTrueNASDiskTemperature` drops a stored +55/50 only from a config without `TrueNASDiskTemperatureByType`, because the +thresholds page wrote that flat factory value back on every save; the page +writes the marker, so a 55 typed later is kept. Every config `GetConfig` +returns carries the marker, so a client that reads and writes back keeps it. +Without the marker (an old saved file, or a page loaded before the upgrade) +55/50 is read as that unchosen factory value; a client building a config from +scratch with an explicit TrueNAS-wide 55 has to send the marker. The TrueNAS +Disks Global Defaults cell reads `By type` while unset +(`globalDefaultFallbacks` on `ResourceTable`, +`resolveAlertResourceGlobalDefaultCell`), or Off when the agent Disk Temp +default switches the policy off, and switching it on then stages an explicit +TrueNAS-wide 55 because unset would stay off. Each disk row inherits its type's +trigger from the unsaved editor state (`resolveTrueNASDiskTemperatureDefault`). +The TrueNAS disk drawer tones a current reading from the same per-type trigger +(`getDiskTemperatureThresholds` passed into `buildTrueNASDetailSections`), so it +no longer judges heat from a fixed 55C. Judges that still differ: the TrueNAS +storage table and drawer do not apply a TrueNAS-wide value or per-disk +override. +`TestTrueNASDiskTemperatureAlertsFollowDiskTemperaturePolicy` and +`TestTrueNASDiskTemperatureAlertIgnoresRetainedReading` in +`internal/alerts/unified_eval_test.go` pin the tiers and retained readings; +`TestTrueNASDiskTemperatureAlertsReevaluatePerDiskType` and +`TestTrueNASDiskTemperatureAlertWithoutDiskTypeHeldAtLowestTrigger` in +`internal/alerts/alerts_test.go` pin re-judging on a config save; +`TestNormalizeTrueNASDiskTemperatureKeepsOnlyChosenValues` pins the +saved-config migration. + ### Agent disk temperature alerts clear when their disk leaves the report `CheckHost` also clears a disk temperature alert, and drops its pending diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 7b7e4abd89..c74b2588c0 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2116,6 +2116,18 @@ the same record; the table renders defaults once, in both desktop table and narrow card layouts, and per-section metric columns must resolve to metric keys the shared column normalizer produces so the defaults editor reads and writes the same record keys the section persists. +A global default that another setting decides while it is unset passes a +`globalDefaultFallbacks` entry for that metric (label, title, whether the other +setting is itself off, and the value switching on stages when it is). The +TrueNAS Disks temperature uses it to read `By type`, following Disk +temperature by type, with `features/alerts/thresholds/trueNASDiskTemperature.ts` +giving each disk row its type's trigger from the unsaved editor state. +`resolveAlertResourceGlobalDefaultCell` decides the cell for both layouts: +unset follows the fallback, switching on from Off returns to unset unless the +fallback is off, and an unset default with no fallback stays Off as the engine +reads it. `ResourceTable.tsx` hands its props to the shared table state through +getters, so the Custom badge and reset control follow defaults that load after +the table mounts. Platform sub-routes that add native provider inventory must stay on the shared platform page and table primitives. The vSphere Networks surface routes through `/vmware/networks`, the shared platform tab model, the command palette diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 3b9f065d08..63fd2077e1 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -6102,6 +6102,13 @@ presentation. `frontend-modern/src/components/Infrastructure/` placement, signal, and snapshot context through the canonical resource drawer and debug/source sections rather than introducing a VMware-only detail route, drawer tab, or provider-local investigation shell. +The TrueNAS physical-disk drawer judges a current temperature reading by the +disk temperature policy, like the TrueNAS storage table: +`useResourceDetailDrawerDerivedState.ts` passes the alerts store's +`getDiskTemperatureThresholds` into `buildTrueNASDetailSections`, and the +Temperature row takes a warning tone only when `isPhysicalDiskRunningHot` +says the reading reached its disk type's alert trigger. A retained reading +keeps its muted last-known row, and without a resolver heat is not judged. That same infrastructure consumer boundary also owns source selection continuity. Settings infrastructure panels and platform/runtime pages must keep canonical sources such as `truenas` and `availability` present in their diff --git a/frontend-modern/browser-verification/26f085dd13bc21bf.json b/frontend-modern/browser-verification/26f085dd13bc21bf.json new file mode 100644 index 0000000000..fa004d8f83 --- /dev/null +++ b/frontend-modern/browser-verification/26f085dd13bc21bf.json @@ -0,0 +1,69 @@ +{ + "version": 1, + "base_sha": "bdf0f76b7bf61d550b1cb8574497eef75b354acb", + "verified_at": "2026-10-07T14:11:27Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx", + "frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx", + "frontend-modern/src/components/Alerts/ResourceTable.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx", + "frontend-modern/src/components/Alerts/alertResourceTableModel.ts", + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts", + "frontend-modern/src/features/alerts/alertsConfigurationModel.ts", + "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsPlatformData.ts", + "frontend-modern/src/features/alerts/thresholds/trueNASDiskTemperature.ts", + "frontend-modern/src/types/alerts.ts", + "frontend-modern/src/utils/alertThresholdDefaults.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx": "4d09dd97adc2868889e256455c207b6b9089ea311713878f0d6ff00efb6b7123", + "frontend-modern/src/components/Alerts/AlertResourceTableMobile.tsx": "ceaf136da9361a5b1756350626a833da4dfebcb4f211c7269d7d89d37fb7e085", + "frontend-modern/src/components/Alerts/ResourceTable.tsx": "7e80a6c789b3069fcadcb86e0a6689e5319e7b0ac5d5bfa1a596520daec2d7e1", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx": "fc4e9f4d05c0bf5799acbbe64c37b901e104869ff120819342432563789ad0e9", + "frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx": "a22b86c28b65ec18ba81171fb1206a24107a0d97db19d7dd488bdfd0a689a956", + "frontend-modern/src/components/Alerts/alertResourceTableModel.ts": "5881aeaf7ee575589997bf67fb713a01784ce7ca16ecb5a5eaea77f36010837a", + "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts": "b60ba59688d6ea0cabfb2b413b8adcaa85c8d41524c1d1f0df9bdb9e6d85733c", + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "63b07de3f354f8328298348a8892ad73fde2f11ec6768e9c0f6fc37eb4c43516", + "frontend-modern/src/features/alerts/alertsConfigurationModel.ts": "0e116e65c992a72e21d700835b0cb0db7173be9ec9c4980382958506d4e75976", + "frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsPlatformData.ts": "8525b0b012198ff87e29063908a9907b7fa1080cd84187202b1cd3cf198a3b46", + "frontend-modern/src/features/alerts/thresholds/trueNASDiskTemperature.ts": "80433d42dd4658a1449c0f0c378d7fa52cd6dfab117d626195099768e750352f", + "frontend-modern/src/types/alerts.ts": "d072175ab98e3785eae643d655be4c2d54cac0eddc8b55803a2841a312062e5a", + "frontend-modern/src/utils/alertThresholdDefaults.ts": "077e291d5c224063a373a5e9f5555d9b470f585801b0cfdd9dc1acacf233af28" + }, + "routes": [ + "/alerts/thresholds/truenas", + "/truenas/storage" + ], + "viewports": [ + { + "width": 1366, + "height": 900 + }, + { + "width": 375, + "height": 812 + } + ], + "states": [ + "Private mock stack (8 nodes, mock TrueNAS truenas-main) with a local-only generator overlay pinning TrueNAS disk temperatures: nvme0n1 60C, nvme1n1 72C, sda 57C (SATA), sdc 63C (SAS); after = this branch, before = main logic in the same tree", + "Thresholds > TrueNAS > Disks with no saved TrueNAS-wide value: header reads 'Defaults: NVMe 70\u00b0C \u00b7 SAS 65\u00b0C \u00b7 SATA 55\u00b0C', note 'Unless set here, each disk alerts at Disk temperature by type, set under Agents.', Global Defaults Temp cell empty with placeholder 'By type' (fits after widening to w-20) and On, disk rows nvme0n1/nvme1n1 70\u00b0C, sda/sdb 55\u00b0C, sdc-sdf 65\u00b0C", + "Same section at 375px: Global Defaults card shows TEMP \u00b0C 'By type' with On, disk cards show their type triggers, no horizontal overflow", + "Agent Disk Temp default switched off through the API: header 'Defaults: all metric alerts off', Global Defaults cell Off with title 'Off because the agent Disk Temp default under Agents is off...', disk rows Off", + "Saved TrueNAS-wide 62: header 'Defaults: Temp 62\u00b0C', every disk row 62\u00b0C, Custom badge and reset control after reload; saved 55 survives save and reload as 55 (stored with truenasDiskTemperatureByType)", + "Alerts with factory per-type thresholds: after stack raises nvme1n1 72 (trigger 70) and sda 57 (trigger 55) with diskType metadata, none for nvme0n1 60 or sdc 63; before (main) raised all four at a flat 55", + "TrueNAS storage table agrees: Attention 'Disk temperature is 72\u00b0C, at or above its 70\u00b0C alert threshold.' on nvme1n1 and '57\u00b0C ... 55\u00b0C' on sda, none on nvme0n1", + "TrueNAS disk drawer (Platform details > Show TrueNAS > Health): nvme0n1 Temperature 60\u00b0C in default text colour (main toned it amber from the fixed 55C); sda 57\u00b0C amber rgb(252,211,77) at 1366 and at 375 with no overflow" + ], + "interactions": [ + "Typed 62 into the TrueNAS Disks Global Defaults Temp cell, saved, reloaded: API truenasDiskDefaults {trigger 62, clear 57}, cell shows 62 with Custom", + "Triple-clicked and typed 55, saved, reloaded: API keeps {55, 50}, cell shows 55 with Custom and the reset control", + "Clicked Reset to factory defaults: cell returns to 'By type'; saved: API truenasDiskDefaults {}", + "On the 375px card, clicked On to switch the default Off, saved: API stores {trigger 0, clear 0} and the after stack's TrueNAS disk alerts resolve; clicked again: returns to 'By type', saved: {}", + "With the agent Disk Temp default off, clicked the Off badge: stages an explicit 55 (Custom, rows 55\u00b0C); discarded", + "Loaded an old saved config (flat 55/50, no marker) with the new binary: GET returns truenasDiskDefaults {} and truenasDiskTemperatureByType true", + "Expanded nvme0n1 and sda rows on /truenas/storage, opened Platform details and Show TrueNAS, read the Temperature row value and computed colour at 1366 and 375" + ] +} diff --git a/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx b/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx index 92593af32f..92232f4ee8 100644 --- a/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx +++ b/frontend-modern/src/components/Alerts/AlertResourceTableDesktop.tsx @@ -43,12 +43,11 @@ import { ALERT_RESOURCE_METRIC_OFF_VALUE, getAlertResourceColumnKind, getAlertResourceColumnHeaderTooltip, - getAlertResourceEnabledDefault, getAlertResourceMetricBounds, getAlertResourceMetricDelayOverride, getAlertResourceMetricStep, - isAlertResourceMetricOff, normalizeAlertResourceMetricKey, + resolveAlertResourceGlobalDefaultCell, } from './alertResourceTableModel'; import type { OfflineState, Resource, ResourceTableProps } from './ResourceTable'; @@ -210,7 +209,10 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps) {(column) => ( {column} @@ -281,10 +283,14 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps) {(column) => { const metric = normalizeAlertResourceMetricKey(column); const bounds = getAlertResourceMetricBounds(metric); - const value = () => props.table.globalDefaults?.[metric] ?? 0; + const value = () => props.table.globalDefaults?.[metric]; + const fallback = () => props.table.globalDefaultFallbacks?.[metric]; // An unset default and a stored 0 are both disabled to the - // alert engine, so neither may render as On. - const isOff = () => isAlertResourceMetricOff(value()); + // alert engine, so neither may render as On, unless the + // metric's unset default follows another setting. + const cell = () => + resolveAlertResourceGlobalDefaultCell(metric, value(), fallback()); + const isOff = () => cell().isOff; return ( { const nextValue = parseFloat(e.currentTarget.value); @@ -318,12 +328,16 @@ export function AlertResourceTableDesktop(props: AlertResourceTableDesktopProps) })); props.table.setHasUnsavedChanges?.(true); }} - class={`w-16 px-2 py-0.5 text-sm text-center border rounded ${ + class={`${cell().follows && !isOff() ? 'w-20' : 'w-16'} px-2 py-0.5 text-sm text-center border rounded ${ isOff() ? 'border-border bg-surface-alt text-muted italic placeholder: dark:placeholder: placeholder:opacity-60 pointer-events-none' : 'border-border text-base-content focus:border-blue-500 focus:ring-1 focus:ring-blue-500' }`} - title={getAlertResourceTableMetricInputTitle(isOff())} + title={ + cell().follows + ? fallback()?.title + : getAlertResourceTableMetricInputTitle(isOff()) + } />