Processes tab: btop-style cpu/mem/net boxes with labels, gradient bars, block graph, CPU model/GHz/temp, memory breakdown, per-interface rates, and a refresh interval control
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e4df878ac0
commit
b802dcf313
+86
-15
@@ -37,9 +37,12 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Mem struct {
|
type Mem struct {
|
||||||
Total uint64 `json:"total"`
|
Total uint64 `json:"total"`
|
||||||
Used uint64 `json:"used"`
|
Used uint64 `json:"used"`
|
||||||
Avail uint64 `json:"avail"`
|
Avail uint64 `json:"avail"`
|
||||||
|
Free uint64 `json:"free,omitempty"`
|
||||||
|
Cached uint64 `json:"cached,omitempty"`
|
||||||
|
Buffers uint64 `json:"buffers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Temp struct {
|
type Temp struct {
|
||||||
@@ -379,7 +382,7 @@ func meminfo() (Mem, Mem) {
|
|||||||
n, _ := strconv.ParseUint(f[0], 10, 64)
|
n, _ := strconv.ParseUint(f[0], 10, 64)
|
||||||
m[k] = n * 1024
|
m[k] = n * 1024
|
||||||
}
|
}
|
||||||
mem := Mem{Total: m["MemTotal"], Avail: m["MemAvailable"]}
|
mem := Mem{Total: m["MemTotal"], Avail: m["MemAvailable"], Free: m["MemFree"], Buffers: m["Buffers"], Cached: m["Cached"] + m["SReclaimable"]}
|
||||||
if mem.Avail == 0 { // pre-3.14 kernels: approximate
|
if mem.Avail == 0 { // pre-3.14 kernels: approximate
|
||||||
mem.Avail = m["MemFree"] + m["Buffers"] + m["Cached"]
|
mem.Avail = m["MemFree"] + m["Buffers"] + m["Cached"]
|
||||||
}
|
}
|
||||||
@@ -885,15 +888,69 @@ func uidName(uid string) string {
|
|||||||
|
|
||||||
// ProcTable is the Processes tab payload.
|
// ProcTable is the Processes tab payload.
|
||||||
type ProcTable struct {
|
type ProcTable struct {
|
||||||
T int64 `json:"t"`
|
T int64 `json:"t"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
Cores []float64 `json:"cores"`
|
Cores []float64 `json:"cores"`
|
||||||
Load [3]float64 `json:"load"`
|
CPUHist []float64 `json:"cpu_hist"` // recent total-CPU samples, oldest first
|
||||||
Mem Mem `json:"mem"`
|
CPUModel string `json:"cpu_model,omitempty"`
|
||||||
Swap Mem `json:"swap"`
|
CPUMHz float64 `json:"cpu_mhz,omitempty"`
|
||||||
Uptime float64 `json:"uptime"`
|
CPUTemp float64 `json:"cpu_temp,omitempty"`
|
||||||
Procs []Proc `json:"procs"`
|
Load [3]float64 `json:"load"`
|
||||||
Total int `json:"total"`
|
Mem Mem `json:"mem"`
|
||||||
|
Swap Mem `json:"swap"`
|
||||||
|
Net []Net `json:"net"`
|
||||||
|
Uptime float64 `json:"uptime"`
|
||||||
|
Procs []Proc `json:"procs"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Running int `json:"running"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var cpuModel = func() string {
|
||||||
|
for _, line := range strings.Split(readStr("/proc/cpuinfo"), "\n") {
|
||||||
|
if strings.HasPrefix(line, "model name") || strings.HasPrefix(line, "Model") || strings.HasPrefix(line, "Hardware") {
|
||||||
|
if _, v, ok := strings.Cut(line, ":"); ok {
|
||||||
|
return strings.Join(strings.Fields(v), " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}()
|
||||||
|
|
||||||
|
func cpuMHz() float64 {
|
||||||
|
var sum float64
|
||||||
|
n := 0
|
||||||
|
for _, line := range strings.Split(readStr("/proc/cpuinfo"), "\n") {
|
||||||
|
if strings.HasPrefix(line, "cpu MHz") {
|
||||||
|
if _, v, ok := strings.Cut(line, ":"); ok {
|
||||||
|
f, _ := strconv.ParseFloat(strings.TrimSpace(v), 64)
|
||||||
|
sum += f
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if n == 0 { // ARM etc.: scaling_cur_freq in kHz
|
||||||
|
if v, ok := readInt("/sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq"); ok {
|
||||||
|
return float64(v) / 1000
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return sum / float64(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cpuTemp picks the sensor that best represents the CPU package.
|
||||||
|
func cpuTemp(temps []Temp) float64 {
|
||||||
|
best, max := 0.0, 0.0
|
||||||
|
for _, t := range temps {
|
||||||
|
l := strings.ToLower(t.Label)
|
||||||
|
if t.Chip == "coretemp" && strings.HasPrefix(l, "package") || t.Chip == "k10temp" && (l == "tctl" || l == "tdie") ||
|
||||||
|
t.Chip == "thermal" && strings.Contains(l, "cpu") || t.Chip == "cpu_thermal" || t.Chip == "soc_thermal" {
|
||||||
|
return t.C
|
||||||
|
}
|
||||||
|
if (t.Chip == "coretemp" || t.Chip == "k10temp") && t.C > max {
|
||||||
|
max, best = t.C, t.C
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
}
|
}
|
||||||
|
|
||||||
var bootTime = func() int64 {
|
var bootTime = func() int64 {
|
||||||
@@ -997,10 +1054,24 @@ func (c *Collector) Processes() ProcTable {
|
|||||||
}
|
}
|
||||||
return procs[i].RSS > procs[j].RSS
|
return procs[i].RSS > procs[j].RSS
|
||||||
})
|
})
|
||||||
t := ProcTable{T: now.UnixMilli(), Procs: procs, Total: len(procs), Uptime: uptime(), Load: loadavg()}
|
t := ProcTable{T: now.UnixMilli(), Procs: procs, Total: len(procs), Uptime: uptime(), Load: loadavg(),
|
||||||
|
CPUModel: cpuModel, CPUMHz: round1(cpuMHz()), CPUHist: []float64{}, Net: []Net{}}
|
||||||
|
for _, p := range procs {
|
||||||
|
if p.State == "R" {
|
||||||
|
t.Running++
|
||||||
|
}
|
||||||
|
}
|
||||||
t.Mem, t.Swap = meminfo()
|
t.Mem, t.Swap = meminfo()
|
||||||
if n := len(c.ring); n > 0 {
|
if n := len(c.ring); n > 0 {
|
||||||
t.CPU, t.Cores = c.ring[n-1].CPU, c.ring[n-1].Cores
|
last := c.ring[n-1]
|
||||||
|
t.CPU, t.Cores, t.Net, t.CPUTemp = last.CPU, last.Cores, last.Net, cpuTemp(last.Temps)
|
||||||
|
from := n - 90
|
||||||
|
if from < 0 {
|
||||||
|
from = 0
|
||||||
|
}
|
||||||
|
for _, s := range c.ring[from:] {
|
||||||
|
t.CPUHist = append(t.CPUHist, s.CPU)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
c.lastTable = &t
|
c.lastTable = &t
|
||||||
return t
|
return t
|
||||||
|
|||||||
+78
-25
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
import React, { useCallback, useEffect, useState } from 'react'
|
||||||
import { api, Proc, ProcTable } from './api'
|
import { api, Proc, ProcTable } from './api'
|
||||||
import { fmtBytes } from './Dashboard'
|
import { fmtBytes, fmtRate } from './Dashboard'
|
||||||
import { Btn, Loading, toast } from './ui'
|
import { Btn, Loading, toast } from './ui'
|
||||||
|
|
||||||
type Col = 'cpu' | 'rss' | 'pid' | 'name' | 'user' | 'started' | 'cpu_time'
|
type Col = 'cpu' | 'rss' | 'pid' | 'name' | 'user' | 'started' | 'cpu_time'
|
||||||
@@ -10,14 +10,32 @@ const fmtDur = (s: number) => (s >= 3600 ? `${Math.floor(s / 3600)}h${String(Mat
|
|||||||
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 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' }
|
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 }) {
|
const INTERVALS = [500, 1000, 2000, 5000] as const
|
||||||
|
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 <div className="gbar"><div className="gcover" style={{ width: `${100 - Math.min(100, Math.max(0, frac * 100))}%` }} /></div>
|
||||||
|
}
|
||||||
|
function Row({ label, frac, value }: { label: string; frac: number; value: string }) {
|
||||||
|
return <div className="brow"><span className="bl">{label}</span><GBar frac={frac} /><span className="bv">{value}</span></div>
|
||||||
|
}
|
||||||
|
// 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 (
|
return (
|
||||||
<div className="pbar" title={label}>
|
<div className="graph" aria-hidden="true">
|
||||||
<div className="meter"><div className={'fill ' + (cls ?? (frac >= 0.9 ? 'bad' : frac >= 0.7 ? 'warn' : ''))} style={{ width: `${Math.min(100, frac * 100)}%` }} /></div>
|
{pad > 0 && <span className="gpad">{'\u2581'.repeat(pad)}</span>}
|
||||||
<span className="num">{label}</span>
|
{v.map((x, i) => { const f = Math.min(1, Math.max(0, x / max)); return <span key={i} className={f >= 0.8 ? 'g3' : f >= 0.5 ? 'g2' : 'g1'}>{BLOCKS[Math.min(7, Math.round(f * 7))]}</span> })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
function Box({ title, right, children, className = '' }: { title: string; right?: string; children: React.ReactNode; className?: string }) {
|
||||||
|
return <section className={'box ' + className}><span className="box-title">{title}</span>{right && <span className="box-right">{right}</span>}{children}</section>
|
||||||
|
}
|
||||||
|
|
||||||
export default function Procs() {
|
export default function Procs() {
|
||||||
const [t, setT] = useState<ProcTable | null>(null)
|
const [t, setT] = useState<ProcTable | null>(null)
|
||||||
@@ -28,18 +46,20 @@ 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 [interval, setIntervalMs] = useState<number>(() => Number(localStorage.getItem('hamlab.procs.interval')) || 2000)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
try { setT(await api.procs()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
try { setT(await api.procs()); setErr('') } catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
||||||
}, [])
|
}, [])
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
load()
|
load()
|
||||||
const id = setInterval(() => { if (!paused && document.visibilityState === 'visible') load() }, 2000)
|
const id = window.setInterval(() => { if (!paused && document.visibilityState === 'visible') load() }, interval)
|
||||||
return () => clearInterval(id)
|
localStorage.setItem('hamlab.procs.interval', String(interval))
|
||||||
}, [load, paused])
|
return () => window.clearInterval(id)
|
||||||
|
}, [load, paused, interval])
|
||||||
|
|
||||||
const head = (c: Col, label: string, cls = '') => (
|
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') } }}>
|
<button className={'th ' + cls + (sort === c ? ' on' : '')} title={{ cpu: 'share of one core, right now', rss: 'resident memory (RSS)', cpu_time: 'total CPU time used since it started', started: 'when the process started', pid: 'process id', name: 'program name', user: 'owner' }[c]} onClick={() => { if (sort === c) setAsc(!asc); else { setSort(c); setAsc(c === 'name' || c === 'user' || c === 'pid') } }}>
|
||||||
{label}{sort === c ? (asc ? ' ▲' : ' ▼') : ''}
|
{label}{sort === c ? (asc ? ' ▲' : ' ▼') : ''}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
@@ -65,25 +85,58 @@ export default function Procs() {
|
|||||||
return (
|
return (
|
||||||
<div className="procs-page">
|
<div className="procs-page">
|
||||||
<div className="procs-top">
|
<div className="procs-top">
|
||||||
<div className="cores">
|
<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` : ''}`}>
|
||||||
{t.cores.map((c, i) => <Bar key={i} frac={c / 100} label={`${i}: ${pct(c)}`} />)}
|
<div className="cpu-total">
|
||||||
</div>
|
<Graph values={t.cpu_hist} />
|
||||||
<div className="memrow">
|
<div className="cpu-total-v"><span className="bl">CPU</span><span className="bv big">{pct(t.cpu)}</span><span className="muted small">total, all cores</span></div>
|
||||||
<Bar frac={memFrac} label={`mem ${fmtBytes(t.mem.used)} / ${fmtBytes(t.mem.total)}`} />
|
</div>
|
||||||
{t.swap.total > 0 && <Bar frac={swapFrac} label={`swap ${fmtBytes(t.swap.used)} / ${fmtBytes(t.swap.total)}`} />}
|
<div className="cores">
|
||||||
<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>
|
{t.cores.map((c, i) => <Row key={i} label={`C${i}`} frac={c / 100} value={pct(c)} />)}
|
||||||
</div>
|
</div>
|
||||||
<div className="procs-tools">
|
<div className="box-foot muted">
|
||||||
<input placeholder="filter name, command, user, pid" value={q} onChange={(e) => setQ(e.target.value)} />
|
<span>load avg <b>{t.load[0].toFixed(2)}</b> {t.load[1].toFixed(2)} {t.load[2].toFixed(2)} <small>(1 · 5 · 15 min, {t.cores.length} cores)</small></span>
|
||||||
<button className={'chip' + (mine ? ' on' : '')} onClick={() => setMine(!mine)}>hide root</button>
|
<span>up <b>{fmtUptime(t.uptime)}</b></span>
|
||||||
<button className={'chip' + (paused ? ' on' : '')} onClick={() => setPaused(!paused)}>{paused ? 'paused' : 'live'}</button>
|
<span><b>{t.total}</b> tasks, <b>{t.running}</b> running</span>
|
||||||
<span className="muted">{rows.length}</span>
|
</div>
|
||||||
</div>
|
</Box>
|
||||||
|
<Box title="mem" right={`${fmtBytes(t.mem.total)} total`}>
|
||||||
|
<Row label="Used" frac={memFrac} value={`${fmtBytes(t.mem.used)} · ${pct(memFrac * 100)}`} />
|
||||||
|
<Row label="Available" frac={t.mem.total ? t.mem.avail / t.mem.total : 0} value={fmtBytes(t.mem.avail)} />
|
||||||
|
<Row label="Cached" frac={t.mem.total ? (t.mem.cached ?? 0) / t.mem.total : 0} value={fmtBytes(t.mem.cached ?? 0)} />
|
||||||
|
<Row label="Free" frac={t.mem.total ? (t.mem.free ?? 0) / t.mem.total : 0} value={fmtBytes(t.mem.free ?? 0)} />
|
||||||
|
{t.swap.total > 0 ? (
|
||||||
|
<><div className="box-sub">swap · {fmtBytes(t.swap.total)} total</div>
|
||||||
|
<Row label="Used" frac={swapFrac} value={`${fmtBytes(t.swap.used)} · ${pct(swapFrac * 100)}`} />
|
||||||
|
<Row label="Free" frac={t.swap.total ? t.swap.avail / t.swap.total : 0} value={fmtBytes(t.swap.avail)} /></>
|
||||||
|
) : <div className="box-sub muted">no swap</div>}
|
||||||
|
<div className="box-foot muted small">Used = total − available. Cached is file cache the kernel gives back under pressure, so "available" is the honest headroom.</div>
|
||||||
|
</Box>
|
||||||
|
<Box title="net" className="netbox">
|
||||||
|
{t.net.filter((n) => n.up).map((n) => (
|
||||||
|
<div className="netrow" key={n.iface}>
|
||||||
|
<span className="bl mono">{n.iface}</span>
|
||||||
|
<span className="bv"><span className="dn">▼ {fmtRate(n.rx_bps)}</span> <span className="upc">▲ {fmtRate(n.tx_bps)}</span></span>
|
||||||
|
<span className="muted small">{fmtBytes(n.rx_total)} in · {fmtBytes(n.tx_total)} out</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{t.net.filter((n) => n.up).length === 0 && <div className="muted">no interfaces up</div>}
|
||||||
|
</Box>
|
||||||
|
</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>
|
||||||
|
<label className="muted small">refresh
|
||||||
|
<select className="btn" value={interval} onChange={(e) => setIntervalMs(Number(e.target.value))}>
|
||||||
|
{INTERVALS.map((ms) => <option key={ms} value={ms}>{ms >= 1000 ? `${ms / 1000} s` : `${ms} ms`}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<span className="muted small">{rows.length} shown</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ptable">
|
<div className="ptable">
|
||||||
<div className="prow phead">
|
<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')}
|
{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>
|
||||||
{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}>
|
||||||
|
|||||||
+2
-1
@@ -41,7 +41,8 @@ export interface Disk { name: string; model?: string; size: number; read_bps: nu
|
|||||||
export interface FS { mount: string; type: string; size: number; used: number }
|
export interface FS { mount: string; type: string; size: number; used: number }
|
||||||
export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string }
|
export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string }
|
||||||
export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number }
|
export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number }
|
||||||
export interface ProcTable { t: number; cpu: number; cores: number[]; load: [number, number, number]; mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }; uptime: number; procs: Proc[]; total: number }
|
export interface MemInfo { total: number; used: number; avail: number; free?: number; cached?: number; buffers?: number }
|
||||||
|
export interface ProcTable { t: number; cpu: number; cores: number[]; cpu_hist: number[]; cpu_model?: string; cpu_mhz?: number; cpu_temp?: number; load: [number, number, number]; mem: MemInfo; swap: MemInfo; net: NetIf[]; uptime: number; procs: Proc[]; total: number; running: number }
|
||||||
export interface Sample {
|
export interface Sample {
|
||||||
t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number]
|
t: number; interval_ms: number; cpu: number; cores: number[]; load: [number, number, number]
|
||||||
mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }
|
mem: { total: number; used: number; avail: number }; swap: { total: number; used: number; avail: number }
|
||||||
|
|||||||
+31
-10
@@ -288,16 +288,34 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
|
|||||||
.files-tools .filter { width: 80px; }
|
.files-tools .filter { width: 80px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* processes tab */
|
/* 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); }
|
||||||
.procs-top { padding: 8px 10px; background: var(--bg2); border-bottom: 1px solid var(--border); display: flex; flex-direction: column; gap: 6px; }
|
.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; }
|
||||||
.cores { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 4px 10px; }
|
.procs-top .netbox { grid-column: 1 / -1; }
|
||||||
.pbar { display: flex; align-items: center; gap: 6px; font-size: 11px; } .pbar .meter { flex: 1; } .pbar .num { color: var(--muted); white-space: nowrap; min-width: 52px; }
|
.box { position: relative; border: 1px solid var(--border); border-radius: 8px; padding: 12px 12px 8px; min-width: 0; }
|
||||||
.memrow { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } .memrow .pbar { flex: 1; min-width: 180px; } .memrow .pbar .num { min-width: 0; }
|
.box-title { position: absolute; top: -8px; left: 10px; padding: 0 6px; background: var(--bg); color: var(--accent2); font-weight: 700; font-size: 12px; letter-spacing: .5px; }
|
||||||
.procs-tools { display: flex; gap: 6px; align-items: center; }
|
.box-right { position: absolute; top: -8px; right: 10px; padding: 0 6px; background: var(--bg); color: var(--muted); font-size: 11px; max-width: 70%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.procs-tools input { flex: 1; min-width: 0; padding: 6px 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 5px; color: var(--fg); font-size: 13px; }
|
.box-sub { margin: 8px 0 2px; color: var(--accent2); font-size: 11px; }
|
||||||
.ptable { flex: 1; overflow: auto; font-size: 13px; }
|
.box-foot { display: flex; gap: 14px; flex-wrap: wrap; margin-top: 8px; font-size: 11px; } .box-foot b { color: var(--fg); font-weight: 600; }
|
||||||
.prow { display: grid; grid-template-columns: 60px 80px minmax(120px, 1fr) 60px 76px 64px 64px; gap: 8px; align-items: center; padding: 4px 10px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
.small { font-size: 11px; }
|
||||||
|
.brow { display: grid; grid-template-columns: 64px 1fr auto; gap: 8px; align-items: center; padding: 2px 0; }
|
||||||
|
.bl { color: var(--muted); } .bv { font-variant-numeric: tabular-nums; white-space: nowrap; } .bv.big { font-size: 20px; font-weight: 700; color: var(--fg); }
|
||||||
|
.gbar { position: relative; height: 10px; border-radius: 3px; overflow: hidden; background: linear-gradient(90deg, #3fb950 0%, #9ccc3f 40%, #d29922 65%, #f0883e 82%, #ff7b72 100%); }
|
||||||
|
.gcover { position: absolute; top: 0; right: 0; bottom: 0; background: var(--bg3); }
|
||||||
|
.graph { display: flex; line-height: 1; font-size: 22px; letter-spacing: -1px; overflow: hidden; white-space: nowrap; }
|
||||||
|
.graph span { flex: 1 1 0; min-width: 0; text-align: center; } .graph .gpad { flex: 0 0 auto; color: var(--bg3); }
|
||||||
|
.graph .g1 { color: var(--ok); } .graph .g2 { color: var(--s2); } .graph .g3 { color: var(--bad); }
|
||||||
|
.cpu-total { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: end; margin-bottom: 8px; }
|
||||||
|
.cpu-total-v { display: flex; flex-direction: column; align-items: flex-end; }
|
||||||
|
.cores { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0 16px; }
|
||||||
|
.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 .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-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; }
|
||||||
|
.ptable { flex: 1; overflow: auto; font-size: 13px; border-top: 1px solid var(--border); }
|
||||||
|
.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); }
|
||||||
@@ -307,6 +325,9 @@ h2.section { margin: 0; font-size: 13px; font-weight: 600; color: var(--muted);
|
|||||||
.pdetail { border-top: 1px solid var(--border); background: var(--bg2); padding: 10px; display: flex; flex-direction: column; gap: 8px; }
|
.pdetail { border-top: 1px solid var(--border); background: var(--bg2); padding: 10px; display: flex; flex-direction: column; gap: 8px; }
|
||||||
.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; }
|
||||||
.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(90px, 1fr)); }
|
.cores { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
|
||||||
|
.graph { font-size: 16px; }
|
||||||
|
.netrow { grid-template-columns: 70px 1fr; } .netrow > :nth-child(3) { display: none; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user