system-monitor: GPU, Network, and Power cards in bTop
Three more same-level grid cards (bTop now 7 cards; Temperature stays under Memory via natural 3-col flow): - GPU: gpu_busy_percent + VRAM used/total from /sys/class/drm (instant). - Network: ↓/↑ throughput (bytes/sec) from /proc/net/dev deltas between /stats calls, aggregate + top interfaces. - Power: CPU package watts via RAPL energy delta + GPU watts (amdgpu hwmon). Note: RAPL energy_uj is root-only by default (Spectre-era lockdown), so CPU package power shows "—" unless made readable (a udev rule); GPU watts work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useClient } from 'hooks/useClient';
|
||||
import { Cpu, MemoryStick, HardDrive, Server, Thermometer } from 'lucide-react';
|
||||
import { Cpu, MemoryStick, HardDrive, Server, Thermometer, CircuitBoard, Network, Zap } from 'lucide-react';
|
||||
import { fmtBytes, fmtDuration } from './shared';
|
||||
|
||||
type Sensor = { name: string; label: string; celsius: number };
|
||||
@@ -14,6 +14,9 @@ type Stats = {
|
||||
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;
|
||||
gpu: { busyPct: number; vramUsedBytes: number; vramTotalBytes: number } | null;
|
||||
net: { rxBytesPerSec: number; txBytesPerSec: number; interfaces: { name: string; rxBytesPerSec: number; txBytesPerSec: number }[] } | null;
|
||||
power: { cpuWatts: number | null; gpuWatts: number | null } | null;
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
@@ -21,6 +24,7 @@ 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 fmtRate = (bps: number) => `${fmtBytes(bps)}/s`;
|
||||
|
||||
const Bar = ({ pct }: { pct: number }) => (
|
||||
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
|
||||
@@ -138,7 +142,22 @@ export const BtopView = () => {
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Temperature" icon={Thermometer} className="lg:col-start-2">
|
||||
<Card title="GPU" icon={CircuitBoard}>
|
||||
{stats.gpu ? (
|
||||
<>
|
||||
<div className="flex items-end justify-between">
|
||||
<span className="text-3xl font-semibold text-foreground">{stats.gpu.busyPct}%</span>
|
||||
<span className="text-xs text-muted-foreground">VRAM {fmtBytes(stats.gpu.vramUsedBytes)} / {fmtBytes(stats.gpu.vramTotalBytes)}</span>
|
||||
</div>
|
||||
<Bar pct={stats.gpu.busyPct} />
|
||||
{stats.gpu.vramTotalBytes > 0 && <Bar pct={(stats.gpu.vramUsedBytes / stats.gpu.vramTotalBytes) * 100} />}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">no gpu</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Temperature" icon={Thermometer}>
|
||||
{stats.temp?.cpuC != null ? (
|
||||
<>
|
||||
<div className="flex items-end justify-between">
|
||||
@@ -161,6 +180,45 @@ export const BtopView = () => {
|
||||
<p className="text-sm text-muted-foreground">no sensors</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Network" icon={Network}>
|
||||
{stats.net ? (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-lg font-semibold text-foreground">
|
||||
<span>↓ {fmtRate(stats.net.rxBytesPerSec)}</span>
|
||||
<span>↑ {fmtRate(stats.net.txBytesPerSec)}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 pt-1">
|
||||
{stats.net.interfaces.slice(0, 4).map((i) => (
|
||||
<div key={i.name} className="flex items-center justify-between text-xs text-muted-foreground">
|
||||
<span className="truncate font-mono">{i.name}</span>
|
||||
<span className="font-mono">↓{fmtRate(i.rxBytesPerSec)} ↑{fmtRate(i.txBytesPerSec)}</span>
|
||||
</div>
|
||||
))}
|
||||
{stats.net.interfaces.length === 0 && <span className="text-xs text-muted-foreground">idle</span>}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">unavailable</p>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Card title="Power" icon={Zap}>
|
||||
{stats.power ? (
|
||||
<div className="flex flex-col gap-2 pt-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">CPU package</span>
|
||||
<span className="font-mono text-foreground">{stats.power.cpuWatts != null ? `${stats.power.cpuWatts} W` : '—'}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">GPU</span>
|
||||
<span className="font-mono text-foreground">{stats.power.gpuWatts != null ? `${stats.power.gpuWatts} W` : '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-muted-foreground">unavailable</p>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title="Top processes" icon={Server}>
|
||||
|
||||
Reference in New Issue
Block a user