From d450e2894aa5eff03c95802e8725719e2c41b6bf Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:03:22 +0100 Subject: [PATCH] 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 --- .../0c9eb2a66a986fd2.json | 42 +++++++++++++++++++ .../__tests__/DockerHostsTable.test.tsx | 25 +++++++++++ .../sharedPlatformPage.temperature.test.tsx | 31 ++++++++++++++ .../platformPage/sharedPlatformPage.tsx | 6 ++- .../features/truenas/TrueNASSystemsTable.tsx | 7 ++-- .../__tests__/TrueNASSystemsTable.test.tsx | 36 +++++++++++++++- 6 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 frontend-modern/browser-verification/0c9eb2a66a986fd2.json create mode 100644 frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.temperature.test.tsx diff --git a/frontend-modern/browser-verification/0c9eb2a66a986fd2.json b/frontend-modern/browser-verification/0c9eb2a66a986fd2.json new file mode 100644 index 0000000000..140374362d --- /dev/null +++ b/frontend-modern/browser-verification/0c9eb2a66a986fd2.json @@ -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." + ] +} diff --git a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx index 031e1af6cc..89f84ea97a 100644 --- a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx +++ b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx @@ -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 => ({ 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(() => ( + } + 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'); diff --git a/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.temperature.test.tsx b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.temperature.test.tsx new file mode 100644 index 0000000000..91d922a987 --- /dev/null +++ b/frontend-modern/src/features/platformPage/__tests__/sharedPlatformPage.temperature.test.tsx @@ -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(() => ); + 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(() => ); + expect(screen.getByText('Not reported')).toBeInTheDocument(); + temperatureStore.setUnit('fahrenheit'); + expect(screen.getByText('Not reported')).toBeInTheDocument(); + expect(screen.queryByText(/°[CF]/)).not.toBeInTheDocument(); + }, + ); +}); diff --git a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx index a3c2ff772a..a255f84eba 100644 --- a/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx +++ b/frontend-modern/src/features/platformPage/sharedPlatformPage.tsx @@ -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: { ); } diff --git a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx index 0ca2c8dd8b..6b47ddc64c 100644 --- a/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx +++ b/frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx @@ -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%]" > @@ -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%]" > - + ({ import { TrueNASSystemsTable } from '@/features/truenas/TrueNASSystemsTable'; import type { Resource } from '@/types/resource'; +import { temperatureStore } from '@/utils/temperature'; const makeSystem = (overrides: Partial & Pick): Resource => ({ @@ -45,10 +46,41 @@ const makeSystem = (overrides: Partial & Pick): 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(() => ( + } + 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(() => ( { 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', ); });