import React, { useCallback, useEffect, useState } from 'react' import { api, Proc, ProcTable } from './api' import { fmtBytes, fmtRate } from './Dashboard' import { Btn, Loading, toast } from './ui' import { useRefreshMs } from './prefs' type Col = 'cpu' | 'rss' | 'pid' | 'name' | 'user' | 'started' | 'cpu_time' const pct = (v: number) => `${Math.round(v)}%` const fmtUptime = (s: number) => { const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); return d > 0 ? `${d}d ${h}h` : h > 0 ? `${h}h ${m}m` : `${m}m` } const fmtDur = (s: number) => (s >= 3600 ? `${Math.floor(s / 3600)}h${String(Math.floor((s % 3600) / 60)).padStart(2, '0')}` : s >= 60 ? `${Math.floor(s / 60)}m${String(Math.floor(s % 60)).padStart(2, '0')}` : `${s.toFixed(1)}s`) const fmtStart = (ms: number) => { const d = new Date(ms), now = new Date(); return d.toDateString() === now.toDateString() ? d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : d.toLocaleDateString(undefined, { month: 'short', day: '2-digit' }) } const STATE: Record = { R: 'running', S: 'sleeping', D: 'disk wait', Z: 'zombie', T: 'stopped', t: 'traced', I: 'idle' } const BLOCKS = '▁▂▃▄▅▆▇█' // btop-style gradient bar: the track carries the green→yellow→red gradient and a cover // from the right hides the unfilled part, so colour depends on position not on level. function GBar({ frac }: { frac: number }) { return
} function Row({ label, frac, value }: { label: string; frac: number; value: string }) { return
{label}{value}
} // Block-character graph, coloured per column by its value, like btop's cpu graph. function Graph({ values, max = 100 }: { values: number[]; max?: number }) { const cols = 60 const v = values.slice(-cols) const pad = cols - v.length return ( ) } function Box({ title, right, children, className = '' }: { title: string; right?: string; children: React.ReactNode; className?: string }) { return
{title}{right && {right}}{children}
} export default function Procs() { const [t, setT] = useState(null) const [err, setErr] = useState('') const [sort, setSort] = useState('cpu') const [asc, setAsc] = useState(false) const [q, setQ] = useState('') const [mine, setMine] = useState(false) const [sel, setSel] = useState(null) const [paused, setPaused] = useState(false) const [showTop, setShowTop] = useState(() => localStorage.getItem('hamlab.procs.top') !== '0') const interval = useRefreshMs() const load = useCallback(async () => { try { setT(await api.procs()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') } }, []) useEffect(() => { load() const id = window.setInterval(() => { if (!paused && document.visibilityState === 'visible') load() }, interval) return () => window.clearInterval(id) }, [load, paused, interval]) const head = (c: Col, label: string, cls = '') => ( ) if (!t) return
{err ?
{err}
: }
const needle = q.trim().toLowerCase() const rows = t.procs .filter((p) => (!needle || p.name.toLowerCase().includes(needle) || (p.cmd ?? '').toLowerCase().includes(needle) || p.user.toLowerCase().includes(needle) || String(p.pid) === needle) && (!mine || p.user !== 'root')) .sort((a, b) => { const va = a[sort] as number | string, vb = b[sort] as number | string const r = typeof va === 'string' ? (va as string).localeCompare(vb as string) : (va as number) - (vb as number) return asc ? r : -r }) const selected = sel !== null ? t.procs.find((p) => p.pid === sel) : undefined const memFrac = t.mem.total ? t.mem.used / t.mem.total : 0 const swapFrac = t.swap.total ? t.swap.used / t.swap.total : 0 const signal = async (p: Proc, sig: 'TERM' | 'KILL') => { if (!window.confirm(`Send SIG${sig} to ${p.name} (pid ${p.pid})?`)) return try { await api.procSignal(p.pid, sig); toast(`SIG${sig} sent to ${p.name}`); load() } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') } } return (
{showTop &&
CPU{pct(t.cpu)}total, all cores
{t.cores.map((c, i) => )}
load avg {t.load[0].toFixed(2)} {t.load[1].toFixed(2)} {t.load[2].toFixed(2)} (1 · 5 · 15 min, {t.cores.length} cores) up {fmtUptime(t.uptime)} {t.total} tasks, {t.running} running
{t.swap.total > 0 ? ( <>
swap · {fmtBytes(t.swap.total)} total
) :
no swap
}
Used = total − available. Cached is file cache the kernel gives back under pressure, so "available" is the honest headroom.
{t.net.filter((n) => n.up).map((n) => (
{n.iface} ▼ {fmtRate(n.rx_bps)} ▲ {fmtRate(n.tx_bps)} {fmtBytes(n.rx_total)} in · {fmtBytes(n.tx_total)} out
))} {t.net.filter((n) => n.up).length === 0 &&
no interfaces up
}
}
setQ(e.target.value)} /> {rows.length} shown
{head('pid', 'PID', 'num')}{head('user', 'User')}{head('name', 'Program')}{head('cpu', 'CPU %', 'num')}{head('rss', 'Memory', 'num')}{head('cpu_time', 'CPU time', 'num')}{head('started', 'Started', 'num')}
{rows.map((p) => (
setSel(p.pid === sel ? null : p.pid)} title={p.cmd || p.name}> {p.pid} {p.user} {p.name}{p.threads > 1 && ×{p.threads}} = 50 ? ' hot' : '')}>{p.cpu.toFixed(1)} {fmtBytes(p.rss)} {fmtDur(p.cpu_time)} {fmtStart(p.started)}
))}
{selected && (
{selected.name}pid {selected.pid} · parent {selected.ppid} · {selected.user} · {STATE[selected.state] ?? selected.state} · nice {selected.nice} · {selected.threads} threads
{selected.cmd || '(kernel thread)'}
signal(selected, 'TERM')}>Terminate signal(selected, 'KILL')}>Kill −9 cpu {selected.cpu.toFixed(1)}% · {fmtBytes(selected.rss)} · running {fmtDur(selected.cpu_time)} of cpu since {new Date(selected.started).toLocaleString()}
)}
) }