From 75a978d2e2c53e6fcc93ce55ca7e7750ea473045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Mon, 27 Jul 2026 10:51:54 +0000 Subject: [PATCH] system-monitor: CPU temperature in bTop (Temperature card under Memory) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Backend: readTemps() scans /sys/class/hwmon for all temp sensors and picks the CPU one (k10temp/coretemp/zenpower Tctl/Tdie/Package); added to /stats.temp. - BtopView: a Temperature card at lg:col-start-2 (under Memory, second row) — big CPU °C (color-graded), its sensor label, and the other sensors (GPU, NVMe, wifi…) listed beneath. Co-Authored-By: Claude Opus 4.8 --- .../api/system-monitor/system-monitor.ts | 41 ++++++++++++++++++- .../src/apps/SystemMonitor/BtopView.tsx | 33 +++++++++++++-- 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/servers/api/system-monitor/system-monitor.ts b/src/servers/api/system-monitor/system-monitor.ts index 557e1909..3387d158 100644 --- a/src/servers/api/system-monitor/system-monitor.ts +++ b/src/servers/api/system-monitor/system-monitor.ts @@ -1,5 +1,5 @@ import os from 'node:os'; -import { readFile } from 'node:fs/promises'; +import { readFile, readdir } from 'node:fs/promises'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { createRouter } from '../../create-router'; @@ -102,6 +102,41 @@ async function readProcesses() { } // GET /stats — one snapshot. CPU% comes from two /proc/stat samples ~120ms apart. +// Read every hwmon temperature sensor (name/label/°C) and pick the CPU one (k10temp/coretemp/…). +async function readTemps() { + const sensors: { name: string; label: string; celsius: number }[] = []; + let dirs: string[]; + try { + dirs = await readdir('/sys/class/hwmon'); + } catch { + return { cpuC: null as number | null, cpuLabel: null as string | null, sensors }; + } + for (const d of dirs) { + const base = `/sys/class/hwmon/${d}`; + const name = (await readFile(`${base}/name`, 'utf8').catch(() => '')).trim(); + let files: string[]; + try { + files = await readdir(base); + } catch { + continue; + } + for (const f of files) { + const m = f.match(/^temp(\d+)_input$/); + if (!m) continue; + const milli = Number.parseInt((await readFile(`${base}/${f}`, 'utf8').catch(() => '')).trim(), 10); + if (!Number.isFinite(milli)) continue; + const label = (await readFile(`${base}/temp${m[1]}_label`, 'utf8').catch(() => '')).trim(); + sensors.push({ name, label: label || `temp${m[1]}`, celsius: Math.round(milli / 100) / 10 }); + } + } + const CPU_DRIVERS = ['k10temp', 'zenpower', 'coretemp', 'k8temp', 'cpu_thermal']; + const cpu = + sensors.find((s) => CPU_DRIVERS.includes(s.name.toLowerCase()) && /tctl|tdie|package|composite|core 0/i.test(s.label)) ?? + sensors.find((s) => CPU_DRIVERS.includes(s.name.toLowerCase())) ?? + null; + return { cpuC: cpu?.celsius ?? null, cpuLabel: cpu ? `${cpu.name} · ${cpu.label}` : null, sensors }; +} + systemMonitorRouter.get('/stats', async (ctx) => { const first = await readCpuSample().catch(() => null); await new Promise((r) => setTimeout(r, 120)); @@ -117,10 +152,11 @@ systemMonitorRouter.get('/stats', async (ctx) => { }) : []; - const [mem, disks, processes] = await Promise.all([ + const [mem, disks, processes, temp] = await Promise.all([ readMem().catch(() => null), readDisks(), readProcesses(), + readTemps().catch(() => null), ]); const cpus = os.cpus(); @@ -138,6 +174,7 @@ systemMonitorRouter.get('/stats', async (ctx) => { mem, disks, processes, + temp, timestamp: Date.now(), }); }); diff --git a/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx b/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx index 353d23b9..aac24ddd 100644 --- a/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx +++ b/src/workspaces/officerdev/src/apps/SystemMonitor/BtopView.tsx @@ -1,8 +1,9 @@ import { useEffect, useRef, useState } from 'react'; import { useClient } from 'hooks/useClient'; -import { Cpu, MemoryStick, HardDrive, Server } from 'lucide-react'; +import { Cpu, MemoryStick, HardDrive, Server, Thermometer } from 'lucide-react'; import { fmtBytes, fmtDuration } from './shared'; +type Sensor = { name: string; label: string; celsius: number }; type Stats = { hostname: string; platform: string; @@ -12,12 +13,14 @@ type Stats = { mem: { totalBytes: number; usedBytes: number; freeBytes: number; usedPct: number; swapTotalBytes: number; swapUsedBytes: number } | null; disks: { mount: string; fsType: string; totalBytes: number; usedBytes: number; usedPct: number }[]; processes: { pid: number; user: string; cpuPct: number; memPct: number; command: string }[]; + temp: { cpuC: number | null; cpuLabel: string | null; sensors: Sensor[] } | null; timestamp: number; }; const POLL_MS = 2000; const barColor = (pct: number) => (pct >= 85 ? 'bg-red-500' : pct >= 60 ? 'bg-amber-500' : 'bg-emerald-500'); +const tempColor = (c: number) => (c >= 80 ? 'text-red-500' : c >= 65 ? 'text-amber-500' : 'text-emerald-500'); const Bar = ({ pct }: { pct: number }) => (
@@ -25,8 +28,8 @@ const Bar = ({ pct }: { pct: number }) => (
); -const Card = ({ title, icon: Icon, children }: { title: string; icon: typeof Cpu; children: React.ReactNode }) => ( -
+const Card = ({ title, icon: Icon, children, className }: { title: string; icon: typeof Cpu; children: React.ReactNode; className?: string }) => ( +
{title} @@ -134,6 +137,30 @@ export const BtopView = () => {

no disks

)} + + + {stats.temp?.cpuC != null ? ( + <> +
+ {stats.temp.cpuC.toFixed(0)}°C + {stats.temp.cpuLabel} +
+
+ {stats.temp.sensors + .filter((s) => `${s.name} · ${s.label}` !== stats.temp!.cpuLabel) + .slice(0, 6) + .map((s, i) => ( +
+ {s.name} · {s.label} + {s.celsius.toFixed(0)}°C +
+ ))} +
+ + ) : ( +

no sensors

+ )} +