"use client" import { useEffect, useState } from "react" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog" import { ScrollArea } from "./ui/scroll-area" import { Activity, FileText, HardDrive, Clock } from "lucide-react" import { fetchApi } from "@/lib/api-config" interface ProcessDetail { pid: number comm: string cmdline: string exe: string | null cwd: string | null state: string ppid: number parent_name: string | null threads: number vm_rss_kb: number vm_size_kb: number vm_swap_kb: number user: string group: string uid: number gid: number start_time: string | null elapsed: string | null cpu: number mem: number io_read_bytes: number | null io_write_bytes: number | null fd_count: number | null captured_at: number } interface ProcessInfoModalProps { pid: number | null accent: { dot: string; bar: string; text: string } onClose: () => void } const REFRESH_MS = 3000 const formatKb = (kb: number | null | undefined): string => { if (kb == null) return "—" if (kb >= 1024 * 1024) return `${(kb / 1024 / 1024).toFixed(2)} GB` if (kb >= 1024) return `${(kb / 1024).toFixed(1)} MB` return `${kb} KB` } const formatBytes = (b: number | null | undefined): string => { if (b == null) return "—" if (b >= 1024 * 1024 * 1024) return `${(b / 1024 / 1024 / 1024).toFixed(2)} GB` if (b >= 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)} MB` if (b >= 1024) return `${(b / 1024).toFixed(1)} KB` return `${b} B` } // Linux process states from /proc//status. The first char of `State:` // is the canonical letter — the rest of the field is a human label like // "(running)". We expand the bare letter to something readable. const stateLabel = (state: string): string => { const letter = (state || "").trim().charAt(0).toUpperCase() const map: Record = { R: "Running", S: "Sleeping", D: "Disk wait", Z: "Zombie", T: "Stopped", t: "Tracing stop", X: "Dead", I: "Idle", } return map[letter] || state || "—" } export function ProcessInfoModal({ pid, accent, onClose }: ProcessInfoModalProps) { const [data, setData] = useState(null) const [error, setError] = useState(null) const [loading, setLoading] = useState(false) const open = pid != null const fetchDetail = async (silent = false) => { if (pid == null) return if (!silent) setLoading(true) setError(null) try { const res = await fetchApi(`/api/processes/${pid}`) setData(res) } catch (e: any) { // 404 means the process exited while the modal was open — surface a // clear message instead of stale data, but don't auto-close (user may // want to read the last snapshot). setError(e?.message?.includes("404") ? "Process exited" : (e?.message || "Failed to fetch process")) if (e?.message?.includes("404")) setData(null) } finally { if (!silent) setLoading(false) } } useEffect(() => { if (pid == null) { setData(null) setError(null) return } fetchDetail() const id = setInterval(() => fetchDetail(true), REFRESH_MS) return () => clearInterval(id) // eslint-disable-next-line react-hooks/exhaustive-deps }, [pid]) return ( { if (!v) onClose() }}> {data?.comm || "Process"} PID {pid} Live snapshot from /proc/{pid}. Auto-refreshes every {REFRESH_MS / 1000} s while open. {error && !data ? (
{error}
) : !data ? (
{loading ? "Loading…" : "—"}
) : (
{/* Overview */}
} title="Overview">
{/* Resources */}
} title="Resources">
{/* Command */}
} title="Command">
{/* Times */}
} title="Lifetime">
)} {data?.captured_at && (
Captured {new Date(data.captured_at * 1000).toLocaleTimeString()} {error ? ` · ${error}` : ""}
)}
) } function Section({ icon, title, children }: { icon: React.ReactNode; title: string; children: React.ReactNode }) { return (
{icon} {title}
{children}
) } function Row({ label, value, mono, wrap, valueClass, }: { label: string value: string mono?: boolean wrap?: boolean valueClass?: string }) { return (
{label}
{value}
) }