"use client" import { useEffect, useState } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog" import { Input } from "./ui/input" import { ScrollArea } from "./ui/scroll-area" import { Cpu, MemoryStick, Search } from "lucide-react" import { fetchApi } from "@/lib/api-config" import { ProcessInfoModal } from "./process-info-modal" interface ProcessInfo { pid: number user: string cpu: number mem: number rss_kb: number command: string } interface ProcessesResponse { processes: ProcessInfo[] sort: "cpu" | "mem" captured_at: number } interface ProcessDetailModalProps { open: boolean onOpenChange: (open: boolean) => void /** Which metric the parent card represents (drives default sort + emphasis) */ sort: "cpu" | "mem" } const REFRESH_MS = 5000 const LIMIT = 25 const formatRss = (kb: number): string => { if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(2)} GB` if (kb >= 1024) return `${(kb / 1024).toFixed(1)} MB` return `${kb} KB` } export function ProcessDetailModal({ open, onOpenChange, sort }: ProcessDetailModalProps) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const [filter, setFilter] = useState("") const [selectedPid, setSelectedPid] = useState(null) const fetchProcesses = async (silent = false) => { if (!silent) setLoading(true) setError(null) try { const res = await fetchApi(`/api/processes?sort=${sort}&limit=${LIMIT}`) setData(res) } catch (e: any) { setError(e?.message || "Failed to fetch processes") } finally { if (!silent) setLoading(false) } } useEffect(() => { if (!open) return fetchProcesses() const id = setInterval(() => fetchProcesses(true), REFRESH_MS) return () => clearInterval(id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [open, sort]) // Reset filter when dialog closes useEffect(() => { if (!open) setFilter("") }, [open]) const filtered = (data?.processes ?? []).filter((p) => { if (!filter) return true const q = filter.toLowerCase() return ( p.command.toLowerCase().includes(q) || p.user.toLowerCase().includes(q) || String(p.pid).includes(q) ) }) 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." // Accent palette matched to the Overview cards: CPU Usage donut uses // blue (#3b82f6), Memory cached uses rgba(99,102,241,0.55) — we keep // the same hues so the modal feels like a continuation of the card. const accent = sort === "cpu" ? { dot: "#3b82f6", bar: "#3b82f6", text: "text-blue-500" } : { dot: "#6366f1", bar: "#6366f1", text: "text-indigo-400" } // Scale bars to the largest value in the (filtered) list so the visual // ranking is preserved even when no process is near 100 %. CPU can // exceed 100 % on multi-threaded apps — falling back to max=1 prevents // a divide-by-zero when the list is empty. const maxPrimary = Math.max( 1, ...filtered.map((p) => (sort === "cpu" ? p.cpu : p.mem)) ) // Mobile drops PID + USER; desktop keeps the full 5-column layout. // CPU and MEM columns are wider on desktop with a real gap between // them so the two metrics don't feel glued together. const gridCols = "grid-cols-[minmax(0,1fr)_70px_90px] sm:grid-cols-[60px_96px_minmax(140px,1fr)_110px_120px]" return ( <> {title} {description}
setFilter(e.target.value)} className="pl-8 h-8 text-sm" />
{error ? (
{error}
) : (
{/* Sticky solid header so scrolled rows don't bleed through */}
PID
User
Command
CPU %
{sort === "mem" ? "Memory" : "Mem %"}
{filtered.length === 0 && !loading ? (
No processes match the filter
) : ( filtered.map((p) => { const primary = sort === "cpu" ? p.cpu : p.mem const barPct = Math.min(100, (primary / maxPrimary) * 100) return ( ) }) )}
)} {data?.captured_at && (
Captured {new Date(data.captured_at * 1000).toLocaleTimeString()} · {filtered.length} of {data.processes.length} shown
)}
setSelectedPid(null)} /> ) }