From 433ede0b0d68a9ff7b17657866ab1f50645de8c2 Mon Sep 17 00:00:00 2001 From: ZacharyZcR Date: Mon, 24 Aug 2026 01:08:39 +0800 Subject: [PATCH] Add selectable host temperature sensors (#1307) --- .../metrics/widgets/temperature-collector.ts | 9 +++- .../widgets/temperature-collector.test.ts | 18 ++++++- .../host-metrics/cards/TemperatureCard.tsx | 52 ++++++++++++++++++- .../host-metrics/temperature-preference.ts | 12 +++++ src/ui/locales/en.json | 1 + .../temperature-preference.test.ts | 28 ++++++++++ 6 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 src/ui/features/host-metrics/temperature-preference.ts create mode 100644 src/ui/tests/features/host-metrics/temperature-preference.test.ts diff --git a/src/backend/hosts/metrics/widgets/temperature-collector.ts b/src/backend/hosts/metrics/widgets/temperature-collector.ts index 1c83811a..fb18c7d0 100644 --- a/src/backend/hosts/metrics/widgets/temperature-collector.ts +++ b/src/backend/hosts/metrics/widgets/temperature-collector.ts @@ -58,12 +58,19 @@ export function parseSysfsThermalOutput(output: string): TemperatureSensor[] { export function parseSensorsOutput(output: string): TemperatureSensor[] { const seen = new Set(); const sensors: TemperatureSensor[] = []; + let device = ""; for (const line of output.split("\n")) { + const trimmed = line.trim(); + if (trimmed && !line.startsWith(" ") && !trimmed.includes(":")) { + device = trimmed; + continue; + } const match = line.match(/^\s*([^:]+):\s*([+-]?\d+(?:\.\d+)?)\s*°?C\b/i); if (!match) continue; - const sensor = normalizeSensor(match[1], Number(match[2])); + const label = device ? `${device}: ${match[1]}` : match[1]; + const sensor = normalizeSensor(label, Number(match[2])); if (!sensor) continue; const key = `${sensor.label.toLowerCase()}:${sensor.celsius}`; diff --git a/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts b/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts index 36757871..629940be 100644 --- a/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts +++ b/src/backend/tests/hosts/metrics/widgets/temperature-collector.test.ts @@ -26,8 +26,22 @@ fan1: 1200 RPM `); expect(result).toEqual([ - { label: "Package id 0", celsius: 52 }, - { label: "Core 0", celsius: 48.5 }, + { label: "coretemp-isa-0000: Package id 0", celsius: 52 }, + { label: "coretemp-isa-0000: Core 0", celsius: 48.5 }, + ]); + }); + + it("keeps duplicate sensor names distinct across devices", () => { + const result = parseSensorsOutput(` +nvme-pci-0100 +Composite: +41.0°C +nvme-pci-0200 +Composite: +52.0°C +`); + + expect(result).toEqual([ + { label: "nvme-pci-0100: Composite", celsius: 41 }, + { label: "nvme-pci-0200: Composite", celsius: 52 }, ]); }); }); diff --git a/src/ui/features/host-metrics/cards/TemperatureCard.tsx b/src/ui/features/host-metrics/cards/TemperatureCard.tsx index a805eeee..fd1a1308 100644 --- a/src/ui/features/host-metrics/cards/TemperatureCard.tsx +++ b/src/ui/features/host-metrics/cards/TemperatureCard.tsx @@ -3,6 +3,11 @@ import { useTranslation } from "react-i18next"; import type { ServerMetrics } from "@/main-axios"; import { StatRow } from "@/components/charts"; import { MetricCard } from "./MetricCard"; +import { useEffect, useState } from "react"; +import { + selectTemperatureSensor, + temperaturePreferenceKey, +} from "../temperature-preference"; function formatTemperature(value: number | null | undefined): string { return typeof value === "number" && Number.isFinite(value) @@ -12,12 +17,37 @@ function formatTemperature(value: number | null | undefined): string { export function TemperatureCard({ metrics, + hostId, }: { metrics: ServerMetrics | null; + hostId: number | null; }) { const { t } = useTranslation(); const temperature = metrics?.temperature; const sensors = temperature?.sensors ?? []; + const [preferredLabel, setPreferredLabel] = useState(""); + + useEffect(() => { + setPreferredLabel( + hostId === null + ? "" + : (localStorage.getItem(temperaturePreferenceKey(hostId)) ?? ""), + ); + }, [hostId]); + + const preferredSensor = selectTemperatureSensor(sensors, preferredLabel); + const displayedTemperature = + preferredSensor?.celsius ?? temperature?.highestCelsius; + const displayedLabel = + preferredSensor?.label ?? t("hostMetrics.highestTemperature"); + + const chooseSensor = (label: string) => { + setPreferredLabel(label); + if (hostId === null) return; + const key = temperaturePreferenceKey(hostId); + if (label) localStorage.setItem(key, label); + else localStorage.removeItem(key); + }; return (
- {formatTemperature(temperature?.highestCelsius)} + {formatTemperature(displayedTemperature)}
- {t("hostMetrics.highestTemperature")} + {displayedLabel}
+ {sensors.length > 1 && ( + + )} + {sensors.length === 0 ? ( N/A ) : ( diff --git a/src/ui/features/host-metrics/temperature-preference.ts b/src/ui/features/host-metrics/temperature-preference.ts new file mode 100644 index 00000000..6a787c43 --- /dev/null +++ b/src/ui/features/host-metrics/temperature-preference.ts @@ -0,0 +1,12 @@ +import type { TemperatureSensor } from "@/types/stats-widgets"; + +export function selectTemperatureSensor( + sensors: TemperatureSensor[], + preferredLabel: string, +) { + return sensors.find((sensor) => sensor.label === preferredLabel) ?? null; +} + +export function temperaturePreferenceKey(hostId: number) { + return `termix-host-metrics:${hostId}:temperature-sensor`; +} diff --git a/src/ui/locales/en.json b/src/ui/locales/en.json index 0546d37b..4aeb835f 100644 --- a/src/ui/locales/en.json +++ b/src/ui/locales/en.json @@ -2511,6 +2511,7 @@ "selectFilesystem": "Select filesystem", "temperature": "Temperature", "highestTemperature": "Highest temperature", + "primaryTemperatureSensor": "Primary sensor", "failedToFetchHostConfig": "Failed to fetch host configuration", "serverOffline": "Server Offline", "cannotFetchMetrics": "Cannot fetch metrics from offline server", diff --git a/src/ui/tests/features/host-metrics/temperature-preference.test.ts b/src/ui/tests/features/host-metrics/temperature-preference.test.ts new file mode 100644 index 00000000..480e785b --- /dev/null +++ b/src/ui/tests/features/host-metrics/temperature-preference.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; +import { + selectTemperatureSensor, + temperaturePreferenceKey, +} from "../../../features/host-metrics/temperature-preference"; + +const sensors = [ + { label: "k10temp: Tctl", celsius: 55 }, + { label: "nvme-pci-0100: Composite", celsius: 70 }, +]; + +describe("temperature sensor preference", () => { + it("selects the configured sensor by its complete label", () => { + expect(selectTemperatureSensor(sensors, "k10temp: Tctl")).toEqual( + sensors[0], + ); + }); + + it("falls back when a sensor disappears", () => { + expect(selectTemperatureSensor(sensors, "missing")).toBeNull(); + }); + + it("scopes the preference to a host", () => { + expect(temperaturePreferenceKey(42)).toBe( + "termix-host-metrics:42:temperature-sensor", + ); + }); +});