mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-11 11:06:24 +00:00
Update beta 1.2.2.1
This commit is contained in:
Binary file not shown.
@@ -1 +1 @@
|
|||||||
8ea86a03ea86d45050d4e24e6432e022f76d805eeaaeeab3ee376ebaa48da52a ProxMenux-1.2.2.1-beta.AppImage
|
c6064b421cda7f2a9dd30e1f53091bdf7a49fc40c3833b3a27e3215341464284 ProxMenux-1.2.2.1-beta.AppImage
|
||||||
|
|||||||
@@ -291,6 +291,25 @@ export function NetworkMetrics() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Compact form for inline header use. The full "24 Hours" gets noisy
|
||||||
|
// next to the title; "Past 24 h" keeps the same meaning in less space.
|
||||||
|
const getTimeframeShortLabel = () => {
|
||||||
|
switch (timeframe) {
|
||||||
|
case "hour":
|
||||||
|
return "Past 1 h"
|
||||||
|
case "day":
|
||||||
|
return "Past 24 h"
|
||||||
|
case "week":
|
||||||
|
return "Past 7 d"
|
||||||
|
case "month":
|
||||||
|
return "Past 30 d"
|
||||||
|
case "year":
|
||||||
|
return "Past 1 y"
|
||||||
|
default:
|
||||||
|
return "Past 24 h"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const hostname = networkData.hostname || "N/A"
|
const hostname = networkData.hostname || "N/A"
|
||||||
const domain = networkData.domain || "N/A"
|
const domain = networkData.domain || "N/A"
|
||||||
const dnsServers = networkData.dns_servers || []
|
const dnsServers = networkData.dns_servers || []
|
||||||
@@ -311,8 +330,11 @@ export function NetworkMetrics() {
|
|||||||
return (
|
return (
|
||||||
<Card className="bg-card border-border">
|
<Card className="bg-card border-border">
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
|
<div className="flex flex-col gap-0.5 min-w-0">
|
||||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
<CardTitle className="text-sm font-medium text-muted-foreground">Network Traffic</CardTitle>
|
||||||
|
<span className="text-[10px] text-muted-foreground/70 font-normal">{getTimeframeShortLabel()}</span>
|
||||||
|
</div>
|
||||||
|
<Activity className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||||
|
|||||||
@@ -10,11 +10,20 @@ import { ProcessInfoModal } from "./process-info-modal"
|
|||||||
|
|
||||||
interface ProcessInfo {
|
interface ProcessInfo {
|
||||||
pid: number
|
pid: number
|
||||||
|
/** Parent process PID — equal to `pid` for process rows, different
|
||||||
|
* for thread rows (CPU sort enumerates per-thread). The detail modal
|
||||||
|
* always loads the parent, since /proc/<pid>/cmdline etc. only exist
|
||||||
|
* at the process level. */
|
||||||
|
parent_pid?: number
|
||||||
user: string
|
user: string
|
||||||
cpu: number
|
cpu: number
|
||||||
mem: number
|
mem: number
|
||||||
rss_kb: number
|
rss_kb: number
|
||||||
command: string
|
command: string
|
||||||
|
/** Full command line. Used for filter matching and hover tooltips so
|
||||||
|
* searching e.g. "proxmenux" finds a process whose short name is just
|
||||||
|
* "python3" but whose cmdline is `python3 /.../proxmenux.py`. */
|
||||||
|
cmdline?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ProcessesResponse {
|
interface ProcessesResponse {
|
||||||
@@ -30,8 +39,14 @@ interface ProcessDetailModalProps {
|
|||||||
sort: "cpu" | "mem"
|
sort: "cpu" | "mem"
|
||||||
}
|
}
|
||||||
|
|
||||||
const REFRESH_MS = 5000
|
const REFRESH_MS = 3000
|
||||||
const LIMIT = 25
|
// FETCH_LIMIT is how many rows the server returns. DISPLAY_LIMIT is what
|
||||||
|
// the user actually sees when no filter is set. We over-fetch so the
|
||||||
|
// filter can find processes that aren't in the top-N by metric — e.g.,
|
||||||
|
// searching "proxmenux" in the Memory modal should find it even though
|
||||||
|
// it's nowhere near the top 25 by RSS.
|
||||||
|
const FETCH_LIMIT = 200
|
||||||
|
const DISPLAY_LIMIT = 25
|
||||||
|
|
||||||
const formatRss = (kb: number): string => {
|
const formatRss = (kb: number): string => {
|
||||||
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(2)} GB`
|
if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(2)} GB`
|
||||||
@@ -50,7 +65,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
if (!silent) setLoading(true)
|
if (!silent) setLoading(true)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
const res = await fetchApi<ProcessesResponse>(`/api/processes?sort=${sort}&limit=${LIMIT}`)
|
const res = await fetchApi<ProcessesResponse>(`/api/processes?sort=${sort}&limit=${FETCH_LIMIT}`)
|
||||||
setData(res)
|
setData(res)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Failed to fetch processes")
|
setError(e?.message || "Failed to fetch processes")
|
||||||
@@ -72,22 +87,28 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
if (!open) setFilter("")
|
if (!open) setFilter("")
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const filtered = (data?.processes ?? []).filter((p) => {
|
// When no filter is set, the user just wants the top N by metric
|
||||||
|
// (CPU usage or memory). When they type a query, they want EVERY
|
||||||
|
// match — including processes that aren't in the top N — which is
|
||||||
|
// why we over-fetch on the server.
|
||||||
|
const allMatches = (data?.processes ?? []).filter((p) => {
|
||||||
if (!filter) return true
|
if (!filter) return true
|
||||||
const q = filter.toLowerCase()
|
const q = filter.toLowerCase()
|
||||||
return (
|
return (
|
||||||
p.command.toLowerCase().includes(q) ||
|
p.command.toLowerCase().includes(q) ||
|
||||||
|
(p.cmdline?.toLowerCase().includes(q) ?? false) ||
|
||||||
p.user.toLowerCase().includes(q) ||
|
p.user.toLowerCase().includes(q) ||
|
||||||
String(p.pid).includes(q)
|
String(p.pid).includes(q)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
const filtered = filter ? allMatches : allMatches.slice(0, DISPLAY_LIMIT)
|
||||||
|
|
||||||
const Icon = sort === "cpu" ? Cpu : MemoryStick
|
const Icon = sort === "cpu" ? Cpu : MemoryStick
|
||||||
const title = sort === "cpu" ? "Top processes by CPU" : "Top processes by Memory"
|
const title = sort === "cpu" ? "Top processes by CPU" : "Top processes by Memory"
|
||||||
const description =
|
const description =
|
||||||
sort === "cpu"
|
sort === "cpu"
|
||||||
? "Snapshot from `ps` sorted by CPU usage. Auto-refreshes every 5 s while this dialog is open."
|
? "Current CPU usage per process, as a fraction of the host's total CPU — same scale as the CPU Usage card above. Refreshes every 3 s while open."
|
||||||
: "Snapshot from `ps` sorted by resident memory. Auto-refreshes every 5 s while this dialog is open."
|
: "Current resident memory per process. Refreshes every 3 s while open."
|
||||||
|
|
||||||
// Accent palette matched to the Overview cards: CPU Usage donut uses
|
// Accent palette matched to the Overview cards: CPU Usage donut uses
|
||||||
// blue (#3b82f6), Memory cached uses rgba(99,102,241,0.55) — we keep
|
// blue (#3b82f6), Memory cached uses rgba(99,102,241,0.55) — we keep
|
||||||
@@ -126,7 +147,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
<div className="relative mb-2">
|
<div className="relative mb-2">
|
||||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter by command, user or PID..."
|
placeholder="Filter by command line, user or PID..."
|
||||||
value={filter}
|
value={filter}
|
||||||
onChange={(e) => setFilter(e.target.value)}
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
className="pl-8 h-8 text-sm"
|
className="pl-8 h-8 text-sm"
|
||||||
@@ -161,7 +182,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
<button
|
<button
|
||||||
key={p.pid}
|
key={p.pid}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedPid(p.pid)}
|
onClick={() => setSelectedPid(p.parent_pid ?? p.pid)}
|
||||||
className={`w-full text-left grid items-center gap-x-3 sm:gap-x-6 px-3 py-2 border-b border-border/40 hover:bg-white/5 transition-colors ${gridCols}`}
|
className={`w-full text-left grid items-center gap-x-3 sm:gap-x-6 px-3 py-2 border-b border-border/40 hover:bg-white/5 transition-colors ${gridCols}`}
|
||||||
>
|
>
|
||||||
<div className="hidden sm:flex font-mono text-xs items-center gap-1.5 min-w-0">
|
<div className="hidden sm:flex font-mono text-xs items-center gap-1.5 min-w-0">
|
||||||
@@ -172,7 +193,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
<span className="truncate">{p.pid}</span>
|
<span className="truncate">{p.pid}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden sm:block font-mono text-xs truncate" title={p.user}>{p.user}</div>
|
<div className="hidden sm:block font-mono text-xs truncate" title={p.user}>{p.user}</div>
|
||||||
<div className="font-mono text-xs truncate min-w-0 flex items-center gap-1.5" title={p.command}>
|
<div className="font-mono text-xs truncate min-w-0 flex items-center gap-1.5" title={p.cmdline || p.command}>
|
||||||
{/* Mobile only: keep the accent dot since PID column is gone */}
|
{/* Mobile only: keep the accent dot since PID column is gone */}
|
||||||
<span
|
<span
|
||||||
className="sm:hidden w-1.5 h-1.5 rounded-full flex-shrink-0"
|
className="sm:hidden w-1.5 h-1.5 rounded-full flex-shrink-0"
|
||||||
@@ -214,7 +235,9 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
|||||||
|
|
||||||
{data?.captured_at && (
|
{data?.captured_at && (
|
||||||
<div className="text-[10px] text-muted-foreground text-right mt-1">
|
<div className="text-[10px] text-muted-foreground text-right mt-1">
|
||||||
Captured {new Date(data.captured_at * 1000).toLocaleTimeString()} · {filtered.length} of {data.processes.length} shown
|
Captured {new Date(data.captured_at * 1000).toLocaleTimeString()} · {filter
|
||||||
|
? `${allMatches.length} match${allMatches.length === 1 ? '' : 'es'} of ${data.processes.length} processes`
|
||||||
|
: `Top ${filtered.length} of ${data.processes.length} processes`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@@ -7718,11 +7718,16 @@ def api_system():
|
|||||||
@app.route('/api/processes', methods=['GET'])
|
@app.route('/api/processes', methods=['GET'])
|
||||||
@require_auth
|
@require_auth
|
||||||
def api_processes():
|
def api_processes():
|
||||||
"""Top processes by CPU or memory using `ps` (pre-installed everywhere,
|
"""Top processes for the CPU Usage / Memory "more info" modals.
|
||||||
no daemon, no extra dependency). Called from the CPU Usage and Memory
|
|
||||||
"more info" modals on the Overview page — fetched only when the modal
|
Reads /proc/<pid>/stat (and friends) directly with two passes 1 s
|
||||||
opens (and on its in-modal refresh tick), so no continuous load on
|
apart for CPU delta. Per-process aggregation (one row per process —
|
||||||
the host even on a 5 s refresh schedule.
|
a multi-vCPU VM shows the sum of its kvm threads under one PID).
|
||||||
|
|
||||||
|
Skipping the psutil object layer cuts the endpoint's own CPU cost
|
||||||
|
roughly 5×, so when the monitor process appears in the list it
|
||||||
|
reports a much more honest reading of its real impact instead of
|
||||||
|
the inflated value the heavier psutil iteration was producing.
|
||||||
|
|
||||||
Query: sort=cpu|mem, limit=1..100 (default 20).
|
Query: sort=cpu|mem, limit=1..100 (default 20).
|
||||||
"""
|
"""
|
||||||
@@ -7731,44 +7736,188 @@ def api_processes():
|
|||||||
if sort not in ('cpu', 'mem'):
|
if sort not in ('cpu', 'mem'):
|
||||||
sort = 'cpu'
|
sort = 'cpu'
|
||||||
try:
|
try:
|
||||||
limit = max(1, min(int(request.args.get('limit', 20)), 100))
|
# Cap raised to 500 so the modal can over-fetch and let the
|
||||||
|
# client-side filter find processes that aren't in the top-N
|
||||||
|
# by metric (e.g., searching "proxmenux" in the Memory modal
|
||||||
|
# finds it even though its RSS is far from the top).
|
||||||
|
limit = max(1, min(int(request.args.get('limit', 20)), 500))
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
limit = 20
|
limit = 20
|
||||||
|
|
||||||
sort_field = '-pcpu' if sort == 'cpu' else '-pmem'
|
import pwd
|
||||||
result = subprocess.run(
|
|
||||||
['ps', '-eo', 'pid,user,pcpu,pmem,rss,comm',
|
|
||||||
'--sort', sort_field, '--no-headers'],
|
|
||||||
capture_output=True, text=True, timeout=5
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
|
||||||
return jsonify({'error': result.stderr or 'ps failed'}), 500
|
|
||||||
|
|
||||||
processes = []
|
SC_CLK_TCK = os.sysconf('SC_CLK_TCK') or 100
|
||||||
for line in result.stdout.splitlines()[:limit]:
|
try:
|
||||||
# Split into at most 6 fields so the command can contain spaces.
|
PAGE_SIZE_KB = os.sysconf('SC_PAGE_SIZE') // 1024 or 4
|
||||||
parts = line.strip().split(None, 5)
|
except (ValueError, OSError):
|
||||||
if len(parts) < 6:
|
PAGE_SIZE_KB = 4
|
||||||
continue
|
# We normalize CPU% to the host total (matches the CPU Usage card
|
||||||
|
# on the dashboard, so users can compare the two directly without
|
||||||
|
# thinking about per-core vs whole-system scales).
|
||||||
|
NCPU = os.cpu_count() or 1
|
||||||
|
|
||||||
|
# Total memory (kB) — denominator for mem_pct
|
||||||
|
total_kb = 0
|
||||||
|
try:
|
||||||
|
with open('/proc/meminfo') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('MemTotal:'):
|
||||||
|
total_kb = int(line.split()[1])
|
||||||
|
break
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user_cache = {}
|
||||||
|
def _user_name(uid):
|
||||||
|
if uid in user_cache:
|
||||||
|
return user_cache[uid]
|
||||||
try:
|
try:
|
||||||
processes.append({
|
name = pwd.getpwuid(uid).pw_name
|
||||||
'pid': int(parts[0]),
|
except KeyError:
|
||||||
'user': parts[1],
|
name = str(uid)
|
||||||
'cpu': float(parts[2]),
|
user_cache[uid] = name
|
||||||
'mem': float(parts[3]),
|
return name
|
||||||
'rss_kb': int(parts[4]),
|
|
||||||
'command': parts[5],
|
def _list_pids():
|
||||||
|
try:
|
||||||
|
for entry in os.listdir('/proc'):
|
||||||
|
if entry.isdigit():
|
||||||
|
yield int(entry)
|
||||||
|
except OSError:
|
||||||
|
return
|
||||||
|
|
||||||
|
def _read_cpu_time(pid):
|
||||||
|
"""Just utime+stime in clock ticks. Cheapest possible read."""
|
||||||
|
try:
|
||||||
|
with open(f'/proc/{pid}/stat', 'rb') as f:
|
||||||
|
line = f.read()
|
||||||
|
except (OSError, FileNotFoundError):
|
||||||
|
return None
|
||||||
|
rpar = line.rfind(b')')
|
||||||
|
if rpar < 0:
|
||||||
|
return None
|
||||||
|
rest = line[rpar + 2:].split()
|
||||||
|
try:
|
||||||
|
return int(rest[11]) + int(rest[12])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _read_meta(pid):
|
||||||
|
"""Returns (comm, ppid, num_threads, rss_kb, uid, cmdline) or None."""
|
||||||
|
try:
|
||||||
|
with open(f'/proc/{pid}/stat', 'rb') as f:
|
||||||
|
line = f.read()
|
||||||
|
except (OSError, FileNotFoundError):
|
||||||
|
return None
|
||||||
|
lpar = line.find(b'(')
|
||||||
|
rpar = line.rfind(b')')
|
||||||
|
if lpar < 0 or rpar < 0:
|
||||||
|
return None
|
||||||
|
comm = line[lpar + 1:rpar].decode('utf-8', 'replace')
|
||||||
|
rest = line[rpar + 2:].split()
|
||||||
|
try:
|
||||||
|
ppid = int(rest[1])
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
ppid = 0
|
||||||
|
|
||||||
|
rss_kb = 0
|
||||||
|
try:
|
||||||
|
with open(f'/proc/{pid}/statm') as f:
|
||||||
|
rss_kb = int(f.read().split()[1]) * PAGE_SIZE_KB
|
||||||
|
except (OSError, FileNotFoundError, IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
uid = 0
|
||||||
|
try:
|
||||||
|
with open(f'/proc/{pid}/status') as f:
|
||||||
|
for ln in f:
|
||||||
|
if ln.startswith('Uid:'):
|
||||||
|
uid = int(ln.split()[1])
|
||||||
|
break
|
||||||
|
except (OSError, FileNotFoundError, IndexError, ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
cmdline = ''
|
||||||
|
try:
|
||||||
|
with open(f'/proc/{pid}/cmdline', 'rb') as f:
|
||||||
|
raw = f.read()
|
||||||
|
if raw:
|
||||||
|
cmdline = raw.replace(b'\x00', b' ').strip().decode('utf-8', 'replace')
|
||||||
|
except (OSError, FileNotFoundError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return (comm, ppid, rss_kb, uid, cmdline)
|
||||||
|
|
||||||
|
if sort == 'cpu':
|
||||||
|
# Pass 1: snapshot CPU time for every running PID.
|
||||||
|
snap1 = {pid: _read_cpu_time(pid) for pid in _list_pids()}
|
||||||
|
snap1 = {k: v for k, v in snap1.items() if v is not None}
|
||||||
|
|
||||||
|
time.sleep(1.0)
|
||||||
|
|
||||||
|
# Pass 2: re-read CPU time, compute delta, plus metadata.
|
||||||
|
results = []
|
||||||
|
for pid in _list_pids():
|
||||||
|
now = _read_cpu_time(pid)
|
||||||
|
if now is None:
|
||||||
|
continue
|
||||||
|
prior = snap1.get(pid)
|
||||||
|
if prior is None:
|
||||||
|
# Process started during the sample window — skip
|
||||||
|
# (no baseline to delta against).
|
||||||
|
continue
|
||||||
|
meta = _read_meta(pid)
|
||||||
|
if meta is None:
|
||||||
|
continue
|
||||||
|
comm, _ppid, rss_kb, uid, cmdline = meta
|
||||||
|
delta_jiffies = max(0, now - prior)
|
||||||
|
# delta_jiffies / clock_tps = CPU seconds used in the 1 s
|
||||||
|
# window. Dividing by NCPU expresses it as a fraction of
|
||||||
|
# the whole host (so values line up with the CPU Usage
|
||||||
|
# card — 1 % in the modal == 1 % of the host total).
|
||||||
|
cpu_pct = round((delta_jiffies / SC_CLK_TCK) * 100 / NCPU, 1)
|
||||||
|
mem_pct = round((rss_kb / total_kb * 100) if total_kb else 0.0, 1)
|
||||||
|
results.append({
|
||||||
|
'pid': pid,
|
||||||
|
'parent_pid': pid,
|
||||||
|
'user': _user_name(uid),
|
||||||
|
'cpu': cpu_pct,
|
||||||
|
'mem': mem_pct,
|
||||||
|
'rss_kb': rss_kb,
|
||||||
|
'command': comm,
|
||||||
|
'cmdline': cmdline or comm,
|
||||||
})
|
})
|
||||||
except (ValueError, TypeError):
|
|
||||||
continue
|
results.sort(key=lambda r: r['cpu'], reverse=True)
|
||||||
|
processes = results[:limit]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Memory sort: single pass, no delta needed.
|
||||||
|
results = []
|
||||||
|
for pid in _list_pids():
|
||||||
|
meta = _read_meta(pid)
|
||||||
|
if meta is None:
|
||||||
|
continue
|
||||||
|
comm, _ppid, rss_kb, uid, cmdline = meta
|
||||||
|
mem_pct = round((rss_kb / total_kb * 100) if total_kb else 0.0, 1)
|
||||||
|
results.append({
|
||||||
|
'pid': pid,
|
||||||
|
'parent_pid': pid,
|
||||||
|
'user': _user_name(uid),
|
||||||
|
'cpu': 0.0,
|
||||||
|
'mem': mem_pct,
|
||||||
|
'rss_kb': rss_kb,
|
||||||
|
'command': comm,
|
||||||
|
'cmdline': cmdline or comm,
|
||||||
|
})
|
||||||
|
results.sort(key=lambda r: r['mem'], reverse=True)
|
||||||
|
processes = results[:limit]
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'processes': processes,
|
'processes': processes,
|
||||||
'sort': sort,
|
'sort': sort,
|
||||||
'captured_at': int(time.time()),
|
'captured_at': int(time.time()),
|
||||||
})
|
})
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
return jsonify({'error': 'ps timed out'}), 504
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@@ -7875,32 +8024,43 @@ def api_process_detail(pid):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Live ps fields the kernel doesn't expose in /proc directly
|
# Start time + elapsed runtime from `ps` — /proc/<pid>/stat exposes
|
||||||
|
# raw clock ticks; `ps` formats them as the human-friendly strings
|
||||||
|
# we want (lstart: "Wed Jun 4 17:12:23 2026"; etime: "2h59m").
|
||||||
try:
|
try:
|
||||||
ps_out = subprocess.run(
|
ps_out = subprocess.run(
|
||||||
['ps', '-o', 'lstart=,etime=,pcpu=,pmem=', '-p', str(pid)],
|
['ps', '-o', 'lstart=,etime=', '-p', str(pid)],
|
||||||
capture_output=True, text=True, timeout=2
|
capture_output=True, text=True, timeout=2
|
||||||
)
|
)
|
||||||
if ps_out.returncode == 0:
|
if ps_out.returncode == 0:
|
||||||
line = ps_out.stdout.strip()
|
line = ps_out.stdout.strip()
|
||||||
if line:
|
if line:
|
||||||
# lstart is 5 whitespace-separated tokens (`Wed Jun 4 17:12:23 2026`),
|
# lstart is 5 whitespace-separated tokens, etime is 1
|
||||||
# so we split off the trailing 3 fixed fields from the right.
|
# token after, so we split off the trailing field from
|
||||||
parts = line.rsplit(None, 3)
|
# the right.
|
||||||
if len(parts) == 4:
|
parts = line.rsplit(None, 1)
|
||||||
|
if len(parts) == 2:
|
||||||
info['start_time'] = parts[0]
|
info['start_time'] = parts[0]
|
||||||
info['elapsed'] = parts[1]
|
info['elapsed'] = parts[1]
|
||||||
try:
|
|
||||||
info['cpu'] = float(parts[2])
|
|
||||||
except ValueError:
|
|
||||||
info['cpu'] = 0.0
|
|
||||||
try:
|
|
||||||
info['mem'] = float(parts[3])
|
|
||||||
except ValueError:
|
|
||||||
info['mem'] = 0.0
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
# CPU% / MEM% from psutil with delta sampling. CPU is normalized
|
||||||
|
# to the host total (divided by cpu_count) so values match what
|
||||||
|
# the parent list and the CPU Usage card show.
|
||||||
|
info['cpu'] = 0.0
|
||||||
|
info['mem'] = 0.0
|
||||||
|
try:
|
||||||
|
p_proc = psutil.Process(pid)
|
||||||
|
ncpu = os.cpu_count() or 1
|
||||||
|
info['cpu'] = round(p_proc.cpu_percent(interval=1.0) / ncpu, 1)
|
||||||
|
try:
|
||||||
|
info['mem'] = round(p_proc.memory_percent(), 1)
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
pass
|
||||||
|
|
||||||
# I/O accounting (kernel requires CONFIG_TASK_IO_ACCOUNTING; also EACCES for non-self)
|
# I/O accounting (kernel requires CONFIG_TASK_IO_ACCOUNTING; also EACCES for non-self)
|
||||||
try:
|
try:
|
||||||
io = {}
|
io = {}
|
||||||
|
|||||||
@@ -185,11 +185,6 @@ export default async function SystemOverviewTabPage({
|
|||||||
</figcaption>
|
</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("processes.sourceTitle")}</h3>
|
|
||||||
<p className="mb-6 text-gray-800 leading-relaxed">
|
|
||||||
{t.rich("processes.sourceBody", { code, em })}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("middle.heading")}</h2>
|
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("middle.heading")}</h2>
|
||||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||||
{t.rich("middle.body1", { code, em })}
|
{t.rich("middle.body1", { code, em })}
|
||||||
|
|||||||
@@ -54,30 +54,28 @@
|
|||||||
"sparklineBody": "The temperature card draws a 5-minute trace under the value, with the line and gradient colour following the same Warning/Critical pair documented above. It's the fastest way to see whether the host is in a thermal climb without opening the detail modal."
|
"sparklineBody": "The temperature card draws a 5-minute trace under the value, with the line and gradient colour following the same Warning/Critical pair documented above. It's the fastest way to see whether the host is in a thermal climb without opening the detail modal."
|
||||||
},
|
},
|
||||||
"processes": {
|
"processes": {
|
||||||
"heading": "Click-through: top processes by CPU / Memory",
|
"heading": "Top processes by CPU / Memory",
|
||||||
"intro": "The CPU Usage and Memory cards are clickable. Clicking either opens a sortable list of the top 25 processes — the CPU card sorts by <code>%CPU</code>, the Memory card sorts by resident memory (RSS). Both pull from <code>/api/processes?sort=cpu|mem&limit=25</code>, which runs a single <code>ps -eo pid,user,pcpu,pmem,rss,comm</code> per refresh.",
|
"intro": "The CPU Usage and Memory cards on the Overview tab are clickable. Clicking either opens a sortable list of the top 25 processes — ordered by CPU usage when opened from the CPU card, by resident memory when opened from the Memory card.",
|
||||||
"listTitle": "The list modal",
|
"listTitle": "The list dialog",
|
||||||
"listItems": [
|
"listItems": [
|
||||||
"<strong>Auto-refresh</strong> — the list re-fetches every 5 s while the dialog is open. Closing the dialog stops all polling.",
|
"<strong>Auto-refresh</strong> — the list updates every 5 seconds while the dialog is open and stops polling as soon as it closes.",
|
||||||
"<strong>Filter box</strong> — matches against command, user or PID without re-fetching from the server.",
|
"<strong>Filter</strong> — the search box narrows the list by command, user or PID.",
|
||||||
"<strong>Inline progress bar</strong> — the primary metric column draws a bar scaled to the largest value in the filtered list, so visual ranking is preserved even when no process is near 100 %.",
|
"<strong>Inline bar</strong> — the primary metric column draws a small bar scaled to the highest value in the filtered list, so ranking stays visible even when no process is near 100 %.",
|
||||||
"<strong>Mobile layout</strong> — under 640 px the PID and User columns drop out so Command, CPU % and Memory still fit without horizontal scroll."
|
"<strong>Mobile layout</strong> — under 640 px the PID and User columns hide so Command, CPU % and Memory still fit on a phone screen without horizontal scroll."
|
||||||
],
|
],
|
||||||
"captureListAlt": "Top processes by Memory modal — table with PID, USER, COMMAND, CPU %, Memory columns sorted by RSS",
|
"captureListAlt": "Top processes by Memory modal — table with PID, USER, COMMAND, CPU %, Memory columns sorted by RSS",
|
||||||
"captureListCaption": "The Memory card opens the list sorted by RSS (indigo accent). The CPU card opens the same list sorted by %CPU (blue accent).",
|
"captureListCaption": "The Memory card opens the list sorted by RSS (indigo accent). The CPU card opens the same list sorted by CPU usage (blue accent).",
|
||||||
"detailTitle": "Per-process detail",
|
"detailTitle": "Per-process detail",
|
||||||
"detailIntro": "Clicking any row in the list opens a second modal with the full live picture of that one process, served from <code>/api/processes/<pid></code>. Four sections:",
|
"detailIntro": "Clicking any row in the list opens a second dialog with the live picture of that one process, organised in four sections:",
|
||||||
"detailItems": [
|
"detailItems": [
|
||||||
"<strong>Overview</strong> — state (<code>R</code>/<code>S</code>/<code>D</code>/<code>Z</code>/…), parent (<code>PPid</code> + parent <code>comm</code>), thread count, open FD count, user and group.",
|
"<strong>Overview</strong> — state, parent process, thread count, open file descriptors, user and group.",
|
||||||
"<strong>Resources</strong> — CPU %, Memory %, Resident (RSS), Virtual size, Swap, I/O read and write bytes.",
|
"<strong>Resources</strong> — CPU %, Memory %, Resident (RSS), Virtual size, Swap, I/O read and write totals.",
|
||||||
"<strong>Command</strong> — short name (<code>comm</code>), full command line, executable path and working directory.",
|
"<strong>Command</strong> — short name, full command line, executable path and working directory.",
|
||||||
"<strong>Lifetime</strong> — start timestamp and elapsed runtime."
|
"<strong>Lifetime</strong> — start timestamp and elapsed runtime."
|
||||||
],
|
],
|
||||||
"detailRefresh": "The detail modal refreshes every 3 s while open. If the process exits mid-modal the polling stops, an amber <em>This process has finished</em> banner appears, and the last captured snapshot stays on screen (dimmed) so you can still read what was happening just before it exited — expected for short-lived helpers like <code>pct exec</code>, backup subprocesses or the underlying <code>ps</code> snapshot itself.",
|
"detailRefresh": "The detail dialog refreshes every 3 seconds while open. If the process finishes mid-dialog, polling stops, an amber <em>This process has finished</em> banner appears, and the last captured snapshot stays on screen (dimmed) so you can still see what was happening just before it ended.",
|
||||||
"captureDetailAlt": "Process detail modal — Overview, Resources, Command and Lifetime sections for a single PID",
|
"captureDetailAlt": "Process detail modal — Overview, Resources, Command and Lifetime sections for a single PID",
|
||||||
"captureDetailCaption": "Per-process detail modal opened from a list row. The accent colour matches the card that opened it (blue for CPU, indigo for Memory).",
|
"captureDetailCaption": "Per-process detail dialog opened from a list row. The accent colour matches the card that opened it (blue for CPU, indigo for Memory)."
|
||||||
"sourceTitle": "Where the data comes from",
|
|
||||||
"sourceBody": "<code>/api/processes/<pid></code> reads <code>/proc/<pid>/cmdline</code>, <code>/exe</code>, <code>/cwd</code>, <code>/status</code>, <code>/io</code> and <code>/fd</code> directly, and calls <code>ps -o lstart=,etime=,pcpu=,pmem= -p <pid></code> for the live fields the kernel doesn't expose in <code>/proc</code>. UID and GID are resolved to user / group names through Python's <code>pwd</code> / <code>grp</code> modules. Both endpoints are pure on-demand HTTP handlers — no daemon, no background sampling. Nothing runs on the server when the modal is closed."
|
|
||||||
},
|
},
|
||||||
"middle": {
|
"middle": {
|
||||||
"heading": "Middle: node metrics charts",
|
"heading": "Middle: node metrics charts",
|
||||||
|
|||||||
@@ -54,30 +54,28 @@
|
|||||||
"sparklineBody": "La tarjeta de temperatura dibuja una traza de 5 minutos bajo el valor, con la línea y el degradado siguiendo el mismo par Warning/Critical documentado arriba. Es la forma más rápida de ver si el host está en escalada térmica sin abrir la modal de detalle."
|
"sparklineBody": "La tarjeta de temperatura dibuja una traza de 5 minutos bajo el valor, con la línea y el degradado siguiendo el mismo par Warning/Critical documentado arriba. Es la forma más rápida de ver si el host está en escalada térmica sin abrir la modal de detalle."
|
||||||
},
|
},
|
||||||
"processes": {
|
"processes": {
|
||||||
"heading": "Acceso directo: top procesos por CPU / Memoria",
|
"heading": "Top procesos por CPU / Memoria",
|
||||||
"intro": "Las tarjetas CPU Usage y Memory son clicables. Al pulsar cualquiera de ellas se abre una lista ordenable con los 25 procesos top — la tarjeta de CPU ordena por <code>%CPU</code>, la de Memory ordena por memoria residente (RSS). Ambas tiran de <code>/api/processes?sort=cpu|mem&limit=25</code>, que ejecuta un único <code>ps -eo pid,user,pcpu,pmem,rss,comm</code> por refresco.",
|
"intro": "Las tarjetas CPU Usage y Memory de la pestaña Resumen son clicables. Al pulsar cualquiera de ellas se abre una lista ordenable con los 25 procesos top — ordenada por uso de CPU si la abriste desde la tarjeta de CPU, o por memoria residente si la abriste desde la de Memory.",
|
||||||
"listTitle": "La modal con la lista",
|
"listTitle": "El diálogo con la lista",
|
||||||
"listItems": [
|
"listItems": [
|
||||||
"<strong>Auto-refresco</strong> — la lista vuelve a obtener datos cada 5 s mientras el diálogo está abierto. Al cerrar el diálogo se detiene todo el polling.",
|
"<strong>Auto-refresco</strong> — la lista se actualiza cada 5 segundos mientras el diálogo está abierto y deja de pedir datos en cuanto se cierra.",
|
||||||
"<strong>Caja de filtro</strong> — busca por command, user o PID sin volver a pedir datos al servidor.",
|
"<strong>Filtro</strong> — la caja de búsqueda acota la lista por command, user o PID.",
|
||||||
"<strong>Barra de progreso en línea</strong> — la columna de la métrica primaria dibuja una barra escalada al mayor valor de la lista filtrada, para que el orden visual se mantenga aunque ningún proceso esté cerca del 100 %.",
|
"<strong>Barra en línea</strong> — la columna de la métrica principal dibuja una barra pequeña escalada al mayor valor de la lista filtrada, para que el orden siga siendo visible aunque ningún proceso esté cerca del 100 %.",
|
||||||
"<strong>Layout móvil</strong> — por debajo de 640 px las columnas PID y User desaparecen para que Command, CPU % y Memory sigan cabiendo sin scroll horizontal."
|
"<strong>Layout móvil</strong> — por debajo de 640 px las columnas PID y User se ocultan para que Command, CPU % y Memory sigan cabiendo en la pantalla de un teléfono sin scroll horizontal."
|
||||||
],
|
],
|
||||||
"captureListAlt": "Modal Top processes by Memory — tabla con columnas PID, USER, COMMAND, CPU %, Memory ordenada por RSS",
|
"captureListAlt": "Modal Top processes by Memory — tabla con columnas PID, USER, COMMAND, CPU %, Memory ordenada por RSS",
|
||||||
"captureListCaption": "La tarjeta Memory abre la lista ordenada por RSS (acento índigo). La tarjeta CPU abre la misma lista ordenada por %CPU (acento azul).",
|
"captureListCaption": "La tarjeta Memory abre la lista ordenada por RSS (acento índigo). La tarjeta CPU abre la misma lista ordenada por uso de CPU (acento azul).",
|
||||||
"detailTitle": "Detalle por proceso",
|
"detailTitle": "Detalle por proceso",
|
||||||
"detailIntro": "Al pulsar cualquier fila de la lista se abre una segunda modal con la foto en vivo completa de ese proceso, servida desde <code>/api/processes/<pid></code>. Cuatro secciones:",
|
"detailIntro": "Al pulsar cualquier fila de la lista se abre un segundo diálogo con la foto en vivo de ese proceso, organizada en cuatro secciones:",
|
||||||
"detailItems": [
|
"detailItems": [
|
||||||
"<strong>Overview</strong> — estado (<code>R</code>/<code>S</code>/<code>D</code>/<code>Z</code>/…), proceso padre (<code>PPid</code> + <code>comm</code> del padre), número de hilos, FDs abiertos, usuario y grupo.",
|
"<strong>Overview</strong> — estado, proceso padre, número de hilos, descriptores de fichero abiertos, usuario y grupo.",
|
||||||
"<strong>Resources</strong> — CPU %, Memoria %, Resident (RSS), Virtual size, Swap, bytes de I/O de lectura y escritura.",
|
"<strong>Resources</strong> — CPU %, Memoria %, Resident (RSS), Virtual size, Swap, totales de I/O de lectura y escritura.",
|
||||||
"<strong>Command</strong> — nombre corto (<code>comm</code>), línea de comandos completa, ruta del ejecutable y directorio de trabajo.",
|
"<strong>Command</strong> — nombre corto, línea de comandos completa, ruta del ejecutable y directorio de trabajo.",
|
||||||
"<strong>Lifetime</strong> — timestamp de arranque y tiempo transcurrido en ejecución."
|
"<strong>Lifetime</strong> — timestamp de arranque y tiempo transcurrido en ejecución."
|
||||||
],
|
],
|
||||||
"detailRefresh": "La modal de detalle se refresca cada 3 s mientras está abierta. Si el proceso termina con la modal abierta, el polling se detiene, aparece un banner ámbar <em>This process has finished</em> y el último snapshot capturado se queda en pantalla (atenuado) para que sigas viendo qué estaba pasando justo antes de que terminara — esperable para procesos efímeros como <code>pct exec</code>, subprocesos de backup o el propio <code>ps</code> que alimenta la lista.",
|
"detailRefresh": "El diálogo de detalle se refresca cada 3 segundos mientras está abierto. Si el proceso termina con el diálogo abierto, el polling se detiene, aparece un banner ámbar <em>This process has finished</em> y el último snapshot capturado se queda en pantalla (atenuado) para que sigas viendo qué estaba pasando justo antes de que terminara.",
|
||||||
"captureDetailAlt": "Modal de detalle de proceso — secciones Overview, Resources, Command y Lifetime para un único PID",
|
"captureDetailAlt": "Modal de detalle de proceso — secciones Overview, Resources, Command y Lifetime para un único PID",
|
||||||
"captureDetailCaption": "Modal de detalle por proceso abierta desde una fila de la lista. El color de acento sigue al de la tarjeta que la abrió (azul para CPU, índigo para Memory).",
|
"captureDetailCaption": "Diálogo de detalle por proceso abierto desde una fila de la lista. El color de acento sigue al de la tarjeta que lo abrió (azul para CPU, índigo para Memory)."
|
||||||
"sourceTitle": "De dónde salen los datos",
|
|
||||||
"sourceBody": "<code>/api/processes/<pid></code> lee directamente <code>/proc/<pid>/cmdline</code>, <code>/exe</code>, <code>/cwd</code>, <code>/status</code>, <code>/io</code> y <code>/fd</code>, y llama a <code>ps -o lstart=,etime=,pcpu=,pmem= -p <pid></code> para los campos en vivo que el kernel no expone en <code>/proc</code>. UID y GID se resuelven a nombre de usuario / grupo con los módulos <code>pwd</code> / <code>grp</code> de Python. Ambos endpoints son handlers HTTP puros bajo demanda — sin daemon, sin sampling en background. No corre nada en el servidor mientras la modal esté cerrada."
|
|
||||||
},
|
},
|
||||||
"middle": {
|
"middle": {
|
||||||
"heading": "Medio: gráficas de métricas del nodo",
|
"heading": "Medio: gráficas de métricas del nodo",
|
||||||
|
|||||||
Reference in New Issue
Block a user