= (props) => {
{/* Use CSS hidden instead of Show to avoid mount/unmount which causes scroll jumps.
overflow-anchor: none prevents browser scroll anchoring from jumping when display toggles. */}
-
+
diff --git a/frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx b/frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx
index 8208946f7..dd0e6edda 100644
--- a/frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx
+++ b/frontend-modern/src/components/Workloads/NodeDrawerOverview.tsx
@@ -9,6 +9,7 @@ import {
formatUptime,
normalizeDiskArray,
} from '@/utils/format';
+import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
import { getNodeDisplayName } from '@/utils/nodes';
import { formatTemperature, getCpuTemperature, getTemperatureTextClass } from '@/utils/temperature';
@@ -17,6 +18,7 @@ import { DrawerDiskListCard, buildDrawerDiskListItems } from './DrawerDiskListCa
interface NodeDrawerOverviewProps {
node: Node;
disks?: Disk[];
+ temperatureThresholds?: MetricDisplayThresholds | null;
}
interface NodeOverviewRow {
@@ -89,25 +91,29 @@ const pushTemperature = (
rows: NodeOverviewRow[],
label: string,
value: number | null | undefined,
+ thresholds: MetricDisplayThresholds | null | undefined,
): void => {
if (typeof value !== 'number' || !Number.isFinite(value)) return;
rows.push({
label,
value: formatTemperature(value),
- valueClass: getTemperatureTextClass(value),
+ valueClass: getTemperatureTextClass(value, thresholds),
});
};
-const getThermalRows = (temperature: Temperature | undefined): NodeOverviewRow[] => {
+const getThermalRows = (
+ temperature: Temperature | undefined,
+ thresholds: MetricDisplayThresholds | null | undefined,
+): NodeOverviewRow[] => {
if (!temperature?.available) return [];
const rows: NodeOverviewRow[] = [];
const primary = getCpuTemperature(temperature);
- pushTemperature(rows, 'CPU current', primary);
- pushTemperature(rows, 'CPU package', temperature.cpuPackage);
- pushTemperature(rows, 'CPU low', temperature.cpuMin);
- pushTemperature(rows, 'CPU record', temperature.cpuMaxRecord);
+ pushTemperature(rows, 'CPU current', primary, thresholds);
+ pushTemperature(rows, 'CPU package', temperature.cpuPackage, thresholds);
+ pushTemperature(rows, 'CPU low', temperature.cpuMin, thresholds);
+ pushTemperature(rows, 'CPU record', temperature.cpuMaxRecord, thresholds);
if (temperature.nvme?.length) {
rows.push({
@@ -325,7 +331,10 @@ export function NodeDrawerOverview(props: NodeDrawerOverviewProps) {
-
+
);
}
diff --git a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
index 691bc89be..d7202b6e7 100644
--- a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx
@@ -41,6 +41,7 @@ type WorkloadPanelProps = Pick<
| 'bottomSpacerHeight'
| 'compactGroupHeaders'
| 'getGroupLabel'
+ | 'getNodeTemperatureThresholds'
| 'groupedGuests'
| 'groupedWindowing'
| 'groupLabelBadges'
@@ -181,7 +182,10 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
PVE {version}
-
+
{formatTemperature(temperature)}
@@ -461,6 +465,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) {
>
-
+
diff --git a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
index 4113a18be..8e38de700 100644
--- a/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadsSurface.tsx
@@ -91,12 +91,14 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) {
}
if (visibleInventoryIssues().length > 0) {
- return state.workloadsNoInventoryState?.() ?? {
- title: 'No workload inventory available',
- description:
- 'Pulse has infrastructure sources, but no VM, container, or pod inventory is available right now.',
- actionLabel: 'Review infrastructure sources',
- };
+ return (
+ state.workloadsNoInventoryState?.() ?? {
+ title: 'No workload inventory available',
+ description:
+ 'Pulse has infrastructure sources, but no VM, container, or pod inventory is available right now.',
+ actionLabel: 'Review infrastructure sources',
+ }
+ );
}
return {
@@ -169,6 +171,7 @@ export function WorkloadsSurface(props: WorkloadsSurfaceComponentProps) {
bottomSpacerHeight={state.bottomSpacerHeight}
compactGroupHeaders={state.compactGroupHeaders}
getGroupLabel={state.getGroupLabel}
+ getNodeTemperatureThresholds={state.getNodeTemperatureThresholds}
groupedGuests={state.groupedGuests}
groupedWindowing={state.groupedWindowing}
groupLabelBadges={state.groupLabelBadges}
diff --git a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
index b86ae63da..4b2e8f1ed 100644
--- a/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
+++ b/frontend-modern/src/components/Workloads/WorkloadsTable.tsx
@@ -16,6 +16,7 @@ type WorkloadsTableProps = Pick<
| 'bottomSpacerHeight'
| 'compactGroupHeaders'
| 'getGroupLabel'
+ | 'getNodeTemperatureThresholds'
| 'groupedGuests'
| 'groupedWindowing'
| 'groupLabelBadges'
@@ -102,6 +103,7 @@ export function WorkloadsTable(props: WorkloadsTableProps) {
bottomSpacerHeight={props.bottomSpacerHeight}
compactGroupHeaders={props.compactGroupHeaders}
getGroupLabel={props.getGroupLabel}
+ getNodeTemperatureThresholds={props.getNodeTemperatureThresholds}
groupedGuests={props.groupedGuests}
groupedWindowing={props.groupedWindowing}
groupLabelBadges={props.groupLabelBadges}
diff --git a/frontend-modern/src/components/Workloads/useWorkloadsState.ts b/frontend-modern/src/components/Workloads/useWorkloadsState.ts
index e2e1b576e..99d579f56 100644
--- a/frontend-modern/src/components/Workloads/useWorkloadsState.ts
+++ b/frontend-modern/src/components/Workloads/useWorkloadsState.ts
@@ -1,6 +1,7 @@
import { createEffect, createMemo, onCleanup, type Accessor } from 'solid-js';
import { useNavigate } from '@solidjs/router';
import { ConnectionsAPI, type ConnectionsListResponse } from '@/api/connections';
+import { nodeOverrideIdCandidates } from '@/features/alerts/alertOverridesModel';
import type { VM, Container, Node } from '@/types/api';
import type { Resource } from '@/types/resource';
import type { ViewMode, WorkloadGuest, WorkloadType } from '@/types/workloads';
@@ -350,6 +351,9 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
reconnect();
};
+ const getNodeTemperatureThresholds = (node: Node) =>
+ alertsActivation.getMetricThresholds('node', 'temperature', nodeOverrideIdCandidates(node));
+
createEffect(() => {
if (!workloadsEnabled()) return;
const handle = window.setInterval(() => {
@@ -481,6 +485,7 @@ export function useWorkloadsState(props: WorkloadsSurfaceProps) {
focusedSummaryWorkloadGroupScope,
focusedSummaryWorkloadGroupId,
getGroupLabel,
+ getNodeTemperatureThresholds,
groupedGuests,
groupedWindowing,
guestMetadata,
diff --git a/frontend-modern/src/components/shared/NodeGroupHeader.tsx b/frontend-modern/src/components/shared/NodeGroupHeader.tsx
index 702b12a47..137826ee9 100644
--- a/frontend-modern/src/components/shared/NodeGroupHeader.tsx
+++ b/frontend-modern/src/components/shared/NodeGroupHeader.tsx
@@ -1,5 +1,6 @@
import { Component, For, Show, type JSX } from 'solid-js';
import type { Node } from '@/types/api';
+import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
import { getNodeDisplayName, getNodeExternalUrl, hasAlternateDisplayName } from '@/utils/nodes';
import { StatusDot } from '@/components/shared/StatusDot';
import { getNodeStatusIndicator } from '@/utils/status';
@@ -22,6 +23,7 @@ interface NodeGroupHeaderProps {
renderColumnCell?: (columnId: string, node: Node) => JSX.Element;
renderAs?: 'tr' | 'div';
showFactsInName?: boolean;
+ temperatureThresholds?: MetricDisplayThresholds | null;
trClass?: string;
trProps?: JSX.HTMLAttributes &
Partial>;
@@ -89,7 +91,10 @@ export const NodeGroupHeader: Component = (props) => {
PVE {pveVersion()}
-
+
{formatTemperature(cpuTemperature())}
diff --git a/frontend-modern/src/components/shared/TemperatureGauge.tsx b/frontend-modern/src/components/shared/TemperatureGauge.tsx
index 98875b81e..01e0c179b 100644
--- a/frontend-modern/src/components/shared/TemperatureGauge.tsx
+++ b/frontend-modern/src/components/shared/TemperatureGauge.tsx
@@ -1,5 +1,9 @@
import { Component, createMemo } from 'solid-js';
-import { formatTemperature, getTemperatureTextClass } from '@/utils/temperature';
+import {
+ formatTemperature,
+ getTemperatureTextClass,
+ type TemperatureDisplayMetric,
+} from '@/utils/temperature';
import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
interface TemperatureGaugeProps {
@@ -9,6 +13,7 @@ interface TemperatureGaugeProps {
critical?: number;
warning?: number;
thresholds?: MetricDisplayThresholds | null;
+ metric?: TemperatureDisplayMetric;
class?: string;
}
@@ -25,7 +30,7 @@ export const TemperatureGauge: Component = (props) => {
});
const textColorClass = createMemo(() =>
- getTemperatureTextClass(props.value, explicitThresholds()),
+ getTemperatureTextClass(props.value, explicitThresholds(), props.metric),
);
return (
diff --git a/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx b/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
index d9c992ced..88f23a060 100644
--- a/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
+++ b/frontend-modern/src/components/shared/__tests__/NodeGroupHeader.test.tsx
@@ -52,4 +52,21 @@ describe('NodeGroupHeader', () => {
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
});
+
+ it('colors grouped header temperatures from configured thresholds', () => {
+ render(() => (
+
+ ));
+
+ expect(screen.getByText('76°C')).toHaveClass('text-green-600');
+ });
});
diff --git a/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts b/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts
index 19f1143a4..8124069ec 100644
--- a/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts
+++ b/frontend-modern/src/features/alerts/__tests__/alertOverridesModel.test.ts
@@ -5,6 +5,7 @@ import type { Resource } from '@/types/resource';
import {
buildContainerRuntimeResources,
buildProjectedOverrides,
+ nodeOverrideIdCandidates,
normalizeRawOverridesConfig,
} from '../alertOverridesModel';
@@ -17,6 +18,25 @@ const makeResource = (overrides: Partial): Resource =>
}) as Resource;
describe('alertOverridesModel', () => {
+ it('builds node temperature override candidates from stable node and agent identities', () => {
+ expect(
+ nodeOverrideIdCandidates({
+ id: 'agent:pve-node-1',
+ name: 'pve-node-1',
+ instance: 'homelab',
+ host: 'https://pve-node-1:8006',
+ linkedAgentId: 'agent-linked',
+ }),
+ ).toEqual([
+ 'agent-linked',
+ 'agent:pve-node-1',
+ 'homelab-pve-node-1',
+ 'homelab:pve-node-1',
+ 'pve-node-1',
+ 'https://pve-node-1:8006',
+ ]);
+ });
+
it('normalizes disk override ids into canonical agent disk keys', () => {
expect(
normalizeRawOverridesConfig({
diff --git a/frontend-modern/src/features/alerts/alertOverridesModel.ts b/frontend-modern/src/features/alerts/alertOverridesModel.ts
index 2ddd535fa..b88b161ed 100644
--- a/frontend-modern/src/features/alerts/alertOverridesModel.ts
+++ b/frontend-modern/src/features/alerts/alertOverridesModel.ts
@@ -1,4 +1,4 @@
-import type { PBSInstance } from '@/types/api';
+import type { Node, PBSInstance } from '@/types/api';
import type { RawOverrideConfig } from '@/types/alerts';
import type { Resource } from '@/types/resource';
import { getActionableAgentIdFromResource, isTrueNASSystemResource } from '@/utils/agentResources';
@@ -154,6 +154,18 @@ export const hostOverrideIdCandidates = (resource: Resource): string[] => {
);
};
+export const nodeOverrideIdCandidates = (
+ node: Pick,
+): string[] =>
+ uniqueIds(
+ node.linkedAgentId,
+ node.id,
+ node.instance && node.name ? `${node.instance}-${node.name}` : undefined,
+ node.instance && node.name ? `${node.instance}:${node.name}` : undefined,
+ node.name,
+ node.host,
+ );
+
export const dockerHostOverrideIdCandidates = (resource: Resource): string[] => {
const data = platformData(resource);
const docker = asRecord(data?.docker);
diff --git a/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx b/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx
index da696c102..d0cbb4b79 100644
--- a/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx
+++ b/frontend-modern/src/features/docker/DockerHostDrawerOverview.tsx
@@ -7,7 +7,9 @@ import {
} from '@/components/Workloads/DrawerDiskListCard';
import { InfoCardFrame } from '@/components/shared/InfoCardFrame';
import { useResourceDetailDrawerDockerActionsState } from '@/components/Infrastructure/useResourceDetailDrawerDockerActionsState';
+import { hostOverrideIdCandidates } from '@/features/alerts/alertOverridesModel';
import { areSystemSettingsLoaded, shouldHideDockerUpdateActions } from '@/stores/systemSettings';
+import { useAlertsActivation } from '@/stores/alertsActivation';
import type { Resource } from '@/types/resource';
import {
formatBytes,
@@ -103,9 +105,17 @@ const DetailCard = (props: { title: string; rows: DockerOverviewRow[] }) => (
);
export function DockerHostDrawerOverview(props: DockerHostDrawerOverviewProps) {
+ const alertsActivation = useAlertsActivation();
const docker = () => props.host.docker;
const agent = () => props.host.agent;
const linkedAgentId = () => cleanText(agent()?.agentId);
+ const temperatureThresholds = createMemo(() =>
+ alertsActivation.getMetricThresholds(
+ 'node',
+ 'temperature',
+ hostOverrideIdCandidates(props.host),
+ ),
+ );
const runtimeLabel = (): string => {
const runtime = cleanText(docker()?.runtime) || 'Docker';
@@ -302,7 +312,7 @@ export function DockerHostDrawerOverview(props: DockerHostDrawerOverviewProps) {
rows.push({
label: 'CPU temp',
value: formatTemperature(temperature),
- valueClass: getTemperatureTextClass(temperature),
+ valueClass: getTemperatureTextClass(temperature, temperatureThresholds()),
});
}
if (
diff --git a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx
index 0c863ceb4..2d5fa2624 100644
--- a/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx
+++ b/frontend-modern/src/features/docker/__tests__/DockerHostsTable.test.tsx
@@ -1,4 +1,4 @@
-import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
+import { cleanup, fireEvent, render, screen, within } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Disk } from '@/types/api';
@@ -19,7 +19,10 @@ vi.mock('@/contexts/appRuntime', () => ({
useWebSocket: () => ({ activeAlerts: {} as Record }),
}));
vi.mock('@/stores/alertsActivation', () => ({
- useAlertsActivation: () => ({ activationState: () => 'active' }),
+ useAlertsActivation: () => ({
+ activationState: () => 'active',
+ getMetricThresholds: () => ({ warning: 80, critical: 85 }),
+ }),
}));
vi.mock('@/components/Workloads/StackedMemoryBar', () => ({
@@ -132,6 +135,23 @@ describe('DockerHostsTable', () => {
expect(screen.getByRole('tab', { name: 'History' })).toHaveAttribute('type', 'button');
});
+ it('colors drawer host temperatures from configured thresholds', () => {
+ render(() => (
+ }
+ emptyTitle="No Docker hosts"
+ emptyDescription="No hosts"
+ showToolbar={false}
+ />
+ ));
+
+ fireEvent.click(screen.getByText('docker-01').closest('tr')!);
+
+ const drawer = screen.getByTestId('docker-host-drawer');
+ expect(within(drawer).getByText('76°C')).toHaveClass('text-green-600');
+ });
+
it('surfaces container update actions in the host drawer', () => {
render(() => (
{
const config = toDiscoveryConfig(node);
return config
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx b/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx
index 50b132126..ca802f005 100644
--- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx
+++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx
@@ -58,8 +58,8 @@ vi.mock('@/components/Workloads/useWorkloadTableMetricHistory', () => ({
}));
vi.mock('@/components/Workloads/NodeDrawer', () => ({
- NodeDrawer: (props: { node: { name: string } }) => {
- nodeDrawerMock(props.node);
+ NodeDrawer: (props: { node: { name: string }; temperatureThresholds?: unknown }) => {
+ nodeDrawerMock(props);
return {props.node.name}
;
},
}));
@@ -304,7 +304,12 @@ describe('ProxmoxNodesTable', () => {
expect(row).toHaveAttribute('aria-expanded', 'true');
expect(screen.getByTestId('node-drawer')).toHaveTextContent('pve-node-1');
- expect(nodeDrawerMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'pve-node-1' }));
+ expect(nodeDrawerMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ node: expect.objectContaining({ name: 'pve-node-1' }),
+ temperatureThresholds: { warning: 80, critical: 85 },
+ }),
+ );
await fireEvent.click(row!);
diff --git a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx
index 31f62df26..a8defb099 100644
--- a/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx
+++ b/frontend-modern/src/features/standalone/AgentsMachinesTable.tsx
@@ -56,6 +56,7 @@ import {
import { useColumnVisibility } from '@/hooks/useColumnVisibility';
import type { Disk } from '@/types/api';
import type { Resource, ResourceAvailabilityMeta } from '@/types/resource';
+import type { MetricDisplayThresholds } from '@/utils/metricThresholds';
import { getActionableAgentIdFromResource } from '@/utils/agentResources';
import { formatBytes, formatSpeed, normalizeDiskArray } from '@/utils/format';
import { STORAGE_KEYS } from '@/utils/localStorage';
@@ -88,6 +89,7 @@ import {
getAgentMachineRaidSummary,
getAgentMachineTemperatureCelsius,
getAgentMachineTemperatureDetailSections,
+ getAgentMachineTemperatureMetric,
getAgentMachineTemperatureTitle,
getAgentMachineThermalPressurePresentation,
getNextAgentMachineSortState,
@@ -99,6 +101,7 @@ import {
type AgentMachineRaidArrayDetail,
type AgentMachineSortKey,
type AgentMachineTemperatureDetailSection,
+ type AgentMachineTemperatureMetric,
} from './agentMachineTableModel';
type MetricFallbackReason = {
@@ -171,7 +174,9 @@ const AgentMachineMetricTooltip: Component<{
const AgentMachineTemperatureCell: Component<{
celsius: number | undefined;
+ metric: AgentMachineTemperatureMetric;
sections: AgentMachineTemperatureDetailSection[];
+ thresholds?: MetricDisplayThresholds | null;
title: string;
thermalPressure?: {
label: string;
@@ -207,7 +212,9 @@ const AgentMachineTemperatureCell: Component<{
}
>
- {(value) => }
+ {(value) => (
+
+ )}
}
>
@@ -1479,6 +1486,13 @@ export const AgentsMachinesTable: Component<{
const raidArrays = () => getAgentMachineRaidArrayDetails(machine);
const raidSummary = () => getAgentMachineRaidSummary(machine);
const temperature = () => getAgentMachineTemperatureCelsius(machine);
+ const temperatureMetric = () => getAgentMachineTemperatureMetric(machine);
+ const temperatureThresholds = () =>
+ alertsActivation.getMetricThresholds(
+ temperatureMetric() === 'diskTemperature' ? 'agent' : 'node',
+ temperatureMetric(),
+ alertResourceIds(),
+ );
const temperatureSections = () =>
getAgentMachineTemperatureDetailSections(machine);
const temperatureTitle = () => getAgentMachineTemperatureTitle(machine);
@@ -1672,7 +1686,9 @@ export const AgentsMachinesTable: Component<{
>
diff --git a/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.test.ts b/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.test.ts
index 33630dc2c..c4b393e52 100644
--- a/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.test.ts
+++ b/frontend-modern/src/features/standalone/__tests__/agentMachineTableModel.test.ts
@@ -6,6 +6,7 @@ import {
getAgentMachineNetworkInterfaceDetails,
getAgentMachineRaidArrayDetails,
getAgentMachineTemperatureCelsius,
+ getAgentMachineTemperatureMetric,
getAgentMachineTemperatureTitle,
matchesAgentMachineSearch,
sortAgentMachines,
@@ -182,35 +183,32 @@ describe('agentMachineTableModel', () => {
});
expect(getAgentMachineTemperatureCelsius(machine)).toBe(44);
+ expect(getAgentMachineTemperatureMetric(machine)).toBe('diskTemperature');
});
it('prefers direct and sensor temperatures over SMART fallback temperatures', () => {
- expect(
- getAgentMachineTemperatureCelsius(
- resource({
- temperature: 55,
- agent: {
- sensors: {
- temperatureCelsius: { 'cpu.package': 61 },
- smart: [{ device: '/dev/nvme0', temperature: 72 }],
- },
- },
- }),
- ),
- ).toBe(55);
+ const direct = resource({
+ temperature: 55,
+ agent: {
+ sensors: {
+ temperatureCelsius: { 'cpu.package': 61 },
+ smart: [{ device: '/dev/nvme0', temperature: 72 }],
+ },
+ },
+ });
+ expect(getAgentMachineTemperatureCelsius(direct)).toBe(55);
+ expect(getAgentMachineTemperatureMetric(direct)).toBe('temperature');
- expect(
- getAgentMachineTemperatureCelsius(
- resource({
- agent: {
- sensors: {
- temperatureCelsius: { 'cpu.package': 61, 'cpu.core0': 64 },
- smart: [{ device: '/dev/nvme0', temperature: 72 }],
- },
- },
- }),
- ),
- ).toBe(64);
+ const sensor = resource({
+ agent: {
+ sensors: {
+ temperatureCelsius: { 'cpu.package': 61, 'cpu.core0': 64 },
+ smart: [{ device: '/dev/nvme0', temperature: 72 }],
+ },
+ },
+ });
+ expect(getAgentMachineTemperatureCelsius(sensor)).toBe(64);
+ expect(getAgentMachineTemperatureMetric(sensor)).toBe('temperature');
});
it('includes agent sensor, SMART, fan, and additional readings in the temperature title', () => {
diff --git a/frontend-modern/src/features/standalone/agentMachineTableModel.ts b/frontend-modern/src/features/standalone/agentMachineTableModel.ts
index dcaca1651..944219095 100644
--- a/frontend-modern/src/features/standalone/agentMachineTableModel.ts
+++ b/frontend-modern/src/features/standalone/agentMachineTableModel.ts
@@ -141,6 +141,8 @@ export type AgentMachineThermalPressurePresentation = {
className: string;
};
+export type AgentMachineTemperatureMetric = 'temperature' | 'diskTemperature';
+
export type AgentMachineNetworkInterfaceDetail = {
name: string;
mac?: string;
@@ -458,6 +460,19 @@ export const getAgentMachineTemperatureCelsius = (machine: Resource): number | u
);
};
+export const getAgentMachineTemperatureMetric = (
+ machine: Resource,
+): AgentMachineTemperatureMetric => {
+ if (positiveTemperature(machine.temperature) !== undefined) return 'temperature';
+ if (maxTemperatureReading(getSensorTemperatureReadings(machine)) !== undefined) {
+ return 'temperature';
+ }
+ if (maxTemperatureReading(getActiveSmartTemperatureReadings(machine)) !== undefined) {
+ return 'diskTemperature';
+ }
+ return 'temperature';
+};
+
export const getAgentMachineTemperatureDetailSections = (
machine: Resource,
): AgentMachineTemperatureDetailSection[] => {
diff --git a/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts
new file mode 100644
index 000000000..3f8e8898e
--- /dev/null
+++ b/frontend-modern/src/utils/__tests__/temperatureDisplayGuardrails.test.ts
@@ -0,0 +1,28 @@
+import { describe, expect, it } from 'vitest';
+
+import nodeDrawerOverviewSource from '@/components/Workloads/NodeDrawerOverview.tsx?raw';
+import workloadPanelSource from '@/components/Workloads/WorkloadPanel.tsx?raw';
+import nodeGroupHeaderSource from '@/components/shared/NodeGroupHeader.tsx?raw';
+import temperatureGaugeSource from '@/components/shared/TemperatureGauge.tsx?raw';
+import dockerHostDrawerOverviewSource from '@/features/docker/DockerHostDrawerOverview.tsx?raw';
+import agentsMachinesTableSource from '@/features/standalone/AgentsMachinesTable.tsx?raw';
+
+describe('temperature display guardrails', () => {
+ it('keeps colored temperature displays threshold-aware', () => {
+ expect(nodeGroupHeaderSource).toContain('temperatureThresholds');
+ expect(workloadPanelSource).toContain('getNodeTemperatureThresholds(node)');
+ expect(nodeDrawerOverviewSource).toContain('getTemperatureTextClass(value, thresholds)');
+ expect(dockerHostDrawerOverviewSource).toContain('temperatureThresholds()');
+ expect(agentsMachinesTableSource).toContain('getAgentMachineTemperatureMetric');
+ expect(agentsMachinesTableSource).toContain('thresholds={temperatureThresholds()}');
+ expect(temperatureGaugeSource).toContain('props.metric');
+ });
+
+ it('does not reintroduce the previous local temperature severity calls', () => {
+ expect(nodeGroupHeaderSource).not.toContain('getTemperatureTextClass(cpuTemperature())');
+ expect(workloadPanelSource).not.toContain('getTemperatureTextClass(temperature)}');
+ expect(nodeDrawerOverviewSource).not.toContain('getTemperatureTextClass(value)');
+ expect(dockerHostDrawerOverviewSource).not.toContain('getTemperatureTextClass(temperature)');
+ expect(agentsMachinesTableSource).not.toContain('');
+ });
+});
diff --git a/frontend-modern/src/utils/temperature.ts b/frontend-modern/src/utils/temperature.ts
index cf015e31c..f8b822504 100644
--- a/frontend-modern/src/utils/temperature.ts
+++ b/frontend-modern/src/utils/temperature.ts
@@ -104,7 +104,10 @@ const TEMPERATURE_TEXT_CLASSES = {
normal: 'text-green-600 dark:text-green-400',
} as const;
-type TemperatureDisplayMetric = Extract;
+export type TemperatureDisplayMetric = Extract<
+ DisplayMetricType,
+ 'temperature' | 'diskTemperature'
+>;
export const getTemperatureTextClass = (
celsius: number | null | undefined,