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', ); });