Honour temperature preferences in platform tables

Use the existing reactive display formatter for Docker and TrueNAS readings and keep the TrueNAS phone unit heading and full value legible. Preserve Celsius observations, thresholds, sorting and missing-reading handling. Regressions cover live preference changes, round trips and invalid readings; actual General settings and production HTTP/table consumers are exercised at desktop and 320px.

Contract-Neutral: Apply the existing temperature display preference and preserve missing-reading semantics; no API, stored Celsius value, threshold, identity or platform scope change.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-24 00:03:22 +01:00
parent acd353f30f
commit d450e2894a
6 changed files with 141 additions and 6 deletions
@@ -0,0 +1,42 @@
{
"version": 1,
"base_sha": "acd353f30fff617ed69ef4473f86a57c59bc5c65",
"verified_at": "2026-10-23T22:59:53Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx",
"frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx"
],
"content_sha256": {
"frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "61af45450e661037e4fe38b773ce6743196c6b3595bc208bfcd5fda6d1411587",
"frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "8ccd23a7923f657843987931aa0e9cdc8e5b528e637493e21a5486caa9e26142"
},
"routes": [
"/browser-proof/index.html?kind=docker",
"/browser-proof/index.html?kind=nas"
],
"viewports": [
{
"width": 1440,
"height": 1000
},
{
"width": 320,
"height": 900
}
],
"states": [
"Evidence-only shell consumes the actual GeneralSettingsPanel, temperatureStore, useUnifiedResources HTTP transform, DockerHostsTable, TrueNASSystemsTable, Docker Details and production CSS. Synthetic GET responses supply 40 Celsius and absent/sentinel readings; unrelated settings handlers are inert. This is not native-server, deployment or full-build acceptance.",
"Chromium 141.0.7390.37 desktop and WebKit 26.0 narrow; Playwright 1.56.1 matches the integration lock. Both platforms switch live from 40.0°C to 104.0°F, restore Fahrenheit after a fixture reload, and switch back to Celsius. Missing readings remain absent, not 32°F.",
"The narrow Docker temperature column remains intentionally hidden; its transformed DOM value follows the preference and actual expanded Details visibly shows 104°F. Desktop table and Details agree. Existing chart Celsius labels are outside this table repair.",
"TrueNAS compact heading follows °F/°C. Final 320px temperature and capacity cells are each 53px with no content overflow; source pixels show both full values. The intermediate 15% temperature cell clipped the reading; a 25% trial clipped Capacity and was not retained.",
"Successful unchanged Docker scenarios are reused by exact imported-file hashes; only TrueNASSystemsTable changed during the subsequent width correction. Final TrueNAS desktop and narrow scenarios pass separately. Earlier harness selector/click failures and the adverse width trials remain evidence, not aggregate passes.",
"Ten accepted captures of the real preference controls, tables, Docker Details and final TrueNAS Celsius/Fahrenheit states were opened with the native image tool. Fixture, driver, build scripts, logs and captures are retained with lane evidence, not committed as source."
],
"interactions": [
"Clicked the production General temperature selector while the table remained mounted; checked the preference in localStorage.",
"Opened desktop Docker Details through its disclosure; tapped the narrow host row and opened its real Details summary. No forced clicks, CSS overrides or modal bypass.",
"Reloaded only the safe temperature fixture to check cold preference initialisation; no stopped Docs Back/reload or production operation.",
"Inspected desktop and 320px actual pixels; measured temperature and capacity fit after the TrueNAS width correction."
]
}
@@ -5,6 +5,7 @@ import { createSignal } from 'solid-js';
import type { Alert, Disk } from '@/types/api';
import type { Resource } from '@/types/resource';
import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
import { temperatureStore } from '@/utils/temperature';
import { DockerHostsTable } from '../DockerHostsTable';
import dockerHostsTableSource from '../DockerHostsTable.tsx?raw';
@@ -127,11 +128,35 @@ const makeDockerHost = (overrides: Partial<Resource> = {}): Resource => ({
afterEach(() => {
cleanup();
temperatureStore.setUnit('celsius');
vi.clearAllMocks();
for (const key of Object.keys(activeAlertsMock)) delete activeAlertsMock[key];
});
describe('DockerHostsTable', () => {
it('updates the host temperature when the display preference changes without changing readings', async () => {
temperatureStore.setUnit('celsius');
const host = makeDockerHost({ temperature: 40 });
render(() => (
<DockerHostsTable
resources={[host]}
emptyIcon={<span />}
emptyTitle="No Docker hosts"
emptyDescription="No hosts"
showToolbar={false}
/>
));
expect(screen.getByText('40.0°C')).toBeInTheDocument();
temperatureStore.setUnit('fahrenheit');
expect(await screen.findByText('104.0°F')).toBeInTheDocument();
expect(screen.queryByText('40.0°C')).not.toBeInTheDocument();
expect(host.temperature).toBe(40);
expect(localStorage.getItem('temperatureUnit')).toBe('fahrenheit');
temperatureStore.setUnit('celsius');
expect(await screen.findByText('40.0°C')).toBeInTheDocument();
});
it('keeps five readable host fields below 360 pixels without wrapped metadata rows', () => {
expect(dockerHostsTableSource).toContain('platform-table-mobile-w-30');
expect(dockerHostsTableSource).toContain('platform-table-narrow-hidden');
@@ -0,0 +1,31 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it } from 'vitest';
import { temperatureStore } from '@/utils/temperature';
import { PlatformTableTemperatureValue } from '../sharedPlatformPage';
afterEach(() => {
cleanup();
temperatureStore.setUnit('celsius');
});
describe('PlatformTableTemperatureValue', () => {
it('uses the live preference with one decimal place', async () => {
temperatureStore.setUnit('fahrenheit');
render(() => <PlatformTableTemperatureValue value={37.5} />);
expect(screen.getByText('99.5°F')).toBeInTheDocument();
temperatureStore.setUnit('celsius');
expect(await screen.findByText('37.5°C')).toBeInTheDocument();
});
it.each([null, undefined, NaN, Infinity, -Infinity, 0, -1])(
'keeps the existing absent/sentinel reading %s unknown in either unit',
(value) => {
temperatureStore.setUnit('celsius');
render(() => <PlatformTableTemperatureValue value={value} emptyText="Not reported" />);
expect(screen.getByText('Not reported')).toBeInTheDocument();
temperatureStore.setUnit('fahrenheit');
expect(screen.getByText('Not reported')).toBeInTheDocument();
expect(screen.queryByText(/°[CF]/)).not.toBeInTheDocument();
},
);
});
@@ -46,6 +46,7 @@ import {
type SearchTermSplit,
} from '@/utils/searchQuery';
import { asTrimmedString } from '@/utils/stringUtils';
import { formatTemperature } from '@/utils/temperature';
import { formatVmwareClusterServices } from '@/utils/vmwareDisplay';
import { getPlatformColumnAlign, type PlatformTableColumnKind } from './columnAlignment';
import {
@@ -1027,7 +1028,8 @@ export const formatPlatformTablePercentValue = (
return `${clamped.toFixed(1)}%`;
};
const formatOneDecimalCelsius = (value: number): string => `${value.toFixed(1)}°C`;
const formatOneDecimalTemperature = (value: number): string =>
formatTemperature(value, { decimals: 1 });
export function PlatformTablePercentValue(props: {
value: number | null | undefined;
@@ -1066,7 +1068,7 @@ export function PlatformTableTemperatureValue(props: {
<PlatformTableNumberValue
value={finitePositiveValue()}
emptyText={props.emptyText}
format={formatOneDecimalCelsius}
format={formatOneDecimalTemperature}
/>
);
}
@@ -9,6 +9,7 @@ import { useWebSocket } from '@/contexts/appRuntime';
import { useAlertsActivation } from '@/stores/alertsActivation';
import { unifiedPlatformOverrideIdCandidates } from '@/features/alerts/alertOverridesModel';
import { asTrimmedString } from '@/utils/stringUtils';
import { getTemperatureSymbol } from '@/utils/temperature';
import { buildMetricKeyForUnifiedResource } from '@/utils/metricsKeys';
import {
PlatformWindowedRows,
@@ -236,7 +237,7 @@ export const TrueNASSystemsTable: Component<{
kind="metric-bar"
sort={sort}
sortKey="memory"
class="platform-table-mobile-w-20 md:w-[10%]"
class="platform-table-mobile-w-15 md:w-[10%]"
>
<PlatformResponsiveTableLabel compact="Mem" full="Memory" />
</PlatformSortableTableHead>
@@ -252,9 +253,9 @@ export const TrueNASSystemsTable: Component<{
kind="numeric-value"
sort={sort}
sortKey="temp"
class="platform-table-mobile-w-15 md:w-[6%]"
class="platform-table-mobile-w-20 md:w-[6%]"
>
<PlatformResponsiveTableLabel compact="°C" full="Temp" />
<PlatformResponsiveTableLabel compact={getTemperatureSymbol()} full="Temp" />
</PlatformSortableTableHead>
<PlatformSortableTableHead
kind="text"
@@ -23,6 +23,7 @@ vi.mock('@/stores/alertsActivation', () => ({
import { TrueNASSystemsTable } from '@/features/truenas/TrueNASSystemsTable';
import type { Resource } from '@/types/resource';
import { temperatureStore } from '@/utils/temperature';
const makeSystem = (overrides: Partial<Resource> & Pick<Resource, 'id'>): Resource =>
({
@@ -45,10 +46,41 @@ const makeSystem = (overrides: Partial<Resource> & Pick<Resource, 'id'>): Resour
afterEach(() => {
cleanup();
temperatureStore.setUnit('celsius');
activeAlertsRef.current = {};
});
describe('TrueNASSystemsTable', () => {
it('updates both the reading and compact temperature heading with the display preference', async () => {
temperatureStore.setUnit('celsius');
const system = makeSystem({ id: 'nas-temperature', temperature: 40 });
const { container } = render(() => (
<TrueNASSystemsTable
systems={[system]}
scope={[system]}
emptyIcon={<span />}
emptyTitle="No systems"
emptyDescription="No systems"
showToolbar={false}
/>
));
const temperatureHeader = () =>
[...container.querySelectorAll('thead th')].find((header) =>
header.textContent?.includes('Temp'),
);
expect(screen.getByText('40.0°C')).toBeInTheDocument();
expect(temperatureHeader()).toHaveTextContent('°C');
temperatureStore.setUnit('fahrenheit');
expect(await screen.findByText('104.0°F')).toBeInTheDocument();
expect(temperatureHeader()).toHaveTextContent('°F');
expect(temperatureHeader()).not.toHaveTextContent('°C');
expect(system.temperature).toBe(40);
temperatureStore.setUnit('celsius');
expect(await screen.findByText('40.0°C')).toBeInTheDocument();
expect(temperatureHeader()).toHaveTextContent('°C');
});
it('keeps capacity and temperature in the phone column set', () => {
const { container } = render(() => (
<TrueNASSystemsTable
@@ -68,8 +100,10 @@ describe('TrueNASSystemsTable', () => {
expect(headers.find((header) => header.textContent?.includes('Capacity'))).toHaveClass(
'platform-table-mobile-w-20',
);
// A three-digit Fahrenheit reading and its unit must fit without cutting
// the value; the name keeps its existing share of the phone table.
expect(headers.find((header) => header.textContent?.includes('°C'))).toHaveClass(
'platform-table-mobile-w-15',
'platform-table-mobile-w-20',
);
});