Files
nvcurve/frontend/src/App.tsx
T
ARIA c11ea73ead feat: GPU process list with per-process VRAM/utilization and kill
Add a GPU process table to the Performance tab showing which processes
use the selected GPU, with per-process VRAM, GPU utilization, CPU, host
memory, and command. Includes a kill endpoint (SIGTERM/SIGKILL, with
optional parent kill) behind a confirm dialog.

- nvcurve/hal/processes.py: NVML + /proc process enumeration
- server.py: GET /api/processes, POST /api/processes/kill (refuses to
  signal init/kthreadd)
- frontend: ProcessList component, API client, types
- tests: test_processes.py
- docs: API reference for the new endpoints
2026-10-03 20:25:33 +02:00

334 lines
12 KiB
TypeScript

import { useGpu } from "./hooks/useGpu.js";
import { useCurve } from "./hooks/useCurve.js";
import { useMonitor } from "./hooks/useMonitor.js";
import { useDashboard } from "./hooks/useDashboard.js";
import { StatusBar } from "./components/Monitor/StatusBar.js";
import { LiveMonitor } from "./components/Monitor/LiveMonitor.js";
import { Dashboard } from "./components/Dashboard/Dashboard.js";
import { CurveEditor } from "./components/CurveEditor/CurveEditor.js";
import { PointTable } from "./components/PointTable/PointTable.js";
import { PerformancePanel } from "./components/Limits/PerformancePanel.js";
import { PerformanceMonitor } from "./components/Monitor/PerformanceMonitor.js";
import { ProcessList } from "./components/Monitor/ProcessList.js";
import { FanMonitor } from "./components/Monitor/FanMonitor.js";
import { FanCurveEditor } from "./components/Fans/FanCurveEditor.js";
import { WireViewPanel } from "./components/WireView/WireViewPanel.js";
import { ProfilePanel } from "./components/Profiles/ProfilePanel.js";
import { api, onUnauthorized } from "./api/client.js";
import { LoginScreen } from "./components/Auth/LoginScreen.js";
import { useCurveStore } from "./store/curveStore.js";
import { useWireview } from "./hooks/useWireview.js";
import { Toaster } from "sonner";
import { Loader, ChevronDown } from "lucide-react";
import { useState, useRef, useEffect } from "react";
import type { FanState } from "./types.js";
type AuthState = "checking" | "login" | "ok";
export default function App() {
const [authState, setAuthState] = useState<AuthState>("checking");
const [authUser, setAuthUser] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
api
.authStatus()
.then((s) => {
if (cancelled) return;
if (s.auth_required && !s.authenticated) {
setAuthState("login");
} else {
setAuthUser(s.username);
setAuthState("ok");
}
})
.catch(() => {
// Server unreachable — fall through to the normal (no-auth) flow.
if (!cancelled) setAuthState("ok");
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
onUnauthorized(() => setAuthState("login"));
return () => onUnauthorized(null);
}, []);
function handleLogout() {
api.logout().catch(() => {});
setAuthUser(null);
setAuthState("login");
}
if (authState === "checking") {
return (
<div className="min-h-screen bg-zinc-950 flex items-center justify-center">
<Loader className="animate-spin text-zinc-500" size={28} />
</div>
);
}
if (authState === "login") {
return (
<LoginScreen
onSuccess={(username) => {
setAuthUser(username);
setAuthState("ok");
}}
/>
);
}
return <MainApp user={authUser} onLogout={handleLogout} />;
}
function MainApp({
user,
onLogout,
}: {
user: string | null;
onLogout: () => void;
}) {
const gpuInfo = useGpu();
const { curve, wsStatus: curveWsStatus } = useCurve();
const { monitor, monitorHistory, wsStatus: monitorWsStatus } = useMonitor();
const { dashboard, loading: dashboardLoading } = useDashboard();
const { setCurve, activeProfile, setActiveProfile, selectedGpuIndex } =
useCurveStore();
const wireview = useWireview();
const [activeTab, setActiveTab] = useState<
"dashboard" | "curve" | "performance" | "fans" | "wireview"
>("dashboard");
const [fanState, setFanState] = useState<FanState | null>(null);
const [activeDomain, setActiveDomain] = useState<"gpu" | "memory">("gpu");
const [isProfileOpen, setIsProfileOpen] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
const currentVoltageMv = monitor?.voltage_mv ?? null;
function handleRefresh() {
api.curve(selectedGpuIndex).then(setCurve).catch(console.error);
}
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (
profileRef.current &&
!profileRef.current.contains(event.target as Node)
) {
setIsProfileOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
// Fetch initial active profile
api
.profiles(selectedGpuIndex)
.then((data) => setActiveProfile(data.active))
.catch(console.error);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [selectedGpuIndex, setActiveProfile]);
function refreshFans() {
api.fans(selectedGpuIndex).then(setFanState).catch(console.error);
}
useEffect(() => {
if (activeTab === "fans") {
refreshFans();
}
}, [activeTab, selectedGpuIndex]);
// Worst connection status wins
const wsStatus =
monitorWsStatus === "disconnected" || curveWsStatus === "disconnected"
? "disconnected"
: monitorWsStatus === "connecting" || curveWsStatus === "connecting"
? "connecting"
: "connected";
return (
<div className="min-h-screen bg-zinc-950 text-zinc-100 flex flex-col">
<Toaster theme="dark" position="top-right" />
<StatusBar
gpuInfo={gpuInfo}
wsStatus={wsStatus}
monitor={monitor}
user={user}
onLogout={onLogout}
/>
{wsStatus !== "connected" && (
<div className="bg-amber-500/10 border-b border-amber-500/20 text-amber-500 px-4 py-2 text-center text-sm font-medium flex justify-center items-center gap-2">
<Loader size={14} className="animate-spin" />
{wsStatus === "connecting"
? "Reconnecting to backend..."
: "Connection lost. Retrying..."}
</div>
)}
<main className="flex-1 p-4 flex flex-col gap-4 w-full max-w-[1600px] mx-auto">
{/* Tab Header and Profile Selector */}
<div className="flex justify-between items-end border-b border-zinc-800 pb-2">
<div className="flex gap-6">
<button
onClick={() => setActiveTab("dashboard")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === "dashboard" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
Dashboard
</button>
<button
onClick={() => setActiveTab("curve")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === "curve" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
Curve
</button>
<button
onClick={() => setActiveTab("performance")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === "performance" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
Performance
</button>
<button
onClick={() => setActiveTab("fans")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors ${activeTab === "fans" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
Fans
</button>
{wireview.available && (
<button
onClick={() => setActiveTab("wireview")}
className={`text-lg font-medium pb-2 -mb-[9px] border-b-2 transition-colors flex items-center gap-2 ${activeTab === "wireview" ? "border-pink-500 text-zinc-100" : "border-transparent text-zinc-500 hover:text-zinc-300"}`}
>
WireView
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
</button>
)}
</div>
<div className="relative" ref={profileRef}>
<button
onClick={() => setIsProfileOpen(!isProfileOpen)}
className="flex items-center gap-2 text-zinc-400 hover:text-zinc-100 transition-colors text-sm font-medium pb-2 -mb-[9px]"
>
{activeProfile ? (
<>
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 shrink-0" />
<span className="text-zinc-200">{activeProfile}</span>
</>
) : (
"Profiles"
)}
<ChevronDown
size={14}
className={
isProfileOpen
? "rotate-180 transition-transform"
: "transition-transform"
}
/>
</button>
{isProfileOpen && (
<div className="absolute right-0 top-full mt-2 w-80 z-50 shadow-[0_8px_30px_rgb(0,0,0,0.8)] border border-zinc-700/50 rounded-lg overflow-hidden">
<ProfilePanel
activeProfile={activeProfile}
onProfileApplied={setActiveProfile}
/>
</div>
)}
</div>
</div>
{/* Main content area */}
<div className="flex flex-col gap-4 w-full">
{activeTab === "dashboard" ? (
<Dashboard
monitor={monitor}
history={monitorHistory}
dashboard={dashboard}
loading={dashboardLoading}
/>
) : activeTab === "curve" ? (
<>
<div className="flex gap-4 items-stretch w-full">
<div className="flex-1 min-w-0 flex flex-col">
{curve ? (
<CurveEditor
curve={curve}
activeDomain={activeDomain}
onDomainChange={setActiveDomain}
currentVoltageMv={currentVoltageMv}
currentClockMhz={monitor?.clock_mhz ?? null}
onRefresh={handleRefresh}
/>
) : (
<div className="bg-zinc-900 rounded-lg p-8 text-center text-zinc-500 h-full flex items-center justify-center">
Loading curve…
</div>
)}
</div>
<div className="w-80 shrink-0 flex flex-col">
<LiveMonitor monitor={monitor} history={monitorHistory} />
</div>
</div>
{curve && (
<div className="w-full">
<PointTable
points={curve.points.filter(
(p) => p.domain === activeDomain,
)}
currentVoltageMv={
activeDomain === "gpu" ? currentVoltageMv : null
}
readOnly={activeDomain === "memory"}
/>
</div>
)}
</>
) : activeTab === "performance" ? (
<div className="flex flex-col gap-4 w-full">
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">
<PerformancePanel />
</div>
<div className="w-80 shrink-0 flex flex-col">
<PerformanceMonitor
monitor={monitor}
history={monitorHistory}
/>
</div>
</div>
<ProcessList />
</div>
) : activeTab === "wireview" ? (
<WireViewPanel
info={wireview.info}
sample={wireview.sample}
history={wireview.history}
/>
) : (
<div className="flex gap-4 items-start w-full">
<div className="flex-1 min-w-0">
<FanCurveEditor onChanged={refreshFans} />
</div>
<div className="w-80 shrink-0 flex flex-col">
<FanMonitor
monitor={monitor}
history={monitorHistory}
fanCurve={fanState?.curve ?? null}
fanCurveActive={fanState?.curve_active ?? false}
/>
</div>
</div>
)}
</div>
</main>
</div>
);
}