Skip to content

Commit 790b099

Browse files
authored
Adding CPU card (#1)
* Adding CPU Card * Imp service folder * Update screenshot * Update readme
1 parent f94f692 commit 790b099

File tree

27 files changed

+398
-234
lines changed

27 files changed

+398
-234
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ Key principles:
3333
temperature, fan, etc.)
3434
- Auto-refresh UI built with Next.js and SWR
3535
- Host-scope monitoring via Docker (`network_mode: host`)
36-
- Clean modular dashboard: General, Memory, Storage, Network cards
36+
- Clean modular dashboard: General, Memory, CPU, Storage, Network cards
3737
- Mock mode for local development
3838
- Standalone Docker image build
3939

docs/screenshot.png

76 KB
Loading

src/app/api/cards/cpu/route.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { NextResponse } from "next/server";
2+
import { services } from "@/services";
3+
4+
export async function GET() {
5+
const svc = await services.cpu();
6+
const data = await svc.get();
7+
return NextResponse.json(data, { headers: { "Cache-Control": "no-store" } });
8+
}

src/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import GeneralCard from "@/components/cards/GeneralCard";
33
import MemoryCard from "@/components/cards/MemoryCard";
44
import StorageCard from "@/components/cards/StorageCard";
55
import NetworkCard from "@/components/cards/NetworkCard";
6+
import CpuCard from "@/components/cards/CpuCard";
67

78
export default function Dashboard() {
89
return (
@@ -11,6 +12,7 @@ export default function Dashboard() {
1112
<CardGrid>
1213
<GeneralCard />
1314
<MemoryCard />
15+
<CpuCard />
1416
<StorageCard />
1517
<NetworkCard />
1618
</CardGrid>

src/components/cards/CpuCard.tsx

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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+
}

src/components/cards/GeneralCard.tsx

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,11 @@
33
import { CardShell } from "@/components/ui/CardShell";
44
import { StatList } from "@/components/ui/StatList";
55
import { StatRow } from "@/components/ui/StatRow";
6-
import { LoadTriple } from "@/components/ui/LoadTriple";
76
import { CardLoading, CardError } from "@/components/ui/CardState";
8-
import { StatusDot } from "@/components/ui/StatusDot";
9-
import { toneForTempC } from "@/utils/health";
107
import { useGeneral } from "@/hooks/useCardData";
118

129
export default function GeneralCard() {
13-
const { data, error } = useGeneral(1000);
10+
const { data, error } = useGeneral(30_000);
1411

1512
if (error) return <CardError title="General" />;
1613
if (!data) return <CardLoading title="General" />;
@@ -19,44 +16,13 @@ export default function GeneralCard() {
1916
const h = Math.floor(secs / 3600);
2017
const m = Math.floor((secs % 3600) / 60);
2118

22-
const l1 = Number(data.load.l1 ?? 0);
23-
const l5 = Number(data.load.l5 ?? 0);
24-
const l15 = Number(data.load.l15 ?? 0);
25-
const cpus = Number(data.load.cpus ?? 1);
26-
const pct = (v: number) => Math.max(0, Math.min(100, (v / Math.max(1, cpus)) * 100));
27-
28-
const tempC = data.cpuTemp == null ? null : Number(data.cpuTemp);
29-
const tempText = tempC == null ? "n/a" : `${tempC.toFixed(1)}°C`;
30-
const tempTone = tempC == null ? "neutral" : toneForTempC(tempC);
31-
32-
const fanRowText =
33-
data.fanRpm != null && Number.isFinite(Number(data.fanRpm))
34-
? `${Math.round(Number(data.fanRpm))} RPM`
35-
: data.fanDutyPct != null && Number.isFinite(Number(data.fanDutyPct))
36-
? `${Math.round(Number(data.fanDutyPct))}% duty`
37-
: null;
38-
3919
return (
40-
<CardShell
41-
title="General"
42-
>
20+
<CardShell title="General">
4321
<StatList>
4422
<StatRow label="Hostname" value={data.os.hostname} />
4523
<StatRow label="Platform" value={`${data.os.platform} - ${data.os.arch}`} />
46-
<StatRow label="Uptime" value={`${h}h ${m}m`}/>
47-
<StatRow
48-
label="CPU Temp"
49-
value={
50-
<span className="inline-flex items-center gap-2">
51-
<StatusDot tone={tempTone} title={`CPU temperature ${tempText}`} />
52-
<span>{tempText}</span>
53-
</span>
54-
}
55-
/>
56-
{fanRowText && <StatRow label="Fan" value={fanRowText} />}
24+
<StatRow label="Uptime" value={`${h}h ${m}m`} />
5725
</StatList>
58-
59-
<LoadTriple abs={[l1, l5, l15]} pct={[pct(l1), pct(l5), pct(l15)]} />
6026
</CardShell>
6127
);
6228
}

src/components/cards/StorageCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { DiskRow } from "@/components/rows/DiskRow";
55
import { StatRow } from "@/components/ui/StatRow";
66
import { CardLoading, CardError } from "@/components/ui/CardState";
77
import { useStorage } from "@/hooks/useCardData";
8-
import type { StorageInfo } from "@/services/contracts";
8+
import type { StorageInfo } from "@/services/storage/contract";
99

1010
export default function StorageCard() {
1111
const { data, error } = useStorage();

src/hooks/useCardData.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,8 @@ export function useStorage() {
1212
}
1313
export function useNetwork(ms: number) {
1414
return useSWR("/api/cards/network", f, { refreshInterval: ms, revalidateOnFocus: false });
15+
}
16+
17+
export function useCpu(ms: number) {
18+
return useSWR("/api/cards/cpu", f, { refreshInterval: ms, revalidateOnFocus: false });
1519
}

src/services/contracts.ts

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/services/cpu/contract.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export type CpuInfo = {
2+
// usage
3+
totalPct: number; // 0–100
4+
perCorePct: number[]; // length = cpu cores
5+
// load
6+
load: { l1: number; l5: number; l15: number; cpus: number };
7+
// thermals & cooling
8+
tempC: number | null;
9+
fanRpm?: number | null;
10+
fanDutyPct?: number | null;
11+
// top processes by cpu
12+
topCpu: { pid: number; cmd: string; cpuPct: number }[];
13+
};
14+
15+
export interface CpuService { get(): Promise<CpuInfo>; }

0 commit comments

Comments
 (0)