1+ "use client" ;
2+
3+ import { CardShell } from "@/components/ui/CardShell" ;
4+ import { StatList } from "@/components/ui/StatList" ;
5+ import { StatRow } from "@/components/ui/StatRow" ;
6+ import { LoadTriple } from "@/components/ui/LoadTriple" ;
7+ import { Progress } from "@/components/ui/progress" ;
8+ import { CardLoading , CardError } from "@/components/ui/CardState" ;
9+ import { StatusDot } from "@/components/ui/StatusDot" ;
10+ import { toneForTempC } from "@/utils/health" ;
11+ import { ShowMoreButton } from "@/components/ui/ShowMoreButton" ;
12+ import { useState } from "react" ;
13+ import { useCpu } from "@/hooks/useCardData" ;
14+ import type { CpuInfo } from "@/services/cpu/contract" ;
15+
16+ export default function CpuCard ( ) {
17+ const { data, error } = useCpu ( 1000 ) ;
18+ const [ expanded , setExpanded ] = useState ( false ) ;
19+
20+ if ( error ) return < CardError title = "CPU" /> ;
21+ if ( ! data ) return < CardLoading title = "CPU" /> ;
22+
23+ const tempTone = data . tempC == null ? "neutral" : toneForTempC ( data . tempC ) ;
24+
25+ const fanText =
26+ data . fanRpm != null && Number . isFinite ( Number ( data . fanRpm ) )
27+ ? `${ Math . round ( Number ( data . fanRpm ) ) } RPM`
28+ : data . fanDutyPct != null && Number . isFinite ( Number ( data . fanDutyPct ) )
29+ ? `${ Math . round ( Number ( data . fanDutyPct ) ) } % duty`
30+ : "n/a" ;
31+
32+ return (
33+ < CardShell
34+ title = "CPU"
35+ actions = { < ShowMoreButton expanded = { expanded } onToggle = { ( ) => setExpanded ( ( v ) => ! v ) } /> }
36+ >
37+ < StatList >
38+ < StatRow
39+ label = "Total"
40+ value = { < span className = "tabular-nums" > { data . totalPct . toFixed ( 1 ) } %</ span > }
41+ />
42+ </ StatList >
43+
44+ < div className = "mt-3" >
45+ < LoadTriple
46+ abs = { [ data . load . l1 , data . load . l5 , data . load . l15 ] }
47+ pct = { [
48+ Math . min ( 100 , ( data . load . l1 / data . load . cpus ) * 100 ) ,
49+ Math . min ( 100 , ( data . load . l5 / data . load . cpus ) * 100 ) ,
50+ Math . min ( 100 , ( data . load . l15 / data . load . cpus ) * 100 ) ,
51+ ] }
52+ />
53+ </ div >
54+
55+ < div className = "mt-3" >
56+ < StatList >
57+ < StatRow
58+ label = "CPU Temp"
59+ value = {
60+ < span className = "inline-flex items-center gap-2" >
61+ < StatusDot tone = { tempTone } title = { `CPU temperature` } />
62+ < span > { data . tempC == null ? "n/a" : `${ data . tempC . toFixed ( 1 ) } °C` } </ span >
63+ </ span >
64+ }
65+ />
66+ < StatRow label = "Fan" value = { fanText } />
67+ </ StatList >
68+ </ div >
69+
70+ < div className = "mt-3 space-y-2" >
71+ { data . perCorePct . map ( ( v : number , i : number ) => (
72+ < div key = { i } className = "space-y-1" >
73+ < div className = "flex justify-between text-xs text-muted-foreground" >
74+ < span > Core { i } </ span >
75+ < span className = "tabular-nums" > { v . toFixed ( 1 ) } %</ span >
76+ </ div >
77+ < Progress value = { v } className = "h-2" />
78+ </ div >
79+ ) ) }
80+ </ div >
81+
82+ { expanded && (
83+ < div className = "mt-3" >
84+ < div className = "text-sm font-semibold mb-2" > Top processes</ div >
85+ < ul className = "space-y-1" >
86+ { data . topCpu . slice ( 0 , 10 ) . map ( ( p : CpuInfo [ "topCpu" ] [ number ] ) => (
87+ < li key = { p . pid } className = "flex justify-between text-xs text-muted-foreground" >
88+ < span className = "truncate max-w-[60%]" >
89+ { p . cmd } < span className = "opacity-70" > ({ p . pid } )</ span >
90+ </ span >
91+ < span className = "tabular-nums" > { p . cpuPct . toFixed ( 1 ) } %</ span >
92+ </ li >
93+ ) ) }
94+ </ ul >
95+ </ div >
96+ ) }
97+ </ CardShell >
98+ ) ;
99+ }
0 commit comments