11"use client" ;
22
3+ import { useMemo , useState } from "react" ;
34import { CardShell } from "@/components/ui/CardShell" ;
45import { StatList } from "@/components/ui/StatList" ;
56import { StatRow } from "@/components/ui/StatRow" ;
67import { LoadTriple } from "@/components/ui/LoadTriple" ;
7- import { Progress } from "@/components/ui/progress" ;
88import { CardLoading , CardError } from "@/components/ui/CardState" ;
9- import { StatusDot } from "@/components/ui/StatusDot" ;
10- import { toneForTempC } from "@/utils/health" ;
119import { ShowMoreButton } from "@/components/ui/ShowMoreButton" ;
12- import { useState } from "react " ;
10+ import { Collapsible } from "@/components/ui/Collapsible " ;
1311import { useCpu } from "@/hooks/useCardData" ;
1412import type { CpuInfo } from "@/services/cpu/contract" ;
13+ import { Section } from "@/components/ui/Section" ;
14+ import { PerCoreList } from "@/components/rows/PerCoreList" ;
15+ import { ProcessList } from "@/components/rows/ProcessList" ;
16+ import { SummaryTitle } from "@/components/ui/SummaryTitle" ;
17+
18+ function fmtFan ( info : Pick < CpuInfo , "fanRpm" | "fanDutyPct" > ) {
19+ const rpm = Number ( info . fanRpm ) ;
20+ const duty = Number ( info . fanDutyPct ) ;
21+ if ( Number . isFinite ( rpm ) ) return `${ Math . round ( rpm ) } RPM` ;
22+ if ( Number . isFinite ( duty ) ) return `${ Math . round ( duty ) } % duty` ;
23+ return "n/a" ;
24+ }
1525
1626export default function CpuCard ( ) {
1727 const { data, error } = useCpu ( 1000 ) ;
1828 const [ expanded , setExpanded ] = useState ( false ) ;
1929
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 ) ;
30+ const snapshot = useMemo ( ( ) => {
31+ if ( ! data ) return null ;
32+ const pctTriple : [ number , number , number ] = [
33+ Math . min ( 100 , ( data . load . l1 / data . load . cpus ) * 100 ) ,
34+ Math . min ( 100 , ( data . load . l5 / data . load . cpus ) * 100 ) ,
35+ Math . min ( 100 , ( data . load . l15 / data . load . cpus ) * 100 ) ,
36+ ] ;
37+ return {
38+ totalPct : data . totalPct ,
39+ pctTriple,
40+ tempText : data . tempC == null ? "n/a" : `${ data . tempC . toFixed ( 1 ) } °C` ,
41+ fanText : fmtFan ( data ) ,
42+ perCore : data . perCorePct ,
43+ topCpu : data . topCpu ,
44+ } ;
45+ } , [ data ] ) ;
2446
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" ;
47+ if ( error ) return < CardError title = "CPU" /> ;
48+ if ( ! snapshot ) return < CardLoading title = "CPU" /> ;
3149
3250 return (
33- < CardShell
34- title = "CPU"
35- actions = { < ShowMoreButton expanded = { expanded } onToggle = { ( ) => setExpanded ( ( v ) => ! v ) } /> }
36- >
51+ < CardShell title = "CPU" actions = { < ShowMoreButton expanded = { expanded } onToggle = { ( ) => setExpanded ( v => ! v ) } /> } >
3752 < StatList >
38- < StatRow
39- label = "Total"
40- value = { < span className = "tabular-nums" > { data . totalPct . toFixed ( 1 ) } %</ span > }
41- />
53+ < StatRow label = "Total" value = { < span className = "tabular-nums" > { snapshot . totalPct . toFixed ( 1 ) } %</ span > } />
4254 </ StatList >
4355
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 >
56+ < Section >
57+ < LoadTriple pct = { snapshot . pctTriple } />
58+ </ Section >
5459
55- < div className = "mt-3" >
60+ < Section >
5661 < 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 } />
62+ < StatRow label = "CPU Temp" value = { < span className = "inline-flex items-center gap-2" > { snapshot . tempText } </ span > } />
63+ < StatRow label = "Fan" value = { snapshot . fanText } />
6764 </ StatList >
68- </ div >
65+ </ Section >
6966
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 >
67+ < PerCoreList values = { snapshot . perCore } />
8168
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- ) }
69+ < Collapsible expanded = { expanded } summary = { < SummaryTitle text = "Top processes" /> } >
70+ < ProcessList
71+ items = { snapshot . topCpu . map ( ( p : CpuInfo [ "topCpu" ] [ number ] ) => ( {
72+ pid : p . pid ,
73+ cmd : p . cmd ,
74+ cpuPct : p . cpuPct ,
75+ } ) ) }
76+ />
77+ </ Collapsible >
9778 </ CardShell >
9879 ) ;
9980}
0 commit comments