From 1eb59fd08c79ed4ccd1c61fadee60314a08feed7 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Wed, 23 Sep 2026 22:58:06 -0700 Subject: [PATCH] One tab called Processes, showing what Top showed The two read the same data and answered the same question. Procs.tsx is gone; a rail order saved before this keeps the merged page in Top's slot. The mouse back-button item is dropped from the plan. Co-Authored-By: Claude Opus 5 --- PLAN.md | 33 ++++----- web/src/Procs.tsx | 166 ---------------------------------------------- web/src/Shell.tsx | 13 ++-- 3 files changed, 17 insertions(+), 195 deletions(-) delete mode 100644 web/src/Procs.tsx diff --git a/PLAN.md b/PLAN.md index 6f846a4..128a77a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -252,6 +252,13 @@ something long in one, switch away and back, refresh — it's all still there. use", and "Remove unused" takes every image no container uses. Nothing is ever forced: Docker refuses what is still in use, and that refusal is the answer. Still absent: removing networks. +- **Top merged into Processes** — **2026-09-23.** The two tabs answered the same question + from the same data (Top already read `/api/procs`, the endpoint the Processes table was + built on), so at Mitchell's instruction there is now one tab, **Processes**, showing what + Top showed: the btop-style whole-machine screen. `Procs.tsx` is deleted; the `top` view + id and its route are gone. A rail order saved before this keeps its shape — `railorder.ts` + maps the retired `top` to `procs` and drops the duplicate, so the merged page sits where + Top sat rather than falling to the bottom. - **Skins** — 13 since 2026-09-19 (`themes.ts`: a palette is one `mk(…)` line): Dark, Light, Cyberpunk, Retro, **HUD** (hairlines of cyan on near-black, corner ticks, thin type), Nord, Gruvbox, Catppuccin + Latte, Dracula, Tokyo Night, Solarized Dark + Light; @@ -487,16 +494,6 @@ something long in one, switch away and back, refresh — it's all still there. - *Not app work, but the trigger:* nothing here blocks starting the migration except the inventory. Do that first. -- **Keep Processes or Top, not both** — asked for 2026-09-23, not started. They answer the - same question and read the same data: `Top.tsx` already fetches `/api/procs` for its - process panel, the same endpoint `Procs.tsx` is built on, and both draw the same CPU, - memory, load and network numbers. Top is the btop-style whole-machine screen; Processes - is the sortable table with filter, kill and signal. Decide which survives and fold the - other's worth into it — most likely Top stays and gains Processes' sorting, filtering and - signal actions, since one screen beats two. Whichever goes, its rail entry, its `View` id - and its hash route go with it, and any saved rail order naming it must degrade quietly - (`railorder.ts` already ignores ids it does not recognise). - - **Rename the Space tab** — asked for 2026-09-23, not started. "Space" is vague; the tab is WizTree for a disk or pool. Candidates: **Usage**, **Disk Usage**, **Storage**. Note `Disks` already exists and `Storage` would muddle with it, so Usage is probably the one. @@ -535,10 +532,10 @@ something long in one, switch away and back, refresh — it's all still there. at all, which is every host here today. Worth scoping as its own plan before starting. - **Deprecate the Dashboard tab** — asked for 2026-09-23, not started. Mitchell: it is nice - but the app repeats itself, and this goes together with choosing between Processes and - Top. Dashboard is the customisable module list (CPU, memory, load, uptime, temps, fans, - network, disks, filesystems, UPS) plus custom sensor widgets; Top shows most of the same - numbers on one screen. **Widgets stays**, reframed: its page should say plainly that the + but the app repeats itself — the same reasoning that merged Top into Processes the same + day. Dashboard is the customisable module list (CPU, memory, load, uptime, temps, fans, + network, disks, filesystems, UPS) plus custom sensor widgets; the Processes tab shows + most of the same numbers on one screen already. **Widgets stays**, reframed: its page should say plainly that the widgets there are for **external dashboards** (Glance, Home Assistant, a wall display) via `/embed/widget` and read-only sensor tokens, not a second copy of this console's own screens. Care needed on the way out: the dashboard layout is per-user in `state.json` @@ -547,13 +544,7 @@ something long in one, switch away and back, refresh — it's all still there. source. `dashboard` is also the default view after login and after switching machines. - **Files explorer: cleanup and ease of use** — asked for 2026-09-23, not started. - Specifically named: **the mouse's back button must go up a directory**. Why it does not - today — `hashsel.ts` navigates with `history.replaceState`, which deliberately leaves no - history entry, so there is nothing for the browser (or the mouse's back button, which is - just `history.back()`) to return to. Fixing it properly means Files pushing a history - entry per folder and answering `popstate`, which `useHashSel` already listens for; doing - it in `hashsel.ts` would change every page that uses it, so decide whether history is - per-page opt-in. Beyond that the ask is general polish, and the **Deprecate copyparty** + General polish and ease of use. The **Deprecate copyparty** item names the heavier pieces (multi-select and bulk actions, folder download, resumable uploads, a gallery, search) — worth doing the two together, since they are the same tab. diff --git a/web/src/Procs.tsx b/web/src/Procs.tsx deleted file mode 100644 index 4b91d8c..0000000 --- a/web/src/Procs.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import React, { useCallback, useEffect, useState } from 'react' -import { api, Proc, ProcTable } from './api' -import { fmtBytes, fmtRate } from './Dashboard' -import { Btn, Loading, askYes, 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 (!await askYes(`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.length === 0 &&
Nothing matches.
} - {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()} -
-
- )} -
- ) -} diff --git a/web/src/Shell.tsx b/web/src/Shell.tsx index e155e0d..f0366f5 100644 --- a/web/src/Shell.tsx +++ b/web/src/Shell.tsx @@ -13,7 +13,6 @@ import Cron from './Cron' import Updates from './Updates' import Disks from './Disks' import Files from './Files' -import Procs from './Procs' import Top from './Top' import Machines from './Machines' import Settings from './Settings' @@ -53,8 +52,7 @@ const BellIcon = ( const GROUPS: Group[] = [ { id: 'dashboard', label: 'Dashboard', icon: '◔' }, - { id: 'top', label: 'Top', icon: '▚' }, - { id: 'procs', label: 'Processes', icon: '≡' }, + { id: 'procs', label: 'Processes', icon: '▚' }, { id: 'terminal', label: 'Terminal', icon: '>_' }, { id: 'services', label: 'Services', icon: '◈' }, { id: 'docker', label: 'Docker', icon: DockerIcon }, @@ -70,8 +68,8 @@ const GROUPS: Group[] = [ { id: 'settings', label: 'Settings', icon: '⚙' }, ] -type View = 'terminal' | 'dashboard' | 'top' | 'widgets' | 'docker' | 'alerts' | 'services' | 'cron' | 'updates' | 'disks' | 'space' | 'files' | 'procs' | 'machines' | 'users' | 'settings' -const VIEWS: View[] = ['terminal', 'dashboard', 'top', 'widgets', 'docker', 'alerts', 'services', 'cron', 'updates', 'disks', 'space', 'files', 'procs', 'machines', 'users', 'settings'] +type View = 'terminal' | 'dashboard' | 'widgets' | 'docker' | 'alerts' | 'services' | 'cron' | 'updates' | 'disks' | 'space' | 'files' | 'procs' | 'machines' | 'users' | 'settings' +const VIEWS: View[] = ['terminal', 'dashboard', 'widgets', 'docker', 'alerts', 'services', 'cron', 'updates', 'disks', 'space', 'files', 'procs', 'machines', 'users', 'settings'] const HUB_ONLY: View[] = ['machines', 'users', 'settings', 'alerts'] // URL hash is the source of truth for where you are: #//. Back and @@ -272,8 +270,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut: {view === 'widgets' && } {view === 'docker' && } {view === 'alerts' && m.name)} titleOf={(n) => titleOf(n === hubMe.host ? null : n, machines, hubMe.host)} />} - {view === 'top' && } - {view === 'procs' && } + {view === 'procs' && } {view === 'services' && } {view === 'cron' && } {view === 'updates' && } @@ -304,7 +301,7 @@ export default function Shell({ me: hubMe, onLoggedOut }: { me: Me; onLoggedOut: {machine ? `${title} · ` : ''}{me.version.startsWith('v') ? me.version : 'v' + me.version} up {fmtUptime(me.uptime)} - {view === 'top' ? 'Top' : view === 'dashboard' ? 'Dashboard' : view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'cron' ? 'Cron' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'space' ? 'Space' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''} + {view === 'dashboard' ? 'Dashboard' : view === 'widgets' ? 'Widgets' : view === 'docker' ? 'Docker' : view === 'alerts' ? 'Alerts' : view === 'services' ? 'Services' : view === 'cron' ? 'Cron' : view === 'updates' ? 'Updates' : view === 'disks' ? 'Disks' : view === 'space' ? 'Space' : view === 'files' ? 'Files' : view === 'procs' ? 'Processes' : view === 'machines' ? 'Machines' : view === 'users' ? 'Users' : view === 'settings' ? 'Settings' : activeTerm ? activeTerm.name : ''}
)