Dashboard: Processes module (top by CPU/memory, PID, user, threads, SIGTERM), sampled at most every 2 s

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 05:58:11 -07:00
co-authored by Claude Fable 5.1
parent 4986af5ac9
commit ff922bda74
5 changed files with 219 additions and 1 deletions
+34
View File
@@ -2,6 +2,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from 'react'
import { api, DashPrefs, Sample, Series, Temp } from './api'
import Sparkline from './Sparkline'
import { Loading, Spinner, toast } from './ui'
import { useState as useStateP } from 'react'
const MAX = 300
const S1 = 'var(--s1)', S2 = 'var(--s2)'
@@ -20,6 +21,7 @@ const MODULES: Module[] = [
{ id: 'disks', label: 'Disks', kind: 'card' },
{ id: 'fs', label: 'Filesystems', kind: 'card' },
{ id: 'ups', label: 'UPS', kind: 'card' },
{ id: 'procs', label: 'Processes', kind: 'card' },
]
const byId = (id: string) => MODULES.find((m) => m.id === id)
@@ -108,6 +110,36 @@ function Tile({ label, value, sub, children }: { label: string; value: string; s
return <><div className="label">{label}</div><div className="value">{value}</div>{sub && <div className="sub">{sub}</div>}{children}</>
}
// Processes card keeps its own sort state, so it's a component rather than a plain body.
function Procs({ s, edit }: { s: Sample; edit: Edit }) {
const [by, setBy] = useStateP<'cpu' | 'rss'>('cpu')
const rows = [...s.procs].filter((p) => !edit.hiddenItems.has(`proc:${p.name}`)).sort((a, b) => (by === 'cpu' ? b.cpu - a.cpu : b.rss - a.rss)).slice(0, 12)
const kill = async (p: Sample['procs'][number]) => {
if (!window.confirm(`Send SIGTERM to ${p.name} (pid ${p.pid})?`)) return
try { await api.procSignal(p.pid, 'TERM'); toast(`SIGTERM sent to ${p.name}`) } catch (e) { toast(e instanceof Error ? e.message : 'failed', 'err') }
}
return (
<>
<div className="sub-head"><h2 style={{ margin: 0 }}>Processes</h2><span className="spacer" />
<button className={'chip' + (by === 'cpu' ? ' on' : '')} onClick={() => setBy('cpu')}>CPU</button>
<button className={'chip' + (by === 'rss' ? ' on' : '')} onClick={() => setBy('rss')}>Memory</button>
</div>
<div className="procs">
{rows.map((p) => (
<div className="proc" key={p.pid} title={p.cmd || p.name}>
<span className="pname"><span>{p.name}</span><small>{p.pid} · {p.user}{p.threads > 1 ? ` · ${p.threads} thr` : ''}</small></span>
<span className="num">{p.cpu.toFixed(1)}%</span>
<span className="num muted">{fmtBytes(p.rss)}</span>
{edit.on ? <HideBtn k={`proc:${p.name}`} edit={edit} /> : <button className="hide-item" title="send SIGTERM" onClick={() => kill(p)}>✕</button>}
</div>
))}
{rows.length === 0 && <div className="empty">No process data yet.</div>}
</div>
<HiddenChips prefix="proc" edit={edit} label={(k) => k} />
</>
)
}
function body(id: string, { s, h, edit }: Ctx): ReactNode {
const shown = (key: string) => !edit.hiddenItems.has(key)
const memFrac = s.mem.total ? s.mem.used / s.mem.total : 0
@@ -230,6 +262,8 @@ function body(id: string, { s, h, edit }: Ctx): ReactNode {
<HiddenChips prefix="fs" edit={edit} label={(k) => k} />
</>
)
case 'procs':
return <Procs s={s} edit={edit} />
case 'ups':
return (
<>