system-monitor: CPU temperature in bTop (Temperature card under Memory)

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 10:51:54 +00:00
co-authored by Claude Opus 4.8
parent 41ebd73c2d
commit 75a978d2e2
2 changed files with 69 additions and 5 deletions
@@ -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 }) => (
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
@@ -25,8 +28,8 @@ const Bar = ({ pct }: { pct: number }) => (
</div>
);
const Card = ({ title, icon: Icon, children }: { title: string; icon: typeof Cpu; children: React.ReactNode }) => (
<div className="flex flex-col gap-3 rounded-xl border border-border bg-card p-4">
const Card = ({ title, icon: Icon, children, className }: { title: string; icon: typeof Cpu; children: React.ReactNode; className?: string }) => (
<div className={`flex flex-col gap-3 rounded-xl border border-border bg-card p-4 ${className ?? ''}`}>
<div className="flex items-center gap-2 text-sm font-medium text-muted-foreground">
<Icon size={16} className="text-primary" />
{title}
@@ -134,6 +137,30 @@ export const BtopView = () => {
<p className="text-sm text-muted-foreground">no disks</p>
)}
</Card>
<Card title="Temperature" icon={Thermometer} className="lg:col-start-2">
{stats.temp?.cpuC != null ? (
<>
<div className="flex items-end justify-between">
<span className={`text-3xl font-semibold ${tempColor(stats.temp.cpuC)}`}>{stats.temp.cpuC.toFixed(0)}°C</span>
<span className="truncate pl-2 text-xs text-muted-foreground">{stats.temp.cpuLabel}</span>
</div>
<div className="flex flex-col gap-1 pt-1">
{stats.temp.sensors
.filter((s) => `${s.name} · ${s.label}` !== stats.temp!.cpuLabel)
.slice(0, 6)
.map((s, i) => (
<div key={i} className="flex items-center justify-between text-xs">
<span className="truncate text-muted-foreground">{s.name} · {s.label}</span>
<span className={`font-mono ${tempColor(s.celsius)}`}>{s.celsius.toFixed(0)}°C</span>
</div>
))}
</div>
</>
) : (
<p className="text-sm text-muted-foreground">no sensors</p>
)}
</Card>
</div>
<Card title="Top processes" icon={Server}>