Processes tab: whole page scrolls, tools + column header pinned together, detail sheet sticks to the bottom, metrics boxes collapsible

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 06:12:29 -07:00
co-authored by Claude Fable 5.1
parent b802dcf313
commit bd475eaafd
2 changed files with 17 additions and 10 deletions
+7 -3
View File
@@ -46,6 +46,7 @@ export default function Procs() {
const [mine, setMine] = useState(false) const [mine, setMine] = useState(false)
const [sel, setSel] = useState<number | null>(null) const [sel, setSel] = useState<number | null>(null)
const [paused, setPaused] = useState(false) const [paused, setPaused] = useState(false)
const [showTop, setShowTop] = useState<boolean>(() => localStorage.getItem('hamlab.procs.top') !== '0')
const [interval, setIntervalMs] = useState<number>(() => Number(localStorage.getItem('hamlab.procs.interval')) || 2000) const [interval, setIntervalMs] = useState<number>(() => Number(localStorage.getItem('hamlab.procs.interval')) || 2000)
const load = useCallback(async () => { const load = useCallback(async () => {
@@ -84,7 +85,7 @@ export default function Procs() {
return ( return (
<div className="procs-page"> <div className="procs-page">
<div className="procs-top"> {showTop && <div className="procs-top">
<Box title="cpu" right={`${t.cpu_model ?? ''}${t.cpu_mhz ? ` · ${(t.cpu_mhz / 1000).toFixed(2)} GHz` : ''}${t.cpu_temp ? ` · ${t.cpu_temp.toFixed(0)}°C` : ''}`}> <Box title="cpu" right={`${t.cpu_model ?? ''}${t.cpu_mhz ? ` · ${(t.cpu_mhz / 1000).toFixed(2)} GHz` : ''}${t.cpu_temp ? ` · ${t.cpu_temp.toFixed(0)}°C` : ''}`}>
<div className="cpu-total"> <div className="cpu-total">
<Graph values={t.cpu_hist} /> <Graph values={t.cpu_hist} />
@@ -121,8 +122,10 @@ export default function Procs() {
))} ))}
{t.net.filter((n) => n.up).length === 0 && <div className="muted">no interfaces up</div>} {t.net.filter((n) => n.up).length === 0 && <div className="muted">no interfaces up</div>}
</Box> </Box>
</div> </div>}
<div className="procs-sticky">
<div className="procs-tools"> <div className="procs-tools">
<button className={'chip' + (showTop ? ' on' : '')} title="show or hide the cpu/mem/net boxes" onClick={() => { setShowTop(!showTop); localStorage.setItem('hamlab.procs.top', showTop ? '0' : '1') }}>{showTop ? 'metrics ▴' : 'metrics ▾'}</button>
<input placeholder="filter name, command, user, pid" value={q} onChange={(e) => setQ(e.target.value)} /> <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' + (mine ? ' on' : '')} onClick={() => setMine(!mine)}>hide root</button>
<button className={'chip' + (paused ? ' on' : '')} onClick={() => setPaused(!paused)}>{paused ? 'paused' : 'live'}</button> <button className={'chip' + (paused ? ' on' : '')} onClick={() => setPaused(!paused)}>{paused ? 'paused' : 'live'}</button>
@@ -134,10 +137,11 @@ export default function Procs() {
<span className="muted small">{rows.length} shown</span> <span className="muted small">{rows.length} shown</span>
</div> </div>
<div className="ptable">
<div className="prow phead"> <div className="prow phead">
{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')} {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')}
</div> </div>
</div>
<div className="ptable">
{rows.map((p) => ( {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}> <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="num muted">{p.pid}</span>
+8 -5
View File
@@ -289,7 +289,8 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
} }
/* processes tab — btop-style boxes */ /* processes tab — btop-style boxes */
.procs-page { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--bg); } .procs-page { position: absolute; inset: 0; display: flex; flex-direction: column; background: var(--bg); overflow-y: auto; -webkit-overflow-scrolling: touch; }
.procs-page > * { flex-shrink: 0; }
.procs-top { padding: 14px 10px 8px; display: grid; grid-template-columns: 1.4fr 1fr; gap: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; } .procs-top { padding: 14px 10px 8px; display: grid; grid-template-columns: 1.4fr 1fr; gap: 12px; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
.procs-top .netbox { grid-column: 1 / -1; } .procs-top .netbox { grid-column: 1 / -1; }
.box { position: relative; border: 1px solid var(--border); border-radius: 8px; padding: 12px 12px 8px; min-width: 0; } .box { position: relative; border: 1px solid var(--border); border-radius: 8px; padding: 12px 12px 8px; min-width: 0; }
@@ -311,21 +312,23 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
.cores .brow { grid-template-columns: 30px 1fr 42px; } .cores .bv { text-align: right; } .cores .brow { grid-template-columns: 30px 1fr 42px; } .cores .bv { text-align: right; }
.netrow { display: grid; grid-template-columns: 90px auto 1fr; gap: 12px; align-items: center; padding: 2px 0; } .netrow { display: grid; grid-template-columns: 90px auto 1fr; gap: 12px; align-items: center; padding: 2px 0; }
.netrow .dn { color: var(--accent2); } .netrow .upc { color: var(--s2); } .netrow .dn { color: var(--accent2); } .netrow .upc { color: var(--s2); }
.procs-tools { display: flex; gap: 6px; align-items: center; padding: 0 10px 8px; flex-wrap: wrap; } .procs-sticky { position: sticky; top: 0; z-index: 2; background: var(--bg); }
.procs-tools { display: flex; gap: 6px; align-items: center; padding: 8px 10px; flex-wrap: wrap; }
.procs-tools input { flex: 1; min-width: 140px; padding: 6px 8px; background: var(--bg2); border: 1px solid var(--border); border-radius: 5px; color: var(--fg); font-size: 13px; } .procs-tools input { flex: 1; min-width: 140px; padding: 6px 8px; background: var(--bg2); border: 1px solid var(--border); border-radius: 5px; color: var(--fg); font-size: 13px; }
.procs-tools label { display: inline-flex; align-items: center; gap: 4px; } .procs-tools select.btn { padding: 3px 6px; } .procs-tools label { display: inline-flex; align-items: center; gap: 4px; } .procs-tools select.btn { padding: 3px 6px; }
.ptable { flex: 1; overflow: auto; font-size: 13px; border-top: 1px solid var(--border); } .ptable { font-size: 13px; min-height: 50vh; }
.prow { display: grid; grid-template-columns: 60px 80px minmax(120px, 1fr) 60px 76px 70px 64px; gap: 8px; align-items: center; padding: 4px 10px; border-bottom: 1px solid var(--border); cursor: pointer; } .prow { display: grid; grid-template-columns: 60px 80px minmax(120px, 1fr) 60px 76px 70px 64px; gap: 8px; align-items: center; padding: 4px 10px; border-bottom: 1px solid var(--border); cursor: pointer; }
.prow:hover { background: var(--bg3); } .prow.active { background: var(--bg3); box-shadow: inset 3px 0 0 var(--accent); } .prow:hover { background: var(--bg3); } .prow.active { background: var(--bg3); box-shadow: inset 3px 0 0 var(--accent); }
.prow.zombie { color: var(--muted); font-style: italic; } .prow.running .mono { color: var(--ok); } .prow.zombie { color: var(--muted); font-style: italic; } .prow.running .mono { color: var(--ok); }
.prow .num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, monospace; font-size: 12px; } .prow .hot { color: var(--s2); } .prow .num { text-align: right; font-variant-numeric: tabular-nums; font-family: ui-monospace, monospace; font-size: 12px; } .prow .hot { color: var(--s2); }
.prow .ell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .prow .ell { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.phead { position: sticky; top: 0; background: var(--bg2); z-index: 1; cursor: default; padding: 0 10px; } .phead { background: var(--bg2); cursor: default; padding: 0 10px; border-top: 1px solid var(--border); }
.th { padding: 6px 0; text-align: left; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; } .th.num { text-align: right; } .th.on { color: var(--fg); } .th { padding: 6px 0; text-align: left; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; } .th.num { text-align: right; } .th.on { color: var(--fg); }
.pdetail { border-top: 1px solid var(--border); background: var(--bg2); padding: 10px; display: flex; flex-direction: column; gap: 8px; } .pdetail { position: sticky; bottom: 0; z-index: 3; border-top: 1px solid var(--border); background: var(--bg2); padding: 10px; display: flex; flex-direction: column; gap: 8px; box-shadow: 0 -6px 16px rgba(0,0,0,.4); }
.cmdline { font-size: 12px; word-break: break-all; white-space: pre-wrap; color: var(--fg); background: #0d1117; padding: 8px; border-radius: 6px; border: 1px solid var(--border); max-height: 120px; overflow: auto; } .cmdline { font-size: 12px; word-break: break-all; white-space: pre-wrap; color: var(--fg); background: #0d1117; padding: 8px; border-radius: 6px; border: 1px solid var(--border); max-height: 120px; overflow: auto; }
@media (max-width: 800px) { @media (max-width: 800px) {
.procs-top { grid-template-columns: 1fr; gap: 14px; padding-top: 16px; } .procs-top { grid-template-columns: 1fr; gap: 14px; padding-top: 16px; }
.cmdline { max-height: 72px; }
.prow { grid-template-columns: 52px minmax(100px, 1fr) 56px 70px; } .prow > :nth-child(2), .prow > :nth-child(6), .prow > :nth-child(7) { display: none; } .prow { grid-template-columns: 52px minmax(100px, 1fr) 56px 70px; } .prow > :nth-child(2), .prow > :nth-child(6), .prow > :nth-child(7) { display: none; }
.cores { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); } .cores { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
.graph { font-size: 16px; } .graph { font-size: 16px; }