StatsWidget.tsx

98 lines
1 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>
    );
}