StatsWidget.tsx
98 lines1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import { useState, useEffect } from 'react';
interface Stats {
Hostname: string;
OS: string;
Arch: string;
NumCPU: number;
CPUPercent: number;
MemTotal: string;
MemUsed: string;
MemPercent: number;
DiskTotal: string;
DiskUsed: string;
DiskPercent: number;
LoadAvg: string;
}
function Gauge({ percent, color, label, detail }: { percent: number; color: string; label: string; detail: string }) {
return (
<div className="rounded-xl bg-base-100/50 backdrop-blur-sm border border-white/[0.06] p-4 flex items-center gap-3">
<div className="relative shrink-0">
<svg viewBox="0 0 36 36" className="w-12 h-12 -rotate-90">
<circle cx="18" cy="18" r="15.9155" fill="none" strokeWidth="2.5" className="stroke-base-content/[0.08]" />
<circle cx="18" cy="18" r="15.9155" fill="none" strokeWidth="2.5" strokeLinecap="round"
strokeDasharray={`${percent}, 100`}
className={`transition-all duration-500 ${color}`} />
</svg>
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-[10px] font-bold">{percent}</span>
</div>
</div>
<div>
<div className="text-[10px] font-medium uppercase tracking-widest text-base-content/30">{label}</div>
<div className="text-xs text-base-content/50 mt-0.5">{detail}</div>
</div>
</div>
);
}
function colorForPercent(percent: number, lowColor: string): string {
if (percent >= 80) return 'stroke-error';
if (percent >= 60) return 'stroke-warning';
return lowColor;
}
export function StatsWidget() {
const [stats, setStats] = useState<Stats | null>(null);
useEffect(() => {
const fetchStats = () => {
fetch('/api/stats')
.then(r => r.json())
.then(setStats)
.catch(() => {});
};
fetchStats();
const interval = setInterval(fetchStats, 5000);
return () => clearInterval(interval);
}, []);
if (!stats) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[0, 1, 2, 3].map(i => (
<div key={i} className="h-[84px] rounded-xl bg-base-100/30 animate-pulse" />
))}
</div>
);
}
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Gauge
percent={stats.CPUPercent}
color={colorForPercent(stats.CPUPercent, 'stroke-emerald-400')}
label="CPU"
detail={`${stats.NumCPU}c${stats.LoadAvg ? ` · load ${stats.LoadAvg}` : ''}`}
/>
<Gauge
percent={stats.MemPercent}
color={colorForPercent(stats.MemPercent, 'stroke-cyan-400')}
label="Memory"
detail={`${stats.MemUsed} / ${stats.MemTotal}`}
/>
<Gauge
percent={stats.DiskPercent}
color={colorForPercent(stats.DiskPercent, 'stroke-violet-400')}
label="Disk"
detail={`${stats.DiskUsed} / ${stats.DiskTotal}`}
/>
<div className="rounded-xl bg-base-100/50 backdrop-blur-sm border border-white/[0.06] p-4">
<div className="text-[10px] font-medium uppercase tracking-widest text-base-content/30 mb-2">Host</div>
<div className="font-semibold text-sm truncate">{stats.Hostname}</div>
<div className="text-xs text-base-content/40 mt-1">{stats.NumCPU} vCPU · {stats.Arch}</div>
</div>
</div>
);
}