Retain independently reported disk temperature on v6.4 (#2406)
Some checks are pending
Core E2E Tests / Validate E2E tier selection (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Provider pair Docker acceptance (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Blocked by required conditions
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions

## What changed and why
Disk details on v6.4 now retain an independently reported temperature
when optional extended SMART attributes are absent. Units and configured
thresholds remain respected; missing counters are not invented, and
unavailable-details status remains truthful.

The predecessor helper hid a supplied 42°C reading. This is a repair to
the existing disk-details surface, not a new collection capability or a
claim that every PBS/TrueNAS panel is fixed.

## Included source
- Reviewed release-line candidate `20261002T175724Z-release-line`:
`2a954ee3b8`, adapting main repair
`442717f409`.
- Complete unpublished range:
`5562d97f76c0277a35253f316e098f625775c12d..913ca30a17437e3b25d6d477eebaf5d6218070a5`.
- Additive merge `913ca30a17` incorporates
the upstream landing of History PR #2402 without rewriting reviewed
ancestry. Its complete tree exactly equals the reviewed candidate tree;
there was no content conflict or correction.

## Validation and limits
- Recorded reviewed proof `source-i24fweeo`: 2,785 connected tests,
typecheck, lint, production/embedded builds and budgets passed. Separate
exact-path boundary proof passed 11 tests. Both receipts retain complete
output and guest shutdown. Earlier full History suite proof remains
evidence for unchanged History content.
- Recorded Chromium/WebKit predecessor controls reproduce hidden
temperature. Final line browser checks pass ten desktop/phone
observations covering reported, hot, Fahrenheit, missing and
restored-counter states; screenshots were inspected during review.
- Publication provenance, range whitespace and the full-range
browser-verification guard passed. The final tree equals the reviewed,
source-proved candidate, and the runtime hash matches its browser
receipt, so sufficient proof was not repeated.
- Earlier admission and selector errors remain recorded, not passing
evidence. Required exact-head CI must pass before landing; native
panels, installed acceptance and containing release qualification remain
separate.

This is later-checkpoint source for the next eligible patch, normally
v6.4.7. The frozen v6.4.6 candidate is unchanged; this PR is not release
publication or promotion.


[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
This commit is contained in:
pulse-triage[bot] 2026-10-02 19:32:25 +00:00 • committed by GitHub
commit a27db15872
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 230 additions and 33 deletions

View file

@ -21,6 +21,22 @@
## Purpose
### Independent physical disk temperature
Disk Overview displays a finite positive reported temperature even when the
optional extended SMART object is missing or empty. Temperature formatting and
configured per-disk-type thresholds remain unchanged; absent counters are not
invented. When no detail readings or collection messages remain, the existing
accessible unavailable-details message is shown. This presentation does not
establish fresh collection, SMART health or native appliance acceptance.
Verification: `DiskDetail.test.tsx` covers same-identity snapshot replacement
through temperature-only, hot, missing and extended-SMART states;
`diskDetailPresentation.branchcov0724pm.test.ts` covers disk types, invalid
readings, units and configured thresholds. The production detail fixture in
`frontend-modern/browser-tests/disk-temperature.cjs` checks desktop and phone
presentation with synthetic snapshots, including the predecessor's omission.
### Pool detail capacity observations
The existing pool drawer's Configuration rows preserve missing capacity as

View file

@ -0,0 +1,70 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { chromium, webkit } = require('playwright');
(async () => {
const root = '/workspace/frontend-modern';
process.chdir(root);
const artifacts = path.join(root, 'node_modules/disk-temperature-proof');
fs.mkdirSync(artifacts, { recursive: true });
const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js'));
const server = await createServer({ root, configFile: path.join(root, 'vite.config.ts'), cacheDir: fs.mkdtempSync(path.join(artifacts, 'vite-')), server: { host: '127.0.0.1', port: 5225, strictPort: true } });
let browser;
const observations = [];
const parent = process.argv.includes('--parent');
try {
await server.listen();
for (const [name, engine, width, height] of [['chromium', chromium, 1365, 900], ['webkit', webkit, 390, 844]]) {
browser = await engine.launch(name === 'chromium' ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } : { headless: true });
const page = await browser.newPage({ viewport: { width, height }, hasTouch: name === 'webkit', isMobile: name === 'webkit' });
const errors = [];
page.on('pageerror', e => errors.push(e.message));
await page.route('**/*', route => {
const url = new URL(route.request().url());
if (url.origin !== 'http://127.0.0.1:5225') return route.abort();
if (!url.pathname.startsWith('/api/')) return route.continue();
return route.fulfill({json: url.pathname === '/api/license/runtime-capabilities' ? {capabilities: [], limits: [], max_history_days: 7, hosted_mode: false, runtime: {build:'community'}, blocked_capabilities: []} : {data: [], enabled: false}});
});
await page.goto('http://127.0.0.1:5225/browser-tests/disk-temperature.html');
if (name === 'webkit') await page.evaluate(() => document.documentElement.classList.add('dark'));
const fallback = page.getByText('Detailed SMART attributes are not available for this disk.', {exact:true});
const value = text => page.getByText(text, {exact:true});
async function state(phase, expected) {
await value(expected).waitFor();
const dimensions = await page.evaluate(() => ({scroll:document.documentElement.scrollWidth, width:innerWidth}));
assert.ok(dimensions.scroll <= dimensions.width + 1);
await page.screenshot({path:path.join(artifacts, `${parent?'parent-':''}${name}-${phase}.png`),fullPage:true});
observations.push({name, version:browser.version(),phase,expected,dimensions});
}
if (parent) {
await state('temperature-hidden', 'Detailed SMART attributes are not available for this disk.');
assert.equal(await value('42°C').count(),0);
} else {
await state('temperature-only','42°C');
assert.equal(await fallback.count(),0);
assert.equal(await value('Power-On Time').count(),0);
assert.match(await value('42°C').getAttribute('class'), /text-green-600/);
await page.getByRole('button',{name:'Hot reading'}).click();
await state('hot','65°C');
assert.match(await value('65°C').getAttribute('class'), /text-red-600/);
await page.getByRole('button',{name:'Fahrenheit',exact:true}).click();
await state('fahrenheit','149°F');
assert.match(await value('149°F').getAttribute('class'), /text-red-600/);
await page.getByRole('button',{name:'No reading',exact:true}).click();
await state('missing','Detailed SMART attributes are not available for this disk.');
assert.equal(await value('Temperature').count(),0);
assert.equal(await fallback.getAttribute('role'),'status');
await page.getByRole('button',{name:'Extended SMART',exact:true}).click();
await state('extended','42°C');
await value('4 days').waitFor();
await value('Reallocated Sectors').waitFor();
assert.equal(await value('0').count(),1);
assert.equal(await value('Temperature').count(),1);
}
assert.deepEqual(errors,[]);
await browser.close(); browser=null;
}
fs.writeFileSync(path.join(artifacts,parent?'parent.json':'result.json'),JSON.stringify({playwright:require('playwright/package.json').version,observations},null,2));
console.log(JSON.stringify({result:'passed',states:observations.length,parent}));
} finally { if(browser) await browser.close(); await server.close(); }
})().catch(e=>{console.error(e);process.exitCode=1});

View file

@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Disk temperature verification</title></head><body><div id="root"></div><script type="module" src="./disk-temperature.tsx"></script></body></html>

View file

@ -0,0 +1,57 @@
import { createSignal } from 'solid-js';
import { render } from 'solid-js/web';
import { DiskDetail } from '../src/components/Storage/DiskDetail';
import type { Resource } from '../src/types/resource';
import { temperatureStore } from '../src/utils/temperature';
import '../src/index.css';
const Fixture = () => {
const initial = {
id: 'disk-fixture',
type: 'physical_disk',
name: 'Archive HDD',
displayName: 'Archive HDD',
platformType: 'proxmox-pve',
platformId: 'fixture',
sourceType: 'api',
status: 'online',
lastSeen: Date.now(),
identity: { hostname: 'storage-host' },
physicalDisk: { devPath: '/dev/sda', model: 'Archive HDD', diskType: 'hdd', temperature: 42 },
} as Resource;
const [disk, setDisk] = createSignal(initial);
const reading = (
temperature: number,
smart?: { powerOnHours: number; reallocatedSectors: number },
) => {
temperatureStore.setUnit('celsius');
setDisk({ ...initial, physicalDisk: { ...initial.physicalDisk!, temperature, smart } });
};
return (
<main class="min-h-screen space-y-4 bg-surface p-4 text-base-content">
<h1>Physical disk Overview</h1>
<p>Synthetic disk snapshots; production detail component.</p>
<div class="flex flex-wrap gap-2">
<button class="min-h-11 border px-3" onClick={() => reading(65)}>
Hot reading
</button>
<button class="min-h-11 border px-3" onClick={() => temperatureStore.setUnit('fahrenheit')}>
Fahrenheit
</button>
<button class="min-h-11 border px-3" onClick={() => reading(0)}>
No reading
</button>
<button
class="min-h-11 border px-3"
onClick={() => reading(42, { powerOnHours: 100, reallocatedSectors: 0 })}
>
Extended SMART
</button>
</div>
<section class="max-w-3xl">
<DiskDetail disk={disk()} nodes={[]} />
</section>
</main>
);
};
render(() => <Fixture />, document.getElementById('root')!);

View file

@ -1,23 +1,16 @@
{
"version": 1,
"base_sha": "73ca5f336e4ee3f9b234969f79cd1cee1a52737a",
"verified_at": "2026-10-02T16:40:31.728157Z",
"base_sha": "a5b531998f80d4bfdd7df06d4b37802e6cc054eb",
"verified_at": "2026-10-02T18:07:50Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/HistoryChart.tsx",
"frontend-modern/src/components/shared/HistoryChartOverlay.tsx",
"frontend-modern/src/components/shared/historyChartModel.ts",
"frontend-modern/src/components/shared/useHistoryChartState.ts"
"frontend-modern/src/features/storageBackups/diskDetailPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/components/shared/HistoryChart.tsx": "a2ab0198a705e66ce9bea222066078709c8334fc342415916b8c179987b139e2",
"frontend-modern/src/components/shared/HistoryChartOverlay.tsx": "4192731461a19ba7c33185c2e616c5023dfef58d93e3893cd3c4fceb58598391",
"frontend-modern/src/components/shared/historyChartModel.ts": "bd9e8954ef7e274a1eb026752d304556df303f612c4485369f8c90a7183cc751",
"frontend-modern/src/components/shared/useHistoryChartState.ts": "8bebaa331f1e2dfc57587f517628bed1b0c4f7d9c9466e21597e7f9f7a4d7137"
"frontend-modern/src/features/storageBackups/diskDetailPresentation.ts": "035614af7ddee52c5ee56099b4f447b0137afd9dccf07be9981d44a486eb19a4"
},
"routes": [
"/browser-tests/history-keyboard.html",
"/browser-tests/history-status.html"
"/browser-tests/disk-temperature.html"
],
"viewports": [
{
@ -30,24 +23,20 @@
}
],
"states": [
"Empty successful result, loaded readings, failed refresh, pending recovery, recovered empty result, failed refresh of empty result, changed-target loading and initial failure",
"Chromium desktop light and WebKit phone dark; screenshots inspected, no horizontal overflow or page errors; synthetic transport, not native collection",
"Keyboard focus, Left/Right, Home/End, Escape, bounds, focus exit and empty data; grouped tooltip geometry and polite active-chart-only announcements"
"Reported temperature without optional extended SMART attributes",
"Hot replacement reading with configured severity",
"Fahrenheit display of hot reading",
"Missing reading with accessible unavailable-details status",
"Extended SMART counters restored without invented absent values"
],
"interactions": [
"Advance polling clock without altering production timers; fail and recover HTTP responses; switch pool target",
"Parent control reproduces misleading empty promise and silent failed refresh; final warning retains data and clears only on recovery or selection replacement"
"Click Hot reading, Fahrenheit, No reading and Extended SMART synthetic snapshot controls; inspect production DiskDetail Overview on Chromium and WebKit",
"Confirm 42 C then 65 C then 149 F, severity classes, counter absence/presence, no horizontal overflow or uncaught page errors",
"Inspect all ten retained screenshots; exact predecessor-helper controls reproduce hidden 42 C on both viewports"
],
"command": "pulse-worker-browser frontend-modern/browser-tests/history-keyboard.cjs && pulse-worker-browser frontend-modern/browser-tests/history-status.cjs",
"browser_versions": {
"playwright": "1.56.1",
"chromium": "141.0.7390.37",
"webkit": "26.0"
},
"artifacts": [
"/var/lib/pulse-maintainer/worker-outputs/release-line-3t375h3z/keyboard64-parent.json",
"/var/lib/pulse-maintainer/worker-outputs/release-line-3t375h3z/keyboard64-result.json",
"/var/lib/pulse-maintainer/worker-outputs/release-line-3t375h3z/status64-parent.json",
"/var/lib/pulse-maintainer/worker-outputs/release-line-3t375h3z/status64-result.json"
"playwright": "1.56.1",
"browser_versions": [
"chromium 141.0.7390.37",
"webkit 26.0"
]
}

View file

@ -1,5 +1,5 @@
import { fireEvent, render, screen } from '@solidjs/testing-library';
import type { JSX } from 'solid-js';
import { createSignal, type JSX } from 'solid-js';
import { describe, expect, it, vi } from 'vitest';
import { DiskDetail } from '@/components/Storage/DiskDetail';
import type { Resource } from '@/types/resource';
@ -107,9 +107,38 @@ describe('DiskDetail', () => {
expect(screen.queryByText(/:diskwrite:/)).not.toBeInTheDocument();
});
it('shows an explicit overview fallback when SMART details are unavailable', () => {
it('keeps standalone temperature readings visible through snapshot replacement', () => {
const initial = buildDisk();
delete initial.physicalDisk!.smart;
const [disk, setDisk] = createSignal(initial);
render(() => <DiskDetail disk={disk()} nodes={[]} />);
expect(screen.getByText('42°C')).toBeInTheDocument();
expect(screen.queryByText('Power-On Time')).not.toBeInTheDocument();
expect(
screen.queryByText('Detailed SMART attributes are not available for this disk.'),
).not.toBeInTheDocument();
setDisk({ ...initial, physicalDisk: { ...initial.physicalDisk!, temperature: 65 } });
expect(screen.getByText('65°C')).toHaveClass('text-red-600');
expect(screen.queryByText('42°C')).not.toBeInTheDocument();
setDisk({ ...initial, physicalDisk: { ...initial.physicalDisk!, temperature: 0 } });
expect(screen.queryByText('Temperature')).not.toBeInTheDocument();
expect(
screen.getByText('Detailed SMART attributes are not available for this disk.'),
).toHaveAttribute('role', 'status');
setDisk(buildDisk());
expect(screen.getByText('42°C')).toBeInTheDocument();
expect(screen.getByText('Power-On Time')).toBeInTheDocument();
expect(screen.getByText('Reallocated Sectors')).toBeInTheDocument();
});
it('shows an explicit overview fallback when no detail readings are available', () => {
const disk = buildDisk();
delete disk.physicalDisk!.smart;
delete disk.physicalDisk!.temperature;
render(() => <DiskDetail disk={disk} nodes={[]} />);

View file

@ -1,4 +1,5 @@
import { describe, expect, it } from 'vitest';
import { temperatureStore } from '@/utils/temperature';
import type { PhysicalDiskPresentationData } from '@/features/storageBackups/diskPresentation';
import {
getDiskDetailAttributeCards,
@ -53,6 +54,40 @@ describe('diskDetailPresentation.branchcov0724pm', () => {
});
});
describe('independently reported temperature', () => {
it.each(['hdd', 'ssd', 'nvme'])('shows %s temperature without extended SMART data', (type) => {
expect(getDiskDetailAttributeCards(makeDiskData({ type, temperature: 42 }))).toEqual([
{ label: 'Temperature', value: '42°C', ok: true },
]);
});
it.each([0, -1, NaN, Infinity, -Infinity])('does not invent a card for %s', (temperature) => {
expect(getDiskDetailAttributeCards(makeDiskData({ temperature }))).toEqual([]);
});
it('keeps configured thresholds and display units independent of attribute presence', () => {
const disk = makeDiskData({ type: 'nvme', temperature: 65 });
expect(getDiskDetailAttributeCards(disk, { warning: 70, critical: 80 })).toEqual([
{ label: 'Temperature', value: '65°C', ok: true },
]);
temperatureStore.setUnit('fahrenheit');
try {
expect(getDiskDetailAttributeCards(disk, { warning: 50, critical: 60 })).toEqual([
{ label: 'Temperature', value: '149°F', ok: false },
]);
} finally {
temperatureStore.setUnit('celsius');
}
});
it('treats empty and missing SMART data alike without inventing counters', () => {
const disk = makeDiskData({ temperature: 42 });
expect(getDiskDetailAttributeCards({ ...disk, smartAttributes: {} })).toEqual(
getDiskDetailAttributeCards(disk),
);
});
});
describe('getDiskDetailAttributeCards SATA offlineUncorrectable arm (L129)', () => {
it('emits an Offline Uncorrectable card marked ok when the count is zero', () => {
const cards = getDiskDetailAttributeCards(

View file

@ -82,8 +82,8 @@ export function getDiskDetailAttributeCards(
disk: PhysicalDiskPresentationData,
diskTempThresholds?: MetricDisplayThresholds | null,
): DiskDetailAttributeCard[] {
const attrs = disk.smartAttributes;
if (!attrs) return [];
// Temperature is reported independently of optional extended SMART attributes.
const attrs = disk.smartAttributes ?? {};
const cards: DiskDetailAttributeCard[] = [];
const isNvme = disk.type?.toLowerCase() === 'nvme';
@ -96,7 +96,7 @@ export function getDiskDetailAttributeCards(
});
}
if (disk.temperature > 0) {
if (Number.isFinite(disk.temperature) && disk.temperature > 0) {
cards.push({
label: 'Temperature',
value: formatTemperature(disk.temperature),