Processes tab (btop-style): every process with CPU%, memory, cpu time, start, sortable + filter, per-core and memory bars, detail pane with terminate/kill; module removed from the dashboard; sampling only while the tab is open
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
ff922bda74
commit
e4df878ac0
@@ -0,0 +1,114 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { api, Proc, ProcTable } from './api'
|
||||
import { fmtBytes } from './Dashboard'
|
||||
import { Btn, Loading, toast } from './ui'
|
||||
|
||||
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<string, string> = { R: 'running', S: 'sleeping', D: 'disk wait', Z: 'zombie', T: 'stopped', t: 'traced', I: 'idle' }
|
||||
|
||||
function Bar({ frac, label, cls }: { frac: number; label: string; cls?: string }) {
|
||||
return (
|
||||
<div className="pbar" title={label}>
|
||||
<div className="meter"><div className={'fill ' + (cls ?? (frac >= 0.9 ? 'bad' : frac >= 0.7 ? 'warn' : ''))} style={{ width: `${Math.min(100, frac * 100)}%` }} /></div>
|
||||
<span className="num">{label}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Procs() {
|
||||
const [t, setT] = useState<ProcTable | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [sort, setSort] = useState<Col>('cpu')
|
||||
const [asc, setAsc] = useState(false)
|
||||
const [q, setQ] = useState('')
|
||||
const [mine, setMine] = useState(false)
|
||||
const [sel, setSel] = useState<number | null>(null)
|
||||
const [paused, setPaused] = useState(false)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try { setT(await api.procs()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
load()
|
||||
const id = setInterval(() => { if (!paused && document.visibilityState === 'visible') load() }, 2000)
|
||||
return () => clearInterval(id)
|
||||
}, [load, paused])
|
||||
|
||||
const head = (c: Col, label: string, cls = '') => (
|
||||
<button className={'th ' + cls + (sort === c ? ' on' : '')} onClick={() => { if (sort === c) setAsc(!asc); else { setSort(c); setAsc(c === 'name' || c === 'user' || c === 'pid') } }}>
|
||||
{label}{sort === c ? (asc ? ' ▲' : ' ▼') : ''}
|
||||
</button>
|
||||
)
|
||||
|
||||
if (!t) return <div className="dash">{err ? <div className="error">{err}</div> : <Loading label="Reading processes…" />}</div>
|
||||
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 (
|
||||
<div className="procs-page">
|
||||
<div className="procs-top">
|
||||
<div className="cores">
|
||||
{t.cores.map((c, i) => <Bar key={i} frac={c / 100} label={`${i}: ${pct(c)}`} />)}
|
||||
</div>
|
||||
<div className="memrow">
|
||||
<Bar frac={memFrac} label={`mem ${fmtBytes(t.mem.used)} / ${fmtBytes(t.mem.total)}`} />
|
||||
{t.swap.total > 0 && <Bar frac={swapFrac} label={`swap ${fmtBytes(t.swap.used)} / ${fmtBytes(t.swap.total)}`} />}
|
||||
<span className="num muted">load {t.load.map((l) => l.toFixed(2)).join(' ')} · up {fmtUptime(t.uptime)} · {t.total} tasks · cpu {pct(t.cpu)}</span>
|
||||
</div>
|
||||
<div className="procs-tools">
|
||||
<input placeholder="filter name, command, user, pid" value={q} onChange={(e) => setQ(e.target.value)} />
|
||||
<button className={'chip' + (mine ? ' on' : '')} onClick={() => setMine(!mine)}>hide root</button>
|
||||
<button className={'chip' + (paused ? ' on' : '')} onClick={() => setPaused(!paused)}>{paused ? 'paused' : 'live'}</button>
|
||||
<span className="muted">{rows.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="ptable">
|
||||
<div className="prow phead">
|
||||
{head('pid', 'PID', 'num')}{head('user', 'User')}{head('name', 'Name')}{head('cpu', 'CPU%', 'num')}{head('rss', 'MEM', 'num')}{head('cpu_time', 'Time', 'num')}{head('started', 'Started', 'num')}
|
||||
</div>
|
||||
{rows.map((p) => (
|
||||
<div key={p.pid} className={'prow' + (p.pid === sel ? ' active' : '') + (p.state === 'Z' ? ' zombie' : p.state === 'R' ? ' running' : '')} onClick={() => setSel(p.pid === sel ? null : p.pid)} title={p.cmd || p.name}>
|
||||
<span className="num muted">{p.pid}</span>
|
||||
<span className="ell">{p.user}</span>
|
||||
<span className="ell mono">{p.name}{p.threads > 1 && <small className="muted"> ×{p.threads}</small>}</span>
|
||||
<span className={'num' + (p.cpu >= 50 ? ' hot' : '')}>{p.cpu.toFixed(1)}</span>
|
||||
<span className="num">{fmtBytes(p.rss)}</span>
|
||||
<span className="num muted">{fmtDur(p.cpu_time)}</span>
|
||||
<span className="num muted">{fmtStart(p.started)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selected && (
|
||||
<div className="pdetail">
|
||||
<div className="sub-head"><b className="mono">{selected.name}</b><span className="muted">pid {selected.pid} · parent {selected.ppid} · {selected.user} · {STATE[selected.state] ?? selected.state} · nice {selected.nice} · {selected.threads} threads</span><span className="spacer" /><button className="icon-btn" onClick={() => setSel(null)}>✕</button></div>
|
||||
<div className="mono cmdline">{selected.cmd || '(kernel thread)'}</div>
|
||||
<div className="svc-actions">
|
||||
<Btn className="btn" onClick={() => signal(selected, 'TERM')}>Terminate</Btn>
|
||||
<Btn className="btn danger" onClick={() => signal(selected, 'KILL')}>Kill −9</Btn>
|
||||
<span className="muted">cpu {selected.cpu.toFixed(1)}% · {fmtBytes(selected.rss)} · running {fmtDur(selected.cpu_time)} of cpu since {new Date(selected.started).toLocaleString()}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user