diff --git a/AppImage/ProxMenux-1.2.2.1-beta.AppImage b/AppImage/ProxMenux-1.2.2.1-beta.AppImage index a05d2628..62e5281d 100755 Binary files a/AppImage/ProxMenux-1.2.2.1-beta.AppImage and b/AppImage/ProxMenux-1.2.2.1-beta.AppImage differ diff --git a/AppImage/ProxMenux-Monitor.AppImage.sha256 b/AppImage/ProxMenux-Monitor.AppImage.sha256 index 52b3ed1f..c0915db9 100644 --- a/AppImage/ProxMenux-Monitor.AppImage.sha256 +++ b/AppImage/ProxMenux-Monitor.AppImage.sha256 @@ -1 +1 @@ -8ea86a03ea86d45050d4e24e6432e022f76d805eeaaeeab3ee376ebaa48da52a ProxMenux-1.2.2.1-beta.AppImage +c6064b421cda7f2a9dd30e1f53091bdf7a49fc40c3833b3a27e3215341464284 ProxMenux-1.2.2.1-beta.AppImage diff --git a/AppImage/components/network-metrics.tsx b/AppImage/components/network-metrics.tsx index fe70e1d9..a863118e 100644 --- a/AppImage/components/network-metrics.tsx +++ b/AppImage/components/network-metrics.tsx @@ -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 domain = networkData.domain || "N/A" const dnsServers = networkData.dns_servers || [] @@ -311,8 +330,11 @@ export function NetworkMetrics() { return ( - Network Traffic - +
+ Network Traffic + {getTimeframeShortLabel()} +
+
diff --git a/AppImage/components/process-detail-modal.tsx b/AppImage/components/process-detail-modal.tsx index ac86f2a7..e9f35751 100644 --- a/AppImage/components/process-detail-modal.tsx +++ b/AppImage/components/process-detail-modal.tsx @@ -10,11 +10,20 @@ import { ProcessInfoModal } from "./process-info-modal" interface ProcessInfo { 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//cmdline etc. only exist + * at the process level. */ + parent_pid?: number user: string cpu: number mem: number rss_kb: number 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 { @@ -30,8 +39,14 @@ interface ProcessDetailModalProps { sort: "cpu" | "mem" } -const REFRESH_MS = 5000 -const LIMIT = 25 +const REFRESH_MS = 3000 +// 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 => { 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) setError(null) try { - const res = await fetchApi(`/api/processes?sort=${sort}&limit=${LIMIT}`) + const res = await fetchApi(`/api/processes?sort=${sort}&limit=${FETCH_LIMIT}`) setData(res) } catch (e: any) { setError(e?.message || "Failed to fetch processes") @@ -72,22 +87,28 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo if (!open) setFilter("") }, [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 const q = filter.toLowerCase() return ( p.command.toLowerCase().includes(q) || + (p.cmdline?.toLowerCase().includes(q) ?? false) || p.user.toLowerCase().includes(q) || String(p.pid).includes(q) ) }) + const filtered = filter ? allMatches : allMatches.slice(0, DISPLAY_LIMIT) const Icon = sort === "cpu" ? Cpu : MemoryStick const title = sort === "cpu" ? "Top processes by CPU" : "Top processes by Memory" const description = sort === "cpu" - ? "Snapshot from `ps` sorted by CPU usage. Auto-refreshes every 5 s while this dialog is open." - : "Snapshot from `ps` sorted by resident memory. 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." + : "Current resident memory per process. Refreshes every 3 s while open." // Accent palette matched to the Overview cards: CPU Usage donut uses // blue (#3b82f6), Memory cached uses rgba(99,102,241,0.55) — we keep @@ -126,7 +147,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
setFilter(e.target.value)} className="pl-8 h-8 text-sm" @@ -161,7 +182,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo