mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-11 19:07:01 +00:00
Update beta 1.2.2.1
This commit is contained in:
@@ -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 (
|
||||
<Card className="bg-card border-border">
|
||||
<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>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="flex flex-col gap-0.5 min-w-0">
|
||||
<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>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-3 mb-3">
|
||||
|
||||
@@ -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/<pid>/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<ProcessesResponse>(`/api/processes?sort=${sort}&limit=${LIMIT}`)
|
||||
const res = await fetchApi<ProcessesResponse>(`/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
|
||||
<div className="relative mb-2">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Filter by command, user or PID..."
|
||||
placeholder="Filter by command line, user or PID..."
|
||||
value={filter}
|
||||
onChange={(e) => setFilter(e.target.value)}
|
||||
className="pl-8 h-8 text-sm"
|
||||
@@ -161,7 +182,7 @@ export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailMo
|
||||
<button
|
||||
key={p.pid}
|
||||
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}`}
|
||||
>
|
||||
<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>
|
||||
</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 */}
|
||||
<span
|
||||
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 && (
|
||||
<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>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
Reference in New Issue
Block a user