mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
Add selectable host temperature sensors (#1307)
This commit is contained in:
@@ -58,12 +58,19 @@ export function parseSysfsThermalOutput(output: string): TemperatureSensor[] {
|
|||||||
export function parseSensorsOutput(output: string): TemperatureSensor[] {
|
export function parseSensorsOutput(output: string): TemperatureSensor[] {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
const sensors: TemperatureSensor[] = [];
|
const sensors: TemperatureSensor[] = [];
|
||||||
|
let device = "";
|
||||||
|
|
||||||
for (const line of output.split("\n")) {
|
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);
|
const match = line.match(/^\s*([^:]+):\s*([+-]?\d+(?:\.\d+)?)\s*°?C\b/i);
|
||||||
if (!match) continue;
|
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;
|
if (!sensor) continue;
|
||||||
|
|
||||||
const key = `${sensor.label.toLowerCase()}:${sensor.celsius}`;
|
const key = `${sensor.label.toLowerCase()}:${sensor.celsius}`;
|
||||||
|
|||||||
@@ -26,8 +26,22 @@ fan1: 1200 RPM
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
expect(result).toEqual([
|
expect(result).toEqual([
|
||||||
{ label: "Package id 0", celsius: 52 },
|
{ label: "coretemp-isa-0000: Package id 0", celsius: 52 },
|
||||||
{ label: "Core 0", celsius: 48.5 },
|
{ 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 },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { useTranslation } from "react-i18next";
|
|||||||
import type { ServerMetrics } from "@/main-axios";
|
import type { ServerMetrics } from "@/main-axios";
|
||||||
import { StatRow } from "@/components/charts";
|
import { StatRow } from "@/components/charts";
|
||||||
import { MetricCard } from "./MetricCard";
|
import { MetricCard } from "./MetricCard";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import {
|
||||||
|
selectTemperatureSensor,
|
||||||
|
temperaturePreferenceKey,
|
||||||
|
} from "../temperature-preference";
|
||||||
|
|
||||||
function formatTemperature(value: number | null | undefined): string {
|
function formatTemperature(value: number | null | undefined): string {
|
||||||
return typeof value === "number" && Number.isFinite(value)
|
return typeof value === "number" && Number.isFinite(value)
|
||||||
@@ -12,12 +17,37 @@ function formatTemperature(value: number | null | undefined): string {
|
|||||||
|
|
||||||
export function TemperatureCard({
|
export function TemperatureCard({
|
||||||
metrics,
|
metrics,
|
||||||
|
hostId,
|
||||||
}: {
|
}: {
|
||||||
metrics: ServerMetrics | null;
|
metrics: ServerMetrics | null;
|
||||||
|
hostId: number | null;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const temperature = metrics?.temperature;
|
const temperature = metrics?.temperature;
|
||||||
const sensors = temperature?.sensors ?? [];
|
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 (
|
return (
|
||||||
<MetricCard
|
<MetricCard
|
||||||
@@ -29,13 +59,31 @@ export function TemperatureCard({
|
|||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-3xl font-semibold tabular-nums">
|
<div className="text-3xl font-semibold tabular-nums">
|
||||||
{formatTemperature(temperature?.highestCelsius)}
|
{formatTemperature(displayedTemperature)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{t("hostMetrics.highestTemperature")}
|
{displayedLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sensors.length > 1 && (
|
||||||
|
<label className="flex flex-col gap-1 text-xs text-muted-foreground">
|
||||||
|
<span>{t("hostMetrics.primaryTemperatureSensor")}</span>
|
||||||
|
<select
|
||||||
|
value={preferredSensor?.label ?? ""}
|
||||||
|
onChange={(event) => chooseSensor(event.target.value)}
|
||||||
|
className="h-8 border border-border bg-background px-2 text-xs text-foreground outline-none focus:ring-1 focus:ring-ring"
|
||||||
|
>
|
||||||
|
<option value="">{t("hostMetrics.highestTemperature")}</option>
|
||||||
|
{sensors.map((sensor) => (
|
||||||
|
<option key={sensor.label} value={sensor.label}>
|
||||||
|
{sensor.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
|
||||||
{sensors.length === 0 ? (
|
{sensors.length === 0 ? (
|
||||||
<span className="text-xs text-muted-foreground">N/A</span>
|
<span className="text-xs text-muted-foreground">N/A</span>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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`;
|
||||||
|
}
|
||||||
@@ -2511,6 +2511,7 @@
|
|||||||
"selectFilesystem": "Select filesystem",
|
"selectFilesystem": "Select filesystem",
|
||||||
"temperature": "Temperature",
|
"temperature": "Temperature",
|
||||||
"highestTemperature": "Highest temperature",
|
"highestTemperature": "Highest temperature",
|
||||||
|
"primaryTemperatureSensor": "Primary sensor",
|
||||||
"failedToFetchHostConfig": "Failed to fetch host configuration",
|
"failedToFetchHostConfig": "Failed to fetch host configuration",
|
||||||
"serverOffline": "Server Offline",
|
"serverOffline": "Server Offline",
|
||||||
"cannotFetchMetrics": "Cannot fetch metrics from offline server",
|
"cannotFetchMetrics": "Cannot fetch metrics from offline server",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user