diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md
index 8bf3f31dd3..daddabce1f 100644
--- a/docs/CONFIGURATION.md
+++ b/docs/CONFIGURATION.md
@@ -567,8 +567,10 @@ check that the intended value persisted.
For TrueNAS physical disks, read the [disk temperature and health guide](TRUENAS.md#disk-temperature-and-health)
before changing a rule: heat, SMART faults and missing readings are different
-signals, and Storage display thresholds do not yet follow TrueNAS-wide or
-per-disk temperature overrides. Silencing an alert is not disk recovery.
+signals. Storage temperature colouring and heat reasons follow the saved
+per-disk override, then the TrueNAS-wide default, then the inherited by-type
+policy. Off leaves the temperature visible but unjudged; last known readings
+are not current heat. Silencing an alert is not disk recovery.
### Metric thresholds, Off and inheritance
diff --git a/docs/TRUENAS.md b/docs/TRUENAS.md
index e6c5df7f73..eceeab1a81 100644
--- a/docs/TRUENAS.md
+++ b/docs/TRUENAS.md
@@ -94,10 +94,11 @@ Temperature rule disables that rule, not its native SMART faults. Acknowledging
or dismissing a TrueNAS SMART alert does not repair the disk; its risk remains
while TrueNAS still reports the condition.
-**Storage display limitation:** temperature colouring and its heat reason
-currently use the inherited agent/by-type policy, not the TrueNAS-wide or
-per-disk temperature override. A display mismatch is not proof that your saved
-rule failed. Compare the same disk, observation time and saved threshold with
+**Storage temperature judgement:** temperature colouring and its heat reason
+use the saved per-disk temperature override, then the TrueNAS-wide default,
+then the inherited agent/by-type policy. An Off or disabled temperature policy
+leaves the reading visible but unjudged; a last known reading is never current
+heat. A display mismatch is not proof that your saved rule failed. Compare the same disk, observation time and saved threshold with
its existing TrueNAS reading and SMART state; do not raise thresholds, run new
SMART tests, force a probe or restart merely to make the views agree.
diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index 2c498bb9ac..61fb8cae35 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -2069,11 +2069,13 @@ Disks Global Defaults cell reads `By type` while unset
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.
+The TrueNAS storage table and disk drawer now judge current disk heat through
+`getTrueNASDiskTemperatureThresholds(disk)`: the disk's canonical-ID temperature
+override, then a TrueNAS-wide value, then the per-type policy. Disabled disks
+or TrueNAS Disks defaults and explicit Off values leave readings unjudged;
+retained values never count as current heat. The API-only disk does not inherit
+a host Disk Temp override or the agent alerts switch. Patrol/AI use the disk-specific backend resolver described below; the
+frontend and backend retain their own verification boundaries.
`TestTrueNASDiskTemperatureAlertsFollowDiskTemperaturePolicy` and
`TestTrueNASDiskTemperatureAlertIgnoresRetainedReading` in
`internal/alerts/unified_eval_test.go` pin the tiers and retained readings;
@@ -4497,5 +4499,22 @@ Off/disabled, untyped, clone and nil-manager compatibility.
`TestAlertThresholdAdapter_TrueNASDiskTemperatureFollowsItsAlertTiers` and
`TestPatrolJudgesTrueNASDisksByTheirAlertTiers` cover adapter, Patrol triage,
verification and Assistant context while preserving agent-host overrides.
-Frontend table/drawer tiers remain a separate Web-owned change; this backend
-repair does not establish browser or native appliance acceptance.
+Frontend table/drawer tiers use the Web resolver described below; these
+backend tests do not establish browser or native appliance acceptance.
+
+### TrueNAS disk display reconciliation (8 October 2026)
+
+The TrueNAS storage table and inline resource drawer resolve temperature by
+the disk resource's own `temperature` override, then `truenasDiskDefaults`,
+then the per-type disk policy. Explicit zero/negative thresholds or a disabled
+disk/TrueNAS default leave readings visible but unjudged. Host `diskTemperature`
+overrides and the agent alerts switch do not control API-only TrueNAS disks.
+The shared agent disk resolver and Proxmox `pulse-relaxed` guest resolution
+remain unchanged. Existing Off-save normalization and inherited TrueNAS
+threshold editors on main are retained, not replaced by the older PR stack.
+
+Verification: `src/utils/__tests__/metricThresholds.test.ts`,
+`src/features/truenas/__tests__/TrueNASStorageTopologyTable.test.tsx` and the
+production table/drawer fixture `browser-tests/truenas-disk-thresholds.cjs`.
+The containing backend also resolves the disk-specific tiers for Patrol.
+Frontend checks do not establish native appliance or notification recovery.
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 724646f4a8..db980d45b6 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -9025,3 +9025,17 @@ passive steps, evidence distinctions, safety link/target, disclosure and exact
shipped mirror. Exact-parent controls reject the old guide. This is safe help
maintenance, not a collector/classifier repair, browser acceptance, native
backup/recovery proof or published availability.
+
+### TrueNAS heat uses the configured disk policy (8 October 2026)
+
+The production TrueNAS storage table and inline resource drawer pass each
+disk's canonical resource ID through the alerts store. Type defaults, a
+TrueNAS-wide value and the disk override produce one consistent status/reason
+and temperature tone; retained values stay muted and unjudged. The existing
+keyboard disclosure and phone table layouts are preserved.
+
+Verification: `TrueNASStorageTopologyTable.test.tsx`, the drawer model tests,
+and `browser-tests/truenas-disk-thresholds.cjs` exercise desktop Chromium and
+phone WebKit, config refresh, Attention filtering and keyboard disclosure.
+The fixture imports the production store/table/drawer/CSS but uses synthetic
+HTTP and readings; it is not full-shell, appliance or published acceptance.
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index f3d693dd8e..937effd895 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -7550,5 +7550,19 @@ classification; ordinary agent disks still follow their reporting host's policy.
Registry identity, source precedence and retained-reading semantics are unchanged.
`TestIsTrueNASDiskResourceMatchesTheUnifiedEvaluator` and
`TestPatrolJudgesTrueNASDisksByTheirAlertTiers` exercise the unified registry
-projection and its consumer boundaries. Table/drawer frontend acceptance remains
-with Web, not established by these backend tests.
+projection and its consumer boundaries. Table/drawer frontend acceptance is described below and remains
+separate from these backend tests.
+
+### TrueNAS disk-specific heat presentation (8 October 2026)
+
+`TrueNASDiskTemperatureThresholdResolver` receives the disk resource (including
+its canonical ID), not just a disk type. The table's status dot, reason,
+Attention filter/count and drawer temperature row use the same resolved tiers.
+A retained temperature is not current heat under any override. Changing alert
+configuration updates those judgments without changing inventory identity.
+
+Verification: `truenasPageModel.test.ts`,
+`resourceDetailDrawerTrueNASModel.coverage2.test.ts` and
+`browser-tests/truenas-disk-thresholds.cjs` cover disk overrides, TrueNAS-wide
+limits, Off and last-known readings. Synthetic browser observations do not
+establish sustained native metrics, History, revocation or release availability.
diff --git a/frontend-modern/browser-tests/truenas-disk-thresholds.cjs b/frontend-modern/browser-tests/truenas-disk-thresholds.cjs
new file mode 100644
index 0000000000..56725a1b11
--- /dev/null
+++ b/frontend-modern/browser-tests/truenas-disk-thresholds.cjs
@@ -0,0 +1,216 @@
+// Fresh final-byte production table/drawer proof, with synthetic HTTP only.
+process.env.RAYON_NUM_THREADS = '2';
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { chromium, webkit } = require('playwright');
+const root = '/workspace/frontend-modern';
+const output = '/workspace/tmp/truenas-threshold-browser';
+const origin = 'http://127.0.0.1:5327';
+const config = (patch = {}) => ({
+ enabled: true,
+ activationState: 'active',
+ guestDefaults: { memory: { trigger: 85, clear: 80 } },
+ nodeDefaults: {},
+ storageDefault: { trigger: 85, clear: 80 },
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: {
+ nvme: { trigger: 70, clear: 65 },
+ sas: { trigger: 65, clear: 60 },
+ sata: { trigger: 55, clear: 50 },
+ },
+ truenasDiskTemperatureByType: true,
+ overrides: {},
+ ...patch,
+});
+
+(async () => {
+ fs.mkdirSync(output, { recursive: true });
+ process.chdir(root);
+ const version = require('playwright/package.json').version;
+ assert.equal(
+ version,
+ JSON.parse(fs.readFileSync('/workspace/tests/integration/package-lock.json')).packages[
+ 'node_modules/@playwright/test'
+ ].version,
+ );
+ const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
+ const server = await createServer({
+ cacheDir: path.join(output, 'vite-cache'),
+ optimizeDeps: { entries: ['browser-tests/truenas-disk-thresholds.html'] },
+ server: { host: '127.0.0.1', port: 5327, strictPort: true },
+ });
+ const observations = [];
+ let browser;
+ try {
+ await server.listen();
+ for (const [name, engine, width, height] of [
+ ['chromium-desktop', chromium, 1440, 1000],
+ ['webkit-phone', webkit, 390, 844],
+ ]) {
+ browser = await engine.launch(
+ engine === chromium
+ ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] }
+ : { headless: true },
+ );
+ const page = await browser.newPage({ viewport: { width, height } });
+ page.setDefaultTimeout(12000);
+ const errors = [];
+ const consoleErrors = [];
+ page.on('console', (m) => {
+ if (m.type() === 'error') consoleErrors.push(m.text());
+ });
+ console.log('BROWSER_VIEWPORT', name);
+ page.on('pageerror', (e) => errors.push(e.message));
+ await page.routeWebSocket(/\/ws(?:\?|$)/, (ws) =>
+ ws.send(
+ JSON.stringify({
+ type: 'initialState',
+ data: { connectedInfrastructure: [], activeAlerts: [], metrics: [], stats: {} },
+ }),
+ ),
+ );
+
+ let current = config();
+ await page.route('**/*', (route) => {
+ const url = new URL(route.request().url());
+ if (url.origin !== origin) return route.abort();
+ if (url.pathname === '/api/alerts/config') return route.fulfill({ json: current });
+ if (url.pathname.startsWith('/api/'))
+ return route.fulfill({
+ json: { data: [], resources: [], policy: {}, enabled: false, hasAuthentication: true },
+ });
+ return route.continue();
+ });
+ // Readiness is distinct from interaction assertions: allow cold fixture compilation
+ // within the unchanged executor deadline, then wait for the production mount.
+ await page.goto(`${origin}/browser-tests/truenas-disk-thresholds.html`, {
+ waitUntil: 'domcontentloaded',
+ timeout: 45000,
+ });
+ await page.waitForFunction(() => !!window.__thresholdFixture, null, { timeout: 45000 });
+ if (width < 768) await page.getByRole('button', { name: /^Filters/ }).click();
+ await page
+ .getByRole('group', { name: 'Storage type', exact: true })
+ .getByRole('button', { name: /(?:Physical disks|Disks), 3/ })
+ .click();
+ const row = (id) => page.locator(`[data-truenas-storage-resource="${id}"]`);
+ const health = async (id) => {
+ await row(id).waitFor();
+ return row(id).evaluate(
+ (element) =>
+ element
+ .querySelector('[data-truenas-storage-health]')
+ ?.getAttribute('data-truenas-storage-health') ?? 'healthy',
+ );
+ };
+ const refresh = async (value) => {
+ current = value;
+ await page.evaluate(() => window.__thresholdFixture.refresh());
+ };
+ const capture = async (state) => {
+ const file = `${name}-${state}.png`;
+ await page.screenshot({ path: path.join(output, file), fullPage: true });
+ observations.push({
+ name,
+ state,
+ viewport: { width, height },
+ file,
+ health: [
+ await health('nvme-wide'),
+ await health('nvme-raised'),
+ await health('nvme-retained'),
+ ],
+ });
+ };
+ await refresh(config());
+ assert.equal(await health('nvme-wide'), 'healthy');
+ assert.equal(await health('nvme-raised'), 'attention');
+ assert.equal(await health('nvme-retained'), 'healthy');
+ assert.match(await row('nvme-retained').innerText(), /95/);
+ await capture('by-type');
+
+ await refresh(
+ config({
+ truenasDiskDefaults: { temperature: { trigger: 62, clear: 57 } },
+ overrides: { 'nvme-raised': { temperature: { trigger: 75, clear: 70 } } },
+ }),
+ );
+ assert.equal(await health('nvme-wide'), 'attention');
+ assert.equal(await health('nvme-raised'), 'healthy');
+ assert.equal(await health('nvme-retained'), 'healthy');
+ await page
+ .getByRole('group', { name: 'Status', exact: true })
+ .getByRole('button', { name: /Attention/ })
+ .click();
+ assert.equal(await page.locator('[data-truenas-storage-resource]').count(), 1);
+ assert.equal(await row('nvme-wide').count(), 1);
+ await page
+ .getByRole('group', { name: 'Status', exact: true })
+ .getByRole('button', { name: 'All, 3', exact: true })
+ .click();
+ const toggle = page.getByRole('button', { name: /Expand details for nvme-raised/i });
+ await toggle.focus();
+ await toggle.press('Enter');
+ await page.getByTestId('resource-platform-details').locator('summary').click();
+ await page.getByRole('button', { name: 'Show TrueNAS', exact: true }).click();
+ const temperatureRow = page
+ .getByTestId('resource-truenas-details-section')
+ .getByRole('row')
+ .filter({ has: page.getByText('Temperature', { exact: true }) });
+ assert.match(await temperatureRow.innerText(), /72(?:\.0)?°C/);
+ assert.match(
+ await temperatureRow.locator('td').last().getAttribute('class'),
+ /text-base-content/,
+ );
+ assert.doesNotMatch(
+ await temperatureRow.locator('td').last().getAttribute('class'),
+ /text-amber/,
+ );
+ assert.match(await page.locator('body').innerText(), /72(?:\.0)?°C/);
+ await capture('raised-drawer');
+ await page.getByRole('button', { name: /Collapse details for nvme-raised/i }).press('Enter');
+
+ await refresh(
+ config({
+ truenasDiskDefaults: { temperature: { trigger: 0, clear: 0 } },
+ overrides: { 'nvme-raised': { temperature: { trigger: 71, clear: 66 } } },
+ }),
+ );
+ assert.equal(await health('nvme-wide'), 'healthy');
+ assert.equal(await health('nvme-raised'), 'attention');
+ await refresh(
+ config({
+ truenasDiskDefaults: { disabled: true },
+ overrides: { 'nvme-raised': { temperature: { trigger: 71, clear: 66 } } },
+ }),
+ );
+ assert.equal(await health('nvme-raised'), 'healthy');
+ await capture('off');
+ const guest = await page.evaluate(() => window.__thresholdFixture.guestControl());
+ assert.deepEqual(guest, { warning: 80, critical: 92 });
+ assert.deepEqual(errors, []);
+ assert.deepEqual(consoleErrors, []);
+ await browser.close();
+ browser = null;
+ }
+ fs.writeFileSync(
+ path.join(output, 'observations.json'),
+ JSON.stringify({ result: 'passed', playwright: version, observations }, null, 2),
+ );
+ console.log(
+ JSON.stringify({
+ result: 'passed',
+ playwright: version,
+ captures: observations.length,
+ observations,
+ }),
+ );
+ } finally {
+ if (browser) await browser.close();
+ await server.close();
+ }
+})().catch((error) => {
+ console.error(error);
+ process.exitCode = 1;
+});
diff --git a/frontend-modern/browser-tests/truenas-disk-thresholds.html b/frontend-modern/browser-tests/truenas-disk-thresholds.html
new file mode 100644
index 0000000000..26500eed9d
--- /dev/null
+++ b/frontend-modern/browser-tests/truenas-disk-thresholds.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ TrueNAS disk threshold verification
+
+
+
+
+
+
diff --git a/frontend-modern/browser-tests/truenas-disk-thresholds.tsx b/frontend-modern/browser-tests/truenas-disk-thresholds.tsx
new file mode 100644
index 0000000000..8492de3483
--- /dev/null
+++ b/frontend-modern/browser-tests/truenas-disk-thresholds.tsx
@@ -0,0 +1,88 @@
+// Production table, inline drawer and alert store. Synthetic API/config only;
+// no appliance, guest-agent, alert delivery or installed acceptance.
+import { onMount } from 'solid-js';
+import { render } from 'solid-js/web';
+import { Router, Route } from '@solidjs/router';
+import { TrueNASStorageTopologyTable } from '../src/features/truenas/TrueNASStorageTopologyTable';
+import { DarkModeContext, WebSocketContext } from '../src/contexts/appRuntime';
+import { getGlobalWebSocketStore } from '../src/stores/websocket-global';
+import { useAlertsActivation } from '../src/stores/alertsActivation';
+import { resolveMetricDisplayThresholds } from '../src/utils/metricThresholds';
+import type { Resource } from '../src/types/resource';
+import '../src/index.css';
+
+const disk = (id: string, temperature: number, retained = false): Resource =>
+ ({
+ id,
+ type: 'physical_disk',
+ name: id,
+ displayName: id,
+ status: 'online',
+ platformType: 'truenas',
+ platformScopes: ['truenas'],
+ sourceType: 'api',
+ physicalDisk: {
+ devPath: `/dev/${id}`,
+ serial: `fixture-${id}`,
+ diskType: 'nvme',
+ temperature,
+ health: 'PASSED',
+ ...(retained
+ ? { collection: { temperature: { state: 'unavailable', reason: 'Fixture standby' } } }
+ : {}),
+ },
+ }) as Resource;
+
+function Fixture() {
+ const store = useAlertsActivation();
+ onMount(() => {
+ (window as any).__thresholdFixture = {
+ refresh: store.refreshConfig,
+ thresholds: (id: string) => store.getTrueNASDiskTemperatureThresholds(disk(id, 72)),
+ guestControl: () =>
+ resolveMetricDisplayThresholds(store.config(), 'guest', 'memory', 'guest', [
+ 'pulse-relaxed',
+ ]),
+ };
+ });
+ const resources = [
+ disk('nvme-wide', 64),
+ disk('nvme-raised', 72),
+ disk('nvme-retained', 95, true),
+ ];
+ return (
+
+ TrueNAS disk temperature
+
+ Synthetic readings and configuration; no appliance operations.
+
+ }
+ emptyTitle="No storage"
+ emptyDescription="No storage"
+ />
+
+ );
+}
+
+render(
+ () => (
+
+ (
+ document.documentElement.classList.contains('dark')}
+ >
+
+
+
+
+ )}
+ />
+
+ ),
+ document.getElementById('root')!,
+);
diff --git a/frontend-modern/browser-verification/39d274dff126fd9c.json b/frontend-modern/browser-verification/39d274dff126fd9c.json
new file mode 100644
index 0000000000..362fdcc6e0
--- /dev/null
+++ b/frontend-modern/browser-verification/39d274dff126fd9c.json
@@ -0,0 +1,47 @@
+{
+ "version": 1,
+ "base_sha": "c9d17b81b18de199157afe3a541a17a8352be995",
+ "verified_at": "2026-10-09T13:06:31Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts",
+ "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts",
+ "frontend-modern/src/stores/alertsActivation.ts",
+ "frontend-modern/src/utils/metricThresholds.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts": "031fcb046ec519e3877323c48051cc1055c0ab684e06710a6510ff37df5b56ac",
+ "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "94e4b364c744f204ff52e2d28cde6b80a5a66ac414c7ba702113e960a6a67e17",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "62f064405b0c8e947ef1127874d3d06f6e3330153594de11b3ff3b443d29444c",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts": "e8dd896694626e81de97343f79cd988311861a678eca1d3925883e8a61644f55",
+ "frontend-modern/src/stores/alertsActivation.ts": "5e080281342faa3753c3d3216d0492d365a10475e498f8a71d2344ec979675bf",
+ "frontend-modern/src/utils/metricThresholds.ts": "6024a0ca020172734c8ce30c8e054714174bd67a0aa66ff1a370c28079c7f8a4"
+ },
+ "routes": ["/browser-tests/truenas-disk-thresholds.html"],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 1000
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Production TrueNAS table, inline ResourceDetailDrawer, alert store, router context and CSS with synthetic HTTP/config/readings; not full-shell, appliance, History, notification or release acceptance. Playwright 1.56.1 equals the committed integration lock; desktop Chromium and phone WebKit.",
+ "By-type NVMe trigger 70C: current 72C disk Attention, 64C disk healthy. Retained 95C reading visible, dotted/muted and unjudged.",
+ "TrueNAS-wide 62C makes the 64C disk Attention. A per-disk canonical-ID override 75C keeps the separate 72C disk healthy and its expanded drawer Temperature tone normal.",
+ "TrueNAS-wide zero leaves the disk with no override unjudged; explicit per-disk 71C still judges its 72C reading. Disabled TrueNAS Disks defaults mute every disk even with that override.",
+ "The shared pulse-relaxed guest control remains warning 80 / critical 92 for memory. No uncaught page errors or error console messages on the complete final pass. Six final screenshots inspected; desktop/phone controls and drawer content legible.",
+ "Fresh pass over c9d17b81 composition: the page model preserves current main Recovery-field removal and disk-specific policy. All six new captures inspected; browser command and outer execution exit 0. Original retained browser/history limitations remain historical, not reconstructed."
+ ],
+ "interactions": [
+ "Refresh configuration through the production alert store and check status dots, reasons and counts.",
+ "Filter Attention, verify only the 64C disk remains, then restore Status All without changing the disk kind filter.",
+ "Open the production inline drawer by keyboard Enter, expand Platform details and Show TrueNAS, verify the 72C temperature is normal, and collapse by keyboard.",
+ "Expand phone Filters and repeat the same disk/status/disclosure interactions in WebKit at 390x844."
+ ]
+}
diff --git a/frontend-modern/browser-verification/71314e58281fa63d.json b/frontend-modern/browser-verification/71314e58281fa63d.json
new file mode 100644
index 0000000000..1c8e8b38eb
--- /dev/null
+++ b/frontend-modern/browser-verification/71314e58281fa63d.json
@@ -0,0 +1,48 @@
+{
+ "version": 1,
+ "base_sha": "55f80f7adab5f6286cef5ad7f3eecd7f67266fb2",
+ "verified_at": "2026-10-08T10:16:10Z",
+ "result": "passed",
+ "changed_paths": [
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts",
+ "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts",
+ "frontend-modern/src/stores/alertsActivation.ts",
+ "frontend-modern/src/utils/metricThresholds.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts": "031fcb046ec519e3877323c48051cc1055c0ab684e06710a6510ff37df5b56ac",
+ "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "94e4b364c744f204ff52e2d28cde6b80a5a66ac414c7ba702113e960a6a67e17",
+ "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "62f064405b0c8e947ef1127874d3d06f6e3330153594de11b3ff3b443d29444c",
+ "frontend-modern/src/features/truenas/truenasPageModel.ts": "a6def9d4c3e5da5c61b863780ea206acfc663345a914f1c8d901b426ad5597b2",
+ "frontend-modern/src/stores/alertsActivation.ts": "5e080281342faa3753c3d3216d0492d365a10475e498f8a71d2344ec979675bf",
+ "frontend-modern/src/utils/metricThresholds.ts": "6024a0ca020172734c8ce30c8e054714174bd67a0aa66ff1a370c28079c7f8a4"
+ },
+ "routes": [
+ "/browser-tests/truenas-disk-thresholds.html"
+ ],
+ "viewports": [
+ {
+ "width": 1440,
+ "height": 1000
+ },
+ {
+ "width": 390,
+ "height": 844
+ }
+ ],
+ "states": [
+ "Production TrueNAS table, inline ResourceDetailDrawer, alert store, router context and CSS with synthetic HTTP/config/readings; not full-shell, appliance, History, notification or release acceptance. Playwright 1.56.1 equals the committed integration lock; desktop Chromium and phone WebKit.",
+ "By-type NVMe trigger 70C: current 72C disk Attention, 64C disk healthy. Retained 95C reading visible, dotted/muted and unjudged.",
+ "TrueNAS-wide 62C makes the 64C disk Attention. A per-disk canonical-ID override 75C keeps the separate 72C disk healthy and its expanded drawer Temperature tone normal.",
+ "TrueNAS-wide zero leaves the disk with no override unjudged; explicit per-disk 71C still judges its 72C reading. Disabled TrueNAS Disks defaults mute every disk even with that override.",
+ "The shared pulse-relaxed guest control remains warning 80 / critical 92 for memory. No uncaught page errors or error console messages on the complete final pass. Six final screenshots inspected; desktop/phone controls and drawer content legible."
+ ],
+ "interactions": [
+ "Refresh configuration through the production alert store and check status dots, reasons and counts.",
+ "Filter Attention, verify only the 64C disk remains, then restore Status All without changing the disk kind filter.",
+ "Open the production inline drawer by keyboard Enter, expand Platform details and Show TrueNAS, verify the 72C temperature is normal, and collapse by keyboard.",
+ "Expand phone Filters and repeat the same disk/status/disclosure interactions in WebKit at 390x844."
+ ]
+}
diff --git a/frontend-modern/public/docs/CONFIGURATION.md b/frontend-modern/public/docs/CONFIGURATION.md
index 8bf3f31dd3..daddabce1f 100644
--- a/frontend-modern/public/docs/CONFIGURATION.md
+++ b/frontend-modern/public/docs/CONFIGURATION.md
@@ -567,8 +567,10 @@ check that the intended value persisted.
For TrueNAS physical disks, read the [disk temperature and health guide](TRUENAS.md#disk-temperature-and-health)
before changing a rule: heat, SMART faults and missing readings are different
-signals, and Storage display thresholds do not yet follow TrueNAS-wide or
-per-disk temperature overrides. Silencing an alert is not disk recovery.
+signals. Storage temperature colouring and heat reasons follow the saved
+per-disk override, then the TrueNAS-wide default, then the inherited by-type
+policy. Off leaves the temperature visible but unjudged; last known readings
+are not current heat. Silencing an alert is not disk recovery.
### Metric thresholds, Off and inheritance
diff --git a/frontend-modern/public/docs/TRUENAS.md b/frontend-modern/public/docs/TRUENAS.md
index e6c5df7f73..eceeab1a81 100644
--- a/frontend-modern/public/docs/TRUENAS.md
+++ b/frontend-modern/public/docs/TRUENAS.md
@@ -94,10 +94,11 @@ Temperature rule disables that rule, not its native SMART faults. Acknowledging
or dismissing a TrueNAS SMART alert does not repair the disk; its risk remains
while TrueNAS still reports the condition.
-**Storage display limitation:** temperature colouring and its heat reason
-currently use the inherited agent/by-type policy, not the TrueNAS-wide or
-per-disk temperature override. A display mismatch is not proof that your saved
-rule failed. Compare the same disk, observation time and saved threshold with
+**Storage temperature judgement:** temperature colouring and its heat reason
+use the saved per-disk temperature override, then the TrueNAS-wide default,
+then the inherited agent/by-type policy. An Off or disabled temperature policy
+leaves the reading visible but unjudged; a last known reading is never current
+heat. A display mismatch is not proof that your saved rule failed. Compare the same disk, observation time and saved threshold with
its existing TrueNAS reading and SMART state; do not raise thresholds, run new
SMART tests, force a probe or restart merely to make the views agree.
diff --git a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.coverage2.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.coverage2.test.ts
index 06e266ae0c..c283c94408 100644
--- a/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.coverage2.test.ts
+++ b/frontend-modern/src/components/Infrastructure/__tests__/resourceDetailDrawerTrueNASModel.coverage2.test.ts
@@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
+import type { AlertConfig } from '@/types/alerts';
import {
buildTrueNASDetailSections,
buildTrueNASDetailsSummary,
@@ -14,6 +15,7 @@ import type {
ResourceTrueNASShareMeta,
ResourceTrueNASVMMeta,
} from '@/types/resource';
+import { resolveTrueNASDiskTemperatureDisplayThresholds } from '@/utils/metricThresholds';
// Every target helper (except the exported buildTrueNASDetailsSummary) is
// module-private, so each case drives it through the two public entry points
@@ -833,8 +835,8 @@ describe('buildTrueNASDiskSections additional branches', () => {
// The drawer judges heat by the disk type's alert trigger, through the
// resolver the alerts store provides; these are the factory triggers.
const factoryTriggers: Record = { nvme: 70, sas: 65, sata: 55 };
- const resolveThresholds = (diskType: string) => {
- const critical = factoryTriggers[diskType.toLowerCase()] ?? 55;
+ const resolveThresholds = (disk: Pick) => {
+ const critical = factoryTriggers[(disk.physicalDisk?.diskType ?? '').toLowerCase()] ?? 55;
return { warning: critical - 5, critical };
};
const temperatureTone = (disk: Partial) =>
@@ -854,6 +856,52 @@ describe('buildTrueNASDiskSections additional branches', () => {
expect(temperatureTone({ diskType: 'sata', temperature: 40 })).toBe('default');
});
+ it('judges heat by the override and TrueNAS-wide value the disk alert uses', () => {
+ const config = {
+ enabled: true,
+ guestDefaults: {},
+ nodeDefaults: {},
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: { nvme: { trigger: 70, clear: 65 } },
+ storageDefault: { trigger: 85, clear: 80 },
+ overrides: { 'truenas-resource': { temperature: { trigger: 75, clear: 70 } } },
+ } as AlertConfig;
+ const toneUnder = (cfg: AlertConfig, disk: Partial) =>
+ buildTrueNASDetailSections(diskRes(disk), (resource) =>
+ resolveTrueNASDiskTemperatureDisplayThresholds(
+ cfg,
+ resource.physicalDisk?.diskType,
+ resource.id,
+ ),
+ )
+ .flatMap((section) => section.rows)
+ .find((row) => row.label === 'Temperature')?.tone;
+
+ // The drawer's disk carries a 75C override, so 72C is not heat even
+ // though it is past the 70C NVMe trigger.
+ expect(toneUnder(config, { diskType: 'nvme', temperature: 72 })).toBe('default');
+ expect(toneUnder(config, { diskType: 'nvme', temperature: 75 })).toBe('warning');
+ // Another disk's override does not reach it.
+ const otherDisk = {
+ ...config,
+ overrides: { 'other-disk': { temperature: { trigger: 75, clear: 70 } } },
+ } as AlertConfig;
+ expect(toneUnder(otherDisk, { diskType: 'nvme', temperature: 72 })).toBe('warning');
+ // A TrueNAS-wide value replaces the type trigger.
+ const trueNASWide = {
+ ...otherDisk,
+ truenasDiskDefaults: { temperature: { trigger: 62, clear: 57 } },
+ } as AlertConfig;
+ expect(toneUnder(trueNASWide, { diskType: 'nvme', temperature: 64 })).toBe('warning');
+ expect(toneUnder(trueNASWide, { diskType: 'nvme', temperature: 60 })).toBe('default');
+ // A muted disk raises no temperature alert, so it is never heat.
+ const muted = {
+ ...trueNASWide,
+ overrides: { 'truenas-resource': { disabled: true } },
+ } as AlertConfig;
+ expect(toneUnder(muted, { diskType: 'nvme', temperature: 90 })).toBe('default');
+ });
+
it('judges no heat without thresholds', () => {
expect(findRow(diskRes({ diskType: 'sata', temperature: 80 }), 'Temperature')?.tone).toBe(
'default',
diff --git a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts
index 9beecfdf4d..ca5000d04f 100644
--- a/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts
+++ b/frontend-modern/src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts
@@ -404,10 +404,12 @@ const diskTemperature = (disk: ResourcePhysicalDiskMeta) =>
// A retained reading, such as a silent host agent's on a merged disk row, is
// not current: it takes no heat tone and its title says why. A current reading
-// is hot from its disk type's alert trigger, the threshold the TrueNAS storage
-// table and the disk's temperature alert judge it by. Without a resolver, heat
-// is not judged.
+// is hot from the trigger the disk's temperature alert fires at (its own
+// override, the TrueNAS-wide value, then its type's policy), the threshold the
+// TrueNAS storage table judges it by too. Without a resolver, heat is not
+// judged.
const diskTemperatureRow = (
+ resource: Resource,
disk: ResourcePhysicalDiskMeta,
resolveDiskTemperatureThresholds?: TrueNASDiskTemperatureThresholdResolver,
): ResourceDetailDrawerTrueNASRow | null => {
@@ -419,7 +421,7 @@ const diskTemperatureRow = (
tone: 'muted',
});
}
- const thresholds = resolveDiskTemperatureThresholds?.(disk.diskType ?? '') ?? null;
+ const thresholds = resolveDiskTemperatureThresholds?.(resource) ?? null;
const hot = isPhysicalDiskRunningHot(
{ temperature: disk.temperature ?? 0, collection: disk.collection },
thresholds,
@@ -451,7 +453,7 @@ const buildTrueNASDiskSections = (
const healthRows = compactRows([
row('Health', normalizeDelimitedLabel(disk.health), { tone: diskStateTone(disk) }),
- diskTemperatureRow(disk, resolveDiskTemperatureThresholds),
+ diskTemperatureRow(resource, disk, resolveDiskTemperatureThresholds),
row(
'Wearout',
disk.wearout === undefined || disk.wearout < 0 ? null : formatPercent(disk.wearout),
diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts
index 8b4c20220b..74cb6709a2 100644
--- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts
+++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts
@@ -382,9 +382,9 @@ export const useResourceDetailDrawerDerivedState = (
const vmwareDetailsSummary = createMemo(() =>
buildVMwareDetailsSummary(resource().type, vmwareData()),
);
- const { getDiskTemperatureThresholds } = useAlertsActivation();
+ const { getTrueNASDiskTemperatureThresholds } = useAlertsActivation();
const trueNASDetailSections = createMemo(() =>
- buildTrueNASDetailSections(resource(), getDiskTemperatureThresholds),
+ buildTrueNASDetailSections(resource(), getTrueNASDiskTemperatureThresholds),
);
const hasTrueNASDetails = createMemo(() => trueNASDetailSections().length > 0);
const trueNASDetailsSummary = createMemo(() => buildTrueNASDetailsSummary(resource()));
diff --git a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
index baa629330f..10f3695083 100644
--- a/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
+++ b/frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx
@@ -355,9 +355,9 @@ export const getTrueNASStorageTopologyIndentClass = (depth: number): string => {
const ResourceCell: Component<{ row: TrueNASStorageTopologyRow; detailToggle?: JSX.Element }> = (
props,
) => {
- const { getDiskTemperatureThresholds } = useAlertsActivation();
+ const { getTrueNASDiskTemperatureThresholds } = useAlertsActivation();
const displayStatus = () =>
- getTrueNASStorageDotStatus(props.row.resource, getDiskTemperatureThresholds);
+ getTrueNASStorageDotStatus(props.row.resource, getTrueNASDiskTemperatureThresholds);
const indicator = () => getSimpleStatusIndicator(displayStatus());
const name = () => resourceName(props.row.resource);
return (
@@ -391,10 +391,11 @@ export const TrueNASStorageTopologyTable: Component<{
kindFilter?: TrueNASStorageKindFilter;
onKindFilterChange?: (value: TrueNASStorageKindFilter) => void;
}> = (props) => {
- // Disk heat follows the user's alert disk temperature thresholds.
- const { getDiskTemperatureThresholds } = useAlertsActivation();
+ // Disk heat follows the thresholds each disk's temperature alert fires at:
+ // its own override, the TrueNAS-wide value, then its type's policy.
+ const { getTrueNASDiskTemperatureThresholds } = useAlertsActivation();
const rows = createMemo(() =>
- buildTrueNASStorageTopologyRows(props.resources, getDiskTemperatureThresholds),
+ buildTrueNASStorageTopologyRows(props.resources, getTrueNASDiskTemperatureThresholds),
);
const [internalKindFilter, setInternalKindFilter] = createSignal('all');
const kindFilter = () => props.kindFilter ?? internalKindFilter();
@@ -410,7 +411,7 @@ export const TrueNASStorageTopologyTable: Component<{
resources: scopedRows,
initialStatus: 'all' as TrueNASStorageStatusFilter,
filter: (rows, search, status) =>
- filterTrueNASStorageTopologyRows(rows, search, status, getDiskTemperatureThresholds),
+ filterTrueNASStorageTopologyRows(rows, search, status, getTrueNASDiskTemperatureThresholds),
});
const kindFilters = createMemo(() => [
{
@@ -427,7 +428,7 @@ export const TrueNASStorageTopologyTable: Component<{
filterTrueNASStorageTopologyRowsByKind(rows(), option.value),
tableState.search(),
tableState.status(),
- getDiskTemperatureThresholds,
+ getTrueNASDiskTemperatureThresholds,
).length,
})),
value: kindFilter,
@@ -448,7 +449,11 @@ export const TrueNASStorageTopologyTable: Component<{
descendingFirst: ['usage', 'disks', 'temp'],
});
const sortedRows = createMemo(() =>
- sortTrueNASStorageTopologyRows(tableState.filtered(), sort, getDiskTemperatureThresholds),
+ sortTrueNASStorageTopologyRows(
+ tableState.filtered(),
+ sort,
+ getTrueNASDiskTemperatureThresholds,
+ ),
);
return (
@@ -557,7 +562,7 @@ export const TrueNASStorageTopologyTable: Component<{
{(row) => {
const resource = () => row.resource;
const issue = createMemo(() =>
- getTrueNASStorageIssue(resource(), getDiskTemperatureThresholds),
+ getTrueNASStorageIssue(resource(), getTrueNASDiskTemperatureThresholds),
);
const detailRowId = () => drawer.detailRowId(resource());
const isExpanded = () => drawer.isExpanded(resource());
diff --git a/frontend-modern/src/features/truenas/__tests__/TrueNASStorageTopologyTable.test.tsx b/frontend-modern/src/features/truenas/__tests__/TrueNASStorageTopologyTable.test.tsx
index 79b1d8688f..1f29e4ddc9 100644
--- a/frontend-modern/src/features/truenas/__tests__/TrueNASStorageTopologyTable.test.tsx
+++ b/frontend-modern/src/features/truenas/__tests__/TrueNASStorageTopologyTable.test.tsx
@@ -381,4 +381,66 @@ describe('TrueNASStorageTopologyTable', () => {
eventBus.emit('org_switched', 'default');
}
});
+ it("follows a disk's own override and the TrueNAS-wide value its alert uses", async () => {
+ const getConfig = vi.spyOn(AlertsAPI, 'getConfig').mockResolvedValue({
+ enabled: true,
+ activationState: 'active',
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: {
+ nvme: { trigger: 70, clear: 65 },
+ sas: { trigger: 65, clear: 60 },
+ sata: { trigger: 55, clear: 50 },
+ },
+ truenasDiskDefaults: { temperature: { trigger: 62, clear: 57 } },
+ truenasDiskTemperatureByType: true,
+ overrides: { nvme1: { temperature: { trigger: 75, clear: 70 } } },
+ } as unknown as AlertConfig);
+ try {
+ const nvmeDisk = (id: string, temperature: number) =>
+ makeStorageResource({
+ id,
+ type: 'physical_disk',
+ name: id,
+ storage: undefined,
+ physicalDisk: {
+ devPath: `/dev/${id}`,
+ serial: `serial-${id}`,
+ diskType: 'nvme',
+ temperature,
+ },
+ });
+ const resources = [nvmeDisk('nvme0', 64), nvmeDisk('nvme1', 72)];
+ const { container } = render(() => (
+ }
+ emptyTitle="No storage"
+ emptyDescription="No storage"
+ />
+ ));
+ const health = (id: string) => {
+ const row = container.querySelector(`[data-truenas-storage-resource="${id}"]`);
+ if (!row) return 'missing';
+ return (
+ row
+ .querySelector('[data-truenas-storage-health]')
+ ?.getAttribute('data-truenas-storage-health') ?? 'healthy'
+ );
+ };
+ // Before the alert configuration loads, the factory NVMe trigger judges.
+ expect(health('nvme0')).toBe('healthy');
+ expect(health('nvme1')).toBe('attention');
+
+ await useAlertsActivation().refreshConfig();
+ // The TrueNAS-wide 62C now judges nvme0, and nvme1's own 75C override
+ // beats it, so the 72C disk that will never alert is not Attention.
+ await waitFor(() => expect(health('nvme0')).toBe('attention'));
+ expect(health('nvme1')).toBe('healthy');
+ expect(screen.getByRole('button', { name: /Attention/ })).toHaveTextContent('1');
+ } finally {
+ getConfig.mockRestore();
+ eventBus.emit('org_switched', 'default');
+ }
+ });
});
diff --git a/frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts b/frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts
index 3a88ea9308..e486181060 100644
--- a/frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts
+++ b/frontend-modern/src/features/truenas/__tests__/truenasPageModel.test.ts
@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest';
import type { RecoveryPoint } from '@/types/recovery';
+import type { AlertConfig } from '@/types/alerts';
import type { Resource } from '@/types/resource';
-import { resolveDiskTemperatureDisplayThresholds } from '@/utils/metricThresholds';
+import { resolveTrueNASDiskTemperatureDisplayThresholds } from '@/utils/metricThresholds';
import {
TRUENAS_TAB_SPECS,
buildTrueNASPageModel,
@@ -20,6 +21,7 @@ import {
filterTrueNASVMs,
getTrueNASPageTabSpecs,
getTrueNASResourceDisplayStatus,
+ getTrueNASStorageDotStatus,
getTrueNASStorageIssue,
mapTrueNASAppStatus,
mapTrueNASIncidentSeverity,
@@ -634,8 +636,8 @@ describe('truenasPageModel', () => {
it('flags a disk whose current reading reaches its alert trigger', () => {
// Disk risk carries no heat, so a hot disk stays `online`; the page judges
// it by the alert disk temperature thresholds, as Physical Disks does.
- const resolveFactory = (diskType: string) =>
- resolveDiskTemperatureDisplayThresholds(null, diskType);
+ const resolveFactory = (disk: Pick) =>
+ resolveTrueNASDiskTemperatureDisplayThresholds(null, disk.physicalDisk?.diskType, disk.id);
const hotDisk = makeResource({
id: 'disk-hot',
type: 'physical_disk',
@@ -707,6 +709,65 @@ describe('truenasPageModel', () => {
expect(mapTrueNASStorageStatus(retained, resolveFactory)).toBe('healthy');
});
+ it('judges disk heat by the override and TrueNAS-wide value its alert uses', () => {
+ const config = {
+ enabled: true,
+ guestDefaults: {},
+ nodeDefaults: {},
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: { nvme: { trigger: 70, clear: 65 }, sata: { trigger: 55, clear: 50 } },
+ storageDefault: { trigger: 85, clear: 80 },
+ overrides: { 'disk-raised': { temperature: { trigger: 75, clear: 70 } } },
+ } as AlertConfig;
+ const resolveWith = (cfg: AlertConfig) => (disk: Pick) =>
+ resolveTrueNASDiskTemperatureDisplayThresholds(cfg, disk.physicalDisk?.diskType, disk.id);
+ const nvmeDisk = (id: string, temperature: number) =>
+ makeResource({
+ id,
+ type: 'physical_disk',
+ name: id,
+ status: 'online',
+ physicalDisk: {
+ devPath: `/dev/${id}`,
+ serial: `serial-${id}`,
+ diskType: 'nvme',
+ health: 'UNKNOWN',
+ temperature,
+ },
+ });
+
+ // At 72C an NVMe is past its 70C type trigger, but the disk whose own
+ // override raised its alert to 75C will not alert, so it is not heat.
+ const plain = nvmeDisk('disk-plain', 72);
+ const raised = nvmeDisk('disk-raised', 72);
+ expect(mapTrueNASStorageStatus(plain, resolveWith(config))).toBe('attention');
+ expect(mapTrueNASStorageStatus(raised, resolveWith(config))).toBe('healthy');
+ expect(getTrueNASStorageIssue(raised, resolveWith(config))).toBeNull();
+ expect(
+ getTrueNASStorageIssue(nvmeDisk('disk-raised', 76), resolveWith(config))?.reasons,
+ ).toEqual(['Disk temperature is 76°C, at or above its 75°C alert threshold.']);
+
+ // A TrueNAS-wide 62C applies to every disk without its own override.
+ const trueNASWide = {
+ ...config,
+ truenasDiskDefaults: { temperature: { trigger: 62, clear: 57 } },
+ } as AlertConfig;
+ const warm = nvmeDisk('disk-warm', 64);
+ expect(mapTrueNASStorageStatus(warm, resolveWith(config))).toBe('healthy');
+ expect(getTrueNASStorageIssue(warm, resolveWith(trueNASWide))?.reasons).toEqual([
+ 'Disk temperature is 64°C, at or above its 62°C alert threshold.',
+ ]);
+ expect(mapTrueNASStorageStatus(raised, resolveWith(trueNASWide))).toBe('healthy');
+ // Switched off TrueNAS-wide, no disk without an override is heat.
+ const trueNASOff = {
+ ...config,
+ truenasDiskDefaults: { temperature: { trigger: 0, clear: 0 } },
+ } as AlertConfig;
+ expect(mapTrueNASStorageStatus(plain, resolveWith(trueNASOff))).toBe('healthy');
+ expect(getTrueNASStorageDotStatus(plain, resolveWith(trueNASOff))).toBe('online');
+ expect(getTrueNASStorageDotStatus(plain, resolveWith(config))).toBe('warning');
+ });
+
it('renders expected receive-side replication readonly posture as healthy', () => {
const replicatedDataset = makeResource({
id: 'dataset-replica',
diff --git a/frontend-modern/src/features/truenas/truenasPageModel.ts b/frontend-modern/src/features/truenas/truenasPageModel.ts
index 24e8edf79d..c07ce55ebb 100644
--- a/frontend-modern/src/features/truenas/truenasPageModel.ts
+++ b/frontend-modern/src/features/truenas/truenasPageModel.ts
@@ -728,16 +728,18 @@ export function mapTrueNASIncidentSeverity(
}
/**
- * Resolves a disk type's alert disk temperature thresholds: the alerts store's
- * `getDiskTemperatureThresholds`. Without one, disk heat is not judged.
+ * Resolves the temperature thresholds that judge a TrueNAS disk: the alerts
+ * store's `getTrueNASDiskTemperatureThresholds`, which applies the disk's own
+ * override, then the TrueNAS-wide value, then its type's policy, as the disk's
+ * temperature alert does. Without one, disk heat is not judged.
*/
export type TrueNASDiskTemperatureThresholdResolver = (
- diskType: string,
+ disk: Pick,
) => MetricDisplayThresholds | null;
-// Disk risk carries no heat, so a disk whose current reading has reached its
-// type's alert trigger is judged here, by the same policy as the Physical
-// Disks Running Hot verdict. Returns the heat reason, or null.
+// Disk risk carries no heat, so a disk whose current reading has reached the
+// trigger its temperature alert fires at is judged here. Returns the heat
+// reason, or null.
function getTrueNASDiskHeatSummary(
resource: Resource,
resolveDiskTemperatureThresholds?: TrueNASDiskTemperatureThresholdResolver,
@@ -745,7 +747,7 @@ function getTrueNASDiskHeatSummary(
const disk = resource.physicalDisk;
if (!resolveDiskTemperatureThresholds || resource.type !== 'physical_disk' || !disk) return null;
const temperature = disk.temperature ?? 0;
- const thresholds = resolveDiskTemperatureThresholds(disk.diskType || '');
+ const thresholds = resolveDiskTemperatureThresholds(resource);
return isPhysicalDiskRunningHot({ temperature, collection: disk.collection }, thresholds)
? getPhysicalDiskHeatSummary(temperature, thresholds)
: null;
diff --git a/frontend-modern/src/stores/alertsActivation.ts b/frontend-modern/src/stores/alertsActivation.ts
index 81fe367f23..677a186db2 100644
--- a/frontend-modern/src/stores/alertsActivation.ts
+++ b/frontend-modern/src/stores/alertsActivation.ts
@@ -2,6 +2,7 @@ import { createSignal } from 'solid-js';
import { AlertsAPI } from '@/api/alerts';
import type { AlertConfig, ActivationState as ActivationStateType } from '@/types/alerts';
import type { Alert } from '@/types/api';
+import type { Resource } from '@/types/resource';
import { setGlobalAlertsDetectionEnabled } from '@/utils/alertsActivation';
import { FACTORY_NODE_DEFAULTS } from '@/utils/alertThresholdDefaults';
import { logger } from '@/utils/logger';
@@ -10,6 +11,7 @@ import {
type DisplayMetricType,
resolveDiskTemperatureDisplayThresholds,
resolveMetricDisplayThresholds,
+ resolveTrueNASDiskTemperatureDisplayThresholds,
} from '@/utils/metricThresholds';
import { eventBus } from './events';
@@ -159,6 +161,17 @@ const getDiskTemperatureThresholds = (
return resolveDiskTemperatureDisplayThresholds(config(), diskType, resourceIds);
};
+// A TrueNAS disk's temperature thresholds, resolved by the tiers its
+// temperature alert uses: its own override, the TrueNAS-wide value, then the
+// per-type policy.
+const getTrueNASDiskTemperatureThresholds = (disk: Pick) => {
+ return resolveTrueNASDiskTemperatureDisplayThresholds(
+ config(),
+ disk.physicalDisk?.diskType,
+ disk.id,
+ );
+};
+
eventBus.on('org_switched', () => {
setConfig(null);
setGlobalAlertsDetectionEnabled(null);
@@ -185,6 +198,7 @@ export const useAlertsActivation = () => ({
getTemperatureThreshold,
getMetricThresholds,
getDiskTemperatureThresholds,
+ getTrueNASDiskTemperatureThresholds,
// Actions
refreshConfig,
diff --git a/frontend-modern/src/utils/__tests__/metricThresholds.test.ts b/frontend-modern/src/utils/__tests__/metricThresholds.test.ts
index 7099f3e0ad..079646a9df 100644
--- a/frontend-modern/src/utils/__tests__/metricThresholds.test.ts
+++ b/frontend-modern/src/utils/__tests__/metricThresholds.test.ts
@@ -11,6 +11,7 @@ import {
hasPulseRelaxedGuestTag,
resolveDiskTemperatureDisplayThresholds,
resolveMetricDisplayThresholds,
+ resolveTrueNASDiskTemperatureDisplayThresholds,
METRIC_THRESHOLDS,
type MetricType,
} from '@/utils/metricThresholds';
@@ -604,6 +605,122 @@ describe('metricThresholds', () => {
).toBeNull();
});
+ it('judges a TrueNAS disk by the tiers its temperature alert resolves', () => {
+ const config = {
+ enabled: true,
+ guestDefaults: {},
+ nodeDefaults: {},
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: {
+ nvme: { trigger: 70, clear: 65 },
+ sata: { trigger: 55, clear: 50 },
+ },
+ storageDefault: { trigger: 85, clear: 80 },
+ overrides: {
+ 'disk-raised': { temperature: { trigger: 75, clear: 70 } },
+ 'disk-off-value': { temperature: { trigger: 0, clear: 0 } },
+ 'disk-minus-one': { temperature: { trigger: -1, clear: 0 } },
+ 'disk-muted': { disabled: true },
+ 'disk-note-only': { note: 'spare shelf' },
+ 'disk-agent-key': { diskTemperature: { trigger: 80, clear: 75 } },
+ },
+ } as AlertConfig;
+ const resolve = (cfg: AlertConfig, diskType: string, id: string) =>
+ resolveTrueNASDiskTemperatureDisplayThresholds(cfg, diskType, id);
+
+ // Without a TrueNAS-wide value each disk follows its type's policy.
+ expect(resolve(config, 'nvme', 'disk-plain')).toEqual({ warning: 65, critical: 70 });
+ expect(resolve(config, 'SATA', 'disk-plain')).toEqual({ warning: 50, critical: 55 });
+ // An empty or unknown type, or one with no entry, takes the Disk Temp
+ // default, as the backend's per-type lookup does.
+ expect(resolve(config, '', 'disk-plain')).toEqual({ warning: 50, critical: 55 });
+ expect(resolve(config, 'scsi', 'disk-plain')).toEqual({ warning: 50, critical: 55 });
+ expect(resolve(config, 'sas', 'disk-plain')).toEqual({ warning: 50, critical: 55 });
+
+ // The disk's own temperature override beats the type, raising or
+ // switching it off; an override that mutes the disk's alerts is off too.
+ expect(resolve(config, 'nvme', 'disk-raised')).toEqual({ warning: 70, critical: 75 });
+ expect(resolve(config, 'nvme', 'disk-off-value')).toBeNull();
+ expect(resolve(config, 'nvme', 'disk-minus-one')).toBeNull();
+ expect(resolve(config, 'nvme', 'disk-muted')).toBeNull();
+ // An override without a temperature, or with only the agent disk
+ // temperature metric, leaves the default tiers.
+ expect(resolve(config, 'nvme', 'disk-note-only')).toEqual({ warning: 65, critical: 70 });
+ expect(resolve(config, 'nvme', 'disk-agent-key')).toEqual({ warning: 65, critical: 70 });
+ expect(resolveTrueNASDiskTemperatureDisplayThresholds(config, 'nvme', null)).toEqual({
+ warning: 65,
+ critical: 70,
+ });
+
+ // A TrueNAS-wide value replaces the type's policy for every disk, and a
+ // disk override still beats it. Zero switches it off.
+ const trueNASWide = {
+ ...config,
+ truenasDiskDefaults: { temperature: { trigger: 62, clear: 57 } },
+ } as AlertConfig;
+ expect(resolve(trueNASWide, 'nvme', 'disk-plain')).toEqual({ warning: 57, critical: 62 });
+ expect(resolve(trueNASWide, 'sata', 'disk-plain')).toEqual({ warning: 57, critical: 62 });
+ expect(resolve(trueNASWide, 'nvme', 'disk-raised')).toEqual({ warning: 70, critical: 75 });
+ const trueNASOff = {
+ ...config,
+ truenasDiskDefaults: { temperature: { trigger: 0, clear: 0 } },
+ } as AlertConfig;
+ expect(resolve(trueNASOff, 'nvme', 'disk-plain')).toBeNull();
+ expect(resolve(trueNASOff, 'nvme', 'disk-raised')).toEqual({ warning: 70, critical: 75 });
+ // An empty TrueNAS Disks entry is unset: the type's policy applies.
+ const trueNASUnset = { ...config, truenasDiskDefaults: {} } as AlertConfig;
+ expect(resolve(trueNASUnset, 'nvme', 'disk-plain')).toEqual({ warning: 65, critical: 70 });
+ // TrueNAS Disks defaults that switch alerts off skip every disk's
+ // evaluation, its own override included.
+ const trueNASMuted = {
+ ...config,
+ truenasDiskDefaults: { disabled: true, temperature: { trigger: 62, clear: 57 } },
+ } as AlertConfig;
+ expect(resolve(trueNASMuted, 'nvme', 'disk-plain')).toBeNull();
+ expect(resolve(trueNASMuted, 'nvme', 'disk-raised')).toBeNull();
+ });
+
+ it('reaches a TrueNAS disk only through the agent Disk Temp value', () => {
+ const config = {
+ enabled: true,
+ guestDefaults: {},
+ nodeDefaults: {},
+ agentDefaults: { diskTemperature: { trigger: 55, clear: 50 } },
+ diskTempByType: { nvme: { trigger: 70, clear: 65 } },
+ storageDefault: { trigger: 85, clear: 80 },
+ overrides: {},
+ } as AlertConfig;
+ // No host agent reports a TrueNAS disk, so the agent default's alert
+ // switch leaves it judged.
+ const agentsMuted = {
+ ...config,
+ agentDefaults: { ...config.agentDefaults, disabled: true },
+ } as AlertConfig;
+ expect(resolveTrueNASDiskTemperatureDisplayThresholds(agentsMuted, 'nvme', 'd')).toEqual({
+ warning: 65,
+ critical: 70,
+ });
+ // A switched-off Disk Temp default switches the per-type tier off, but a
+ // TrueNAS-wide value still applies.
+ const diskTempOff = {
+ ...config,
+ agentDefaults: { diskTemperature: { trigger: 0, clear: 0 } },
+ } as AlertConfig;
+ expect(resolveTrueNASDiskTemperatureDisplayThresholds(diskTempOff, 'nvme', 'd')).toBeNull();
+ expect(
+ resolveTrueNASDiskTemperatureDisplayThresholds(
+ { ...diskTempOff, truenasDiskDefaults: { temperature: { trigger: 60, clear: 55 } } },
+ 'nvme',
+ 'd',
+ ),
+ ).toEqual({ warning: 55, critical: 60 });
+ // Without config, the factory per-type triggers apply.
+ expect(resolveTrueNASDiskTemperatureDisplayThresholds(null, 'nvme', 'd')).toEqual({
+ warning: 65,
+ critical: 70,
+ });
+ });
+
it('falls back to seeded per-type disk temperature defaults without config', () => {
expect(resolveDiskTemperatureDisplayThresholds(null, 'nvme')).toEqual({
warning: 65,
diff --git a/frontend-modern/src/utils/metricThresholds.ts b/frontend-modern/src/utils/metricThresholds.ts
index 2e4f2a061f..5c97f9e278 100644
--- a/frontend-modern/src/utils/metricThresholds.ts
+++ b/frontend-modern/src/utils/metricThresholds.ts
@@ -350,6 +350,34 @@ export const resolveMetricDisplayThresholds = (
return resolveThreshold(value, getFallbackCritical(scope, metric), margin);
};
+// The disk temperature policy for a disk type, with no host override: the
+// type's diskTempByType entry, else the agent Disk Temp default. A switched-off
+// Disk Temp default switches every type off.
+const resolveDiskTemperatureTypePolicy = (
+ config: AlertConfig | null,
+ normalizedType: string,
+ margin: number,
+): MetricDisplayThresholds | null => {
+ const baseValue = getBaseThresholdValue(config?.agentDefaults, 'diskTemperature');
+ if (baseValue !== undefined && resolveThreshold(baseValue, undefined, margin) === null) {
+ return null;
+ }
+
+ const byTypeFallback = normalizedType ? FACTORY_DISK_TEMP_BY_TYPE[normalizedType] : undefined;
+ if (normalizedType) {
+ const byType = config?.diskTempByType?.[normalizedType];
+ if (isHysteresisThreshold(byType)) {
+ return resolveThreshold(byType, byTypeFallback, margin);
+ }
+ }
+
+ return resolveThreshold(
+ baseValue,
+ byTypeFallback ?? FACTORY_AGENT_DEFAULTS.diskTemperature,
+ margin,
+ );
+};
+
/**
* Resolve display thresholds for a physical disk's SMART temperature.
* Mirrors the backend precedence: an explicit diskTemperature override on the
@@ -378,24 +406,42 @@ export const resolveDiskTemperatureDisplayThresholds = (
return resolveThreshold(overrideValue, FACTORY_AGENT_DEFAULTS.diskTemperature, margin);
}
- const baseValue = getBaseThresholdValue(config?.agentDefaults, 'diskTemperature');
- if (baseValue !== undefined && resolveThreshold(baseValue, undefined, margin) === null) {
+ return resolveDiskTemperatureTypePolicy(config, normalizedType, margin);
+};
+
+/**
+ * Resolve display thresholds for a TrueNAS disk's temperature by the tiers its
+ * temperature alert resolves (backend `trueNASDiskTemperatureDefaultNoLock`
+ * under `effectiveAlertPolicyNoLock`): the disk's own temperature override,
+ * stored under its resource ID, then a TrueNAS-wide value saved under TrueNAS
+ * Disks, then the disk temperature policy for its type. An override or a
+ * TrueNAS Disks default that switches alerts off, or a value of 0 at either
+ * explicit tier, means off (null). No host agent reports a TrueNAS disk, so
+ * host overrides and the agent default's alert switch do not reach it; only
+ * the agent Disk Temp value does, through the per-type tier.
+ */
+export const resolveTrueNASDiskTemperatureDisplayThresholds = (
+ config: AlertConfig | null,
+ diskType: string | null | undefined,
+ resourceId: string | null | undefined,
+): MetricDisplayThresholds | null => {
+ const margin = normalizeMargin(config?.hysteresisMargin);
+
+ const override = findOverride(config?.overrides, resourceId ?? undefined);
+ if (override?.disabled || config?.truenasDiskDefaults?.disabled === true) {
return null;
}
-
- const byTypeFallback = normalizedType ? FACTORY_DISK_TEMP_BY_TYPE[normalizedType] : undefined;
- if (normalizedType) {
- const byType = config?.diskTempByType?.[normalizedType];
- if (isHysteresisThreshold(byType)) {
- return resolveThreshold(byType, byTypeFallback, margin);
- }
+ const overrideValue = getOverrideValue(override, 'temperature');
+ if (overrideValue !== undefined) {
+ return resolveThreshold(overrideValue, undefined, margin);
}
- return resolveThreshold(
- baseValue,
- byTypeFallback ?? FACTORY_AGENT_DEFAULTS.diskTemperature,
- margin,
- );
+ const trueNASWideValue = getBaseThresholdValue(config?.truenasDiskDefaults, 'temperature');
+ if (trueNASWideValue !== undefined) {
+ return resolveThreshold(trueNASWideValue, undefined, margin);
+ }
+
+ return resolveDiskTemperatureTypePolicy(config, (diskType ?? '').trim().toLowerCase(), margin);
};
const getFallbackSeverityThresholds = (metric: DisplayMetricType): MetricDisplayThresholds => {
diff --git a/scripts/tests/test_truenas_docs.py b/scripts/tests/test_truenas_docs.py
index 7de736534f..34ea5ade28 100644
--- a/scripts/tests/test_truenas_docs.py
+++ b/scripts/tests/test_truenas_docs.py
@@ -99,8 +99,9 @@ class TrueNASDocsTest(unittest.TestCase):
def test_disk_display_mismatch_has_a_passive_private_reporting_route(self):
prose = " ".join(self.disk_health_section().split())
for boundary in (
- "**Storage display limitation:**", "inherited agent/by-type policy",
- "not the TrueNAS-wide or per-disk temperature override",
+ "**Storage temperature judgement:**", "inherited agent/by-type policy",
+ "saved per-disk temperature override, then the TrueNAS-wide default",
+ "reading visible but unjudged", "last known reading is never current heat",
"not proof that your saved rule failed", "same disk, observation time and saved threshold",
"existing TrueNAS reading and SMART state", "do not raise thresholds",
"run new SMART tests", "force a probe or restart", "only the disk type",
@@ -109,8 +110,15 @@ class TrueNASDocsTest(unittest.TestCase):
):
with self.subTest(boundary=boundary):
self.assertIn(boundary, prose)
+ self.assertNotIn("Storage display limitation", prose)
config = ROOT / "docs/CONFIGURATION.md"
- self.assertIn("TRUENAS.md#disk-temperature-and-health", config.read_text())
+ config_prose = " ".join(config.read_text().split())
+ self.assertIn("TRUENAS.md#disk-temperature-and-health", config_prose)
+ self.assertIn("per-disk override, then the TrueNAS-wide default, then the inherited by-type policy",
+ config_prose)
+ self.assertIn("Off leaves the temperature visible but unjudged", config_prose)
+ self.assertIn("last known readings are not current heat", config_prose)
+ self.assertNotIn("do not yet follow", config_prose)
self.assertEqual(config.read_bytes(),
(ROOT / "frontend-modern/public/docs/CONFIGURATION.md").read_bytes())