From 1ea50d85909e23d5f44a5d1449e3e40b2b66c1f2 Mon Sep 17 00:00:00 2001 From: Jaret Burkett Date: Tue, 7 Oct 2025 08:30:23 -0600 Subject: [PATCH] Add cpu info the the job page --- ui/package-lock.json | 87 +++++++++++++++++++------------ ui/package.json | 1 + ui/src/app/api/cpu/route.ts | 29 +++++++++++ ui/src/components/CPUWidget.tsx | 84 +++++++++++++++++++++++++++++ ui/src/components/JobOverview.tsx | 6 ++- ui/src/hooks/useCPUInfo.tsx | 44 ++++++++++++++++ ui/src/types.ts | 10 ++++ 7 files changed, 228 insertions(+), 33 deletions(-) create mode 100644 ui/src/app/api/cpu/route.ts create mode 100644 ui/src/components/CPUWidget.tsx create mode 100644 ui/src/hooks/useCPUInfo.tsx diff --git a/ui/package-lock.json b/ui/package-lock.json index e2b6183e..05006d4c 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -25,6 +25,7 @@ "react-icons": "^5.5.0", "react-select": "^5.10.1", "sqlite3": "^5.1.7", + "systeminformation": "^5.27.11", "uuid": "^11.1.0", "yaml": "^2.7.0" }, @@ -1576,12 +1577,12 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "dependencies": { "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", + "form-data": "^4.0.4", "proxy-from-env": "^1.1.0" } }, @@ -1668,9 +1669,9 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dependencies": { "balanced-match": "^1.0.0" } @@ -1767,10 +1768,9 @@ } }, "node_modules/cacache/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -2760,13 +2760,14 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -2914,16 +2915,16 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", - "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dependencies": { - "call-bind-apply-helpers": "^1.0.1", + "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "get-proto": "^1.0.0", + "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", @@ -3989,10 +3990,9 @@ } }, "node_modules/node-gyp/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -4763,10 +4763,9 @@ } }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "license": "MIT", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "optional": true, "dependencies": { "balanced-match": "^1.0.0", @@ -5373,6 +5372,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/systeminformation": { + "version": "5.27.11", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.27.11.tgz", + "integrity": "sha512-K3Lto/2m3K2twmKHdgx5B+0in9qhXK4YnoT9rIlgwN/4v7OV5c8IjbeAUkuky/6VzCQC7iKCAqi8rZathCdjHg==", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", @@ -5433,10 +5457,9 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", - "license": "MIT", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", diff --git a/ui/package.json b/ui/package.json index fbc06ef2..683e8cbc 100644 --- a/ui/package.json +++ b/ui/package.json @@ -29,6 +29,7 @@ "react-icons": "^5.5.0", "react-select": "^5.10.1", "sqlite3": "^5.1.7", + "systeminformation": "^5.27.11", "uuid": "^11.1.0", "yaml": "^2.7.0" }, diff --git a/ui/src/app/api/cpu/route.ts b/ui/src/app/api/cpu/route.ts new file mode 100644 index 00000000..0505efb9 --- /dev/null +++ b/ui/src/app/api/cpu/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server'; +import si from 'systeminformation'; +import { CpuInfo } from '@/types'; + +export async function GET() { + try { + const cpuInfoRaw = await si.cpu(); + const memoryData = await si.mem(); + let cpuInfo: CpuInfo = { + name: `${cpuInfoRaw.manufacturer} ${cpuInfoRaw.brand}`, + cores: cpuInfoRaw.cores, + temperature: (await si.cpuTemperature()).main || 0, + totalMemory: memoryData.total / (1024 * 1024), + availableMemory: memoryData.available / (1024 * 1024), + freeMemory: memoryData.free / (1024 * 1024), + currentLoad: (await si.currentLoad()).currentLoad || 0, + }; + + return NextResponse.json(cpuInfo); + } catch (error) { + console.error('Error fetching CPU stats:', error); + return NextResponse.json( + { + error: `Failed to fetch CPU stats: ${error instanceof Error ? error.message : String(error)}`, + }, + { status: 500 }, + ); + } +} diff --git a/ui/src/components/CPUWidget.tsx b/ui/src/components/CPUWidget.tsx new file mode 100644 index 00000000..ed4e11ba --- /dev/null +++ b/ui/src/components/CPUWidget.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { CpuInfo } from '@/types'; +import { Thermometer, Zap, Clock, HardDrive, Fan, Cpu } from 'lucide-react'; + +interface CPUWidgetProps { + cpu: CpuInfo | null; +} + +export default function CPUWidget({ cpu }: CPUWidgetProps) { + const formatMemory = (mb: number): string => { + return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb} MB`; + }; + + const getUtilizationColor = (value: number): string => { + return value < 30 ? 'bg-emerald-500' : value < 70 ? 'bg-amber-500' : 'bg-rose-500'; + }; + + const getTemperatureColor = (temp: number): string => { + return temp < 50 ? 'text-emerald-500' : temp < 80 ? 'text-amber-500' : 'text-rose-500'; + }; + + if (!cpu) { + return ( +
+
+
+

CPU Info

+
+
+
+

No CPU data available

+
+
+ ); + } + + return ( +
+
+
+

{cpu.name}

+ {/* #{1} */} +
+
+ +
+ {/* Temperature, Fan, and Utilization Section */} +
+
+
+ +

CPU Load

+ {cpu.currentLoad.toFixed(1)}% +
+
+
+
+
+
+
+ +

Memory

+ + {(((cpu.totalMemory - cpu.availableMemory) / cpu.totalMemory) * 100).toFixed(1)}% + +
+
+
+
+

+ {formatMemory(cpu.totalMemory - cpu.availableMemory)} / {formatMemory(cpu.totalMemory)} +

+
+
+
+
+ ); +} diff --git a/ui/src/components/JobOverview.tsx b/ui/src/components/JobOverview.tsx index 9f304ca3..df3c6457 100644 --- a/ui/src/components/JobOverview.tsx +++ b/ui/src/components/JobOverview.tsx @@ -1,6 +1,8 @@ import { Job } from '@prisma/client'; import useGPUInfo from '@/hooks/useGPUInfo'; +import useCPUInfo from '@/hooks/useCPUInfo'; import GPUWidget from '@/components/GPUWidget'; +import CPUWidget from '@/components/CPUWidget'; import FilesWidget from '@/components/FilesWidget'; import { getTotalSteps } from '@/utils/jobs'; import { Cpu, HardDrive, Info, Gauge } from 'lucide-react'; @@ -19,6 +21,7 @@ export default function JobOverview({ job }: JobOverviewProps) { const [isScrolledToBottom, setIsScrolledToBottom] = useState(true); const { gpuList, isGPUInfoLoaded } = useGPUInfo(gpuIds, 5000); + const { cpuInfo, isCPUInfoLoaded } = useCPUInfo(5000); const totalSteps = getTotalSteps(job); const progress = (job.step / totalSteps) * 100; const isStopping = job.stop && job.status === 'running'; @@ -154,7 +157,8 @@ export default function JobOverview({ job }: JobOverviewProps) { {/* GPU Widget Panel */}
-
{isGPUInfoLoaded && gpuList.length > 0 && }
+
{isCPUInfoLoaded && cpuInfo && }
+
{isGPUInfoLoaded && gpuList.length > 0 && }
diff --git a/ui/src/hooks/useCPUInfo.tsx b/ui/src/hooks/useCPUInfo.tsx new file mode 100644 index 00000000..913b749d --- /dev/null +++ b/ui/src/hooks/useCPUInfo.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { CpuInfo } from '@/types'; +import { useEffect, useState } from 'react'; +import { apiClient } from '@/utils/api'; + +export default function useCPUInfo(reloadInterval: null | number = null) { + const [cpuInfo, setCpuInfo] = useState(null); + const [isCPUInfoLoaded, setIsLoaded] = useState(false); + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + + const fetchCpuInfo = async () => { + setStatus('loading'); + try { + const data: CpuInfo = await apiClient.get('/api/cpu').then(res => res.data); + setCpuInfo(data); + setStatus('success'); + } catch (err) { + console.error(`Failed to fetch CPU data: ${err instanceof Error ? err.message : String(err)}`); + setStatus('error'); + } finally { + setIsLoaded(true); + } + }; + + useEffect(() => { + // Fetch immediately on component mount + fetchCpuInfo(); + + // Set up interval if specified + if (reloadInterval) { + const interval = setInterval(() => { + fetchCpuInfo(); + }, reloadInterval); + + // Cleanup interval on unmount + return () => { + clearInterval(interval); + }; + } + }, [reloadInterval]); // Added dependencies + + return { cpuInfo, isCPUInfoLoaded, status, refreshCpuInfo: fetchCpuInfo }; +} diff --git a/ui/src/types.ts b/ui/src/types.ts index 71048ca5..ba55d8da 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -39,6 +39,16 @@ export interface GpuInfo { fan: GpuFan; } +export interface CpuInfo { + name: string; + cores: number; + temperature: number; + totalMemory: number; + freeMemory: number; + availableMemory: number; + currentLoad: number; +} + export interface GPUApiResponse { hasNvidiaSmi: boolean; gpus: GpuInfo[];