redesign: modern System Monitor with gauge rings + fix OCPlatform
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m0s
All checks were successful
Build & Deploy Dashboard / deploy (push) Successful in 1m0s
This commit is contained in:
@@ -2,16 +2,21 @@
|
|||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { Cpu, MemoryStick, HardDrive, Clock, RefreshCw, Wifi, Activity } from "lucide-react";
|
import { RefreshCw, Cpu, HardDrive, Wifi, Clock, Activity } from "lucide-react";
|
||||||
|
|
||||||
const TABS = ["Openclaw", "Сервисы"] as const;
|
const TABS = ["Openclaw", "Сервисы"] as const;
|
||||||
type TabName = typeof TABS[number];
|
type TabName = typeof TABS[number];
|
||||||
|
|
||||||
const MACHINE_MAP: Record<TabName, string> = {
|
const MACHINE_MAP: Record<TabName, string> = {
|
||||||
"Openclaw": "ocplatform",
|
"Openclaw": "openclaw",
|
||||||
"Сервисы": "services",
|
"Сервисы": "services",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MACHINE_INFO: Record<TabName, { ip: string; os: string }> = {
|
||||||
|
"Openclaw": { ip: "192.168.31.103", os: "Ubuntu 22.04" },
|
||||||
|
"Сервисы": { ip: "192.168.31.60", os: "Ubuntu 22.04" },
|
||||||
|
};
|
||||||
|
|
||||||
interface Metrics {
|
interface Metrics {
|
||||||
cpu: number | null;
|
cpu: number | null;
|
||||||
load1: number | null;
|
load1: number | null;
|
||||||
@@ -22,57 +27,95 @@ interface Metrics {
|
|||||||
network: { rxBytes: number | null; txBytes: number | null };
|
network: { rxBytes: number | null; txBytes: number | null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function CircularGauge({ value, size = 96, color, label }: { value: number; size?: number; color: string; label: string }) {
|
function formatBytes(bytes: number | null): string {
|
||||||
const radius = (size - 14) / 2;
|
if (!bytes) return "—";
|
||||||
const circumference = 2 * Math.PI * radius;
|
const gb = bytes / 1024 / 1024 / 1024;
|
||||||
const offset = circumference - (value / 100) * circumference;
|
if (gb >= 1) return gb.toFixed(1) + " GB";
|
||||||
|
return (bytes / 1024 / 1024).toFixed(0) + " MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
function GaugeRing({
|
||||||
|
value, size = 120, strokeWidth = 10, color, label, sub
|
||||||
|
}: {
|
||||||
|
value: number; size?: number; strokeWidth?: number;
|
||||||
|
color: string; label: string; sub?: string;
|
||||||
|
}) {
|
||||||
|
const r = (size - strokeWidth * 2) / 2;
|
||||||
|
const cx = size / 2;
|
||||||
|
const cy = size / 2;
|
||||||
|
const circumference = 2 * Math.PI * r;
|
||||||
|
const filled = (value / 100) * circumference;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-2">
|
<div className="flex flex-col items-center gap-2">
|
||||||
<div className="relative" style={{ width: size, height: size }}>
|
<div className="relative" style={{ width: size, height: size }}>
|
||||||
<svg width={size} height={size} className="-rotate-90">
|
<svg width={size} height={size} style={{ transform: "rotate(-90deg)" }}>
|
||||||
<defs>
|
|
||||||
<linearGradient id={`gauge-gradient-${label}`} x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" stopColor={color} stopOpacity="0.9" />
|
|
||||||
<stop offset="100%" stopColor={color} stopOpacity="0.4" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<circle cx={size/2} cy={size/2} r={radius} fill="none" stroke="rgba(255,255,255,0.05)" strokeWidth="10" />
|
|
||||||
<circle
|
<circle
|
||||||
cx={size/2} cy={size/2} r={radius}
|
cx={cx} cy={cy} r={r}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke={`url(#gauge-gradient-${label})`}
|
stroke="rgba(255,255,255,0.05)"
|
||||||
strokeWidth="10" strokeLinecap="round"
|
strokeWidth={strokeWidth}
|
||||||
strokeDasharray={circumference} strokeDashoffset={offset}
|
/>
|
||||||
style={{ transition: "stroke-dashoffset 0.8s ease", filter: `drop-shadow(0 0 8px ${color}60)` }}
|
<circle
|
||||||
|
cx={cx} cy={cy} r={r}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={`${filled} ${circumference - filled}`}
|
||||||
|
style={{ transition: "stroke-dasharray 0.8s ease", filter: `drop-shadow(0 0 6px ${color}60)` }}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
<span className="text-lg font-bold text-white">{value}%</span>
|
<span className="text-2xl font-bold text-white">{value}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-slate-400 font-medium">{label}</span>
|
<div className="text-center">
|
||||||
|
<div className="text-sm font-medium text-slate-300">{label}</div>
|
||||||
|
{sub && <div className="text-xs text-slate-600 mt-0.5">{sub}</div>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsageBar({ label, value, color, detail }: { label: string; value: number; color: string; detail?: string }) {
|
function MetricBar({ label, value, max, unit, color }: {
|
||||||
|
label: string; value: number; max: number; unit: string; color: string;
|
||||||
|
}) {
|
||||||
|
const pct = Math.min(100, (value / max) * 100);
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5">
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="text-slate-400 font-medium">{label}</span>
|
<span className="text-slate-400">{label}</span>
|
||||||
<span className="text-white font-semibold">{detail || `${value}%`}</span>
|
<span className="text-slate-200 font-medium">{value.toFixed(1)} / {max.toFixed(1)} {unit}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 rounded-full overflow-hidden bg-white/5">
|
<div className="h-1.5 rounded-full" style={{ background: "rgba(255,255,255,0.05)" }}>
|
||||||
<div
|
<div
|
||||||
className="h-full rounded-full transition-all duration-700"
|
className="h-full rounded-full transition-all duration-700"
|
||||||
style={{ width: `${value}%`, background: color, boxShadow: `0 0 8px ${color}80` }}
|
style={{ width: `${pct}%`, background: color, boxShadow: `0 0 8px ${color}60` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StatCard({ icon: Icon, label, value, sub, color }: {
|
||||||
|
icon: React.ElementType; label: string; value: string; sub?: string; color: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="card p-4 flex items-center gap-3" style={{ borderTop: `2px solid ${color}` }}>
|
||||||
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
|
style={{ background: `${color}15` }}>
|
||||||
|
<Icon className="w-5 h-5" style={{ color }} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-500">{label}</div>
|
||||||
|
<div className="text-base font-bold text-white">{value}</div>
|
||||||
|
{sub && <div className="text-xs text-slate-600">{sub}</div>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SystemPage() {
|
export default function SystemPage() {
|
||||||
const [activeTab, setActiveTab] = useState<TabName>("Openclaw");
|
const [activeTab, setActiveTab] = useState<TabName>("Openclaw");
|
||||||
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
const [metrics, setMetrics] = useState<Metrics | null>(null);
|
||||||
@@ -84,46 +127,46 @@ export default function SystemPage() {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(false);
|
setError(false);
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/metrics?machine=${MACHINE_MAP[activeTab]}`);
|
const res = await fetch(`/api/metrics?machine=${MACHINE_MAP[activeTab]}`, { cache: "no-store" });
|
||||||
if (!res.ok) throw new Error();
|
if (!res.ok) throw new Error();
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setMetrics(data);
|
setMetrics(data);
|
||||||
setLastUpdated(new Date());
|
setLastUpdated(new Date());
|
||||||
} catch {
|
} catch {
|
||||||
setError(true);
|
setError(true);
|
||||||
|
setMetrics(null);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [activeTab]);
|
}, [activeTab]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setMetrics(null);
|
||||||
|
setError(false);
|
||||||
fetchMetrics();
|
fetchMetrics();
|
||||||
const interval = setInterval(fetchMetrics, 30000);
|
const interval = setInterval(fetchMetrics, 30000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [fetchMetrics]);
|
}, [fetchMetrics]);
|
||||||
|
|
||||||
const formatBytes = (bytes: number | null) => {
|
const info = MACHINE_INFO[activeTab];
|
||||||
if (bytes == null) return "—";
|
|
||||||
const gb = bytes / 1024 / 1024 / 1024;
|
|
||||||
if (gb >= 1) return `${gb.toFixed(1)} GB`;
|
|
||||||
const mb = bytes / 1024 / 1024;
|
|
||||||
return `${mb.toFixed(0)} MB`;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5 max-w-7xl mx-auto">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between pt-1">
|
<div className="flex items-start justify-between pt-1">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-white">System Monitor</h1>
|
<h1 className="text-2xl font-bold text-white">System Monitor</h1>
|
||||||
<p className="text-slate-500 text-sm mt-0.5">
|
<p className="text-sm text-slate-500 mt-0.5">
|
||||||
{lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` : "Загрузка..."}
|
{lastUpdated
|
||||||
|
? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}`
|
||||||
|
: "Загрузка..."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={fetchMetrics}
|
onClick={fetchMetrics}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="flex items-center gap-2 px-4 py-2 rounded-xl border border-white/10 text-slate-300 text-sm transition-all hover:border-white/20 hover:bg-white/5 disabled:opacity-50 bg-white/3"
|
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm text-slate-400 hover:text-white transition-all disabled:opacity-50"
|
||||||
|
style={{ background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)" }}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
<RefreshCw className={`w-4 h-4 ${loading ? "animate-spin" : ""}`} />
|
||||||
Обновить
|
Обновить
|
||||||
@@ -131,103 +174,152 @@ export default function SystemPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="flex gap-1 p-1 rounded-2xl w-fit bg-white/5">
|
<div className="flex gap-1 p-1 rounded-2xl w-fit" style={{ background: "rgba(255,255,255,0.04)" }}>
|
||||||
{TABS.map((tab) => (
|
{TABS.map(tab => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
onClick={() => setActiveTab(tab)}
|
onClick={() => setActiveTab(tab)}
|
||||||
className={`px-5 py-2 rounded-xl text-sm font-medium transition-all ${
|
className="px-5 py-2 rounded-xl text-sm font-medium transition-all"
|
||||||
activeTab === tab
|
style={activeTab === tab ? {
|
||||||
? "text-white shadow-lg shadow-indigo-500/20"
|
background: "linear-gradient(135deg,#6366f1,#8b5cf6)",
|
||||||
: "text-slate-500 hover:text-slate-300"
|
color: "white",
|
||||||
}`}
|
boxShadow: "0 4px 20px rgba(99,102,241,0.3)"
|
||||||
style={activeTab === tab ? { background: "linear-gradient(135deg, #6366f1, #8b5cf6)" } : {}}
|
} : {
|
||||||
|
color: "#475569",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{tab}
|
{tab}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Machine info badge */}
|
||||||
|
{!loading && !error && (
|
||||||
|
<div className="flex items-center gap-3 text-xs text-slate-600">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400 inline-block" style={{ boxShadow: "0 0 6px #10b981" }} />
|
||||||
|
<span className="text-slate-400">Online</span>
|
||||||
|
</div>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{info.ip}</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{info.os}</span>
|
||||||
|
{metrics?.cpuCount ? <><span>·</span><span>{metrics.cpuCount} CPU cores</span></> : null}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{error ? (
|
{error ? (
|
||||||
<div className="card p-8 text-center">
|
<div className="card p-8 text-center space-y-3">
|
||||||
<div className="text-red-400 text-sm mb-2">⚠️ Не удалось получить метрики</div>
|
<div className="text-4xl">⚠️</div>
|
||||||
<div className="text-slate-600 text-xs">Убедитесь что node_exporter запущен на хосте</div>
|
<div className="text-slate-400 text-sm">Не удалось получить метрики</div>
|
||||||
<button onClick={fetchMetrics} className="mt-4 text-indigo-400 text-sm hover:text-indigo-300 transition-colors">
|
<div className="text-slate-600 text-xs">node_exporter должен быть запущен на {info.ip}:9100</div>
|
||||||
|
<button onClick={fetchMetrics}
|
||||||
|
className="text-indigo-400 text-sm hover:text-indigo-300 transition-colors">
|
||||||
Попробовать снова
|
Попробовать снова
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Circular gauges */}
|
{/* Gauges */}
|
||||||
<div className="card card-violet p-6">
|
<div className="card p-6">
|
||||||
<div className="flex items-center gap-2 mb-6">
|
<div className="flex items-center gap-2 mb-6">
|
||||||
<div className="w-7 h-7 rounded-lg bg-violet-500/15 flex items-center justify-center">
|
<Activity className="w-4 h-4" style={{ color: "#818cf8" }} />
|
||||||
<Activity className="w-3.5 h-3.5 text-violet-400" />
|
<span className="text-sm font-medium text-slate-300">Загрузка системы</span>
|
||||||
</div>
|
|
||||||
<span className="text-sm font-semibold text-white">Загрузка системы</span>
|
|
||||||
</div>
|
</div>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex gap-12 justify-center animate-pulse">
|
<div className="flex justify-around animate-pulse">
|
||||||
{[1,2,3].map(i => <div key={i} className="w-24 h-28 bg-white/5 rounded-full" />)}
|
{[1,2,3].map(i => <div key={i} className="w-28 h-36 rounded-2xl bg-white/5" />)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-wrap gap-10 justify-around">
|
<div className="flex flex-wrap justify-around gap-6">
|
||||||
<CircularGauge value={metrics?.cpu ?? 0} label="CPU" color="#6366f1" />
|
<GaugeRing
|
||||||
<CircularGauge value={metrics?.ram?.percent ?? 0} label="RAM" color="#8b5cf6" />
|
value={metrics?.cpu ?? 0}
|
||||||
<CircularGauge value={metrics?.disk?.percent ?? 0} label="Disk" color="#10b981" />
|
color="#6366f1"
|
||||||
|
label="CPU"
|
||||||
|
sub={metrics?.load1 != null ? `Load: ${metrics.load1.toFixed(2)}` : undefined}
|
||||||
|
/>
|
||||||
|
<GaugeRing
|
||||||
|
value={metrics?.ram?.percent ?? 0}
|
||||||
|
color="#8b5cf6"
|
||||||
|
label="RAM"
|
||||||
|
sub={metrics?.ram ? `${metrics.ram.used} / ${metrics.ram.total} GB` : undefined}
|
||||||
|
/>
|
||||||
|
<GaugeRing
|
||||||
|
value={metrics?.disk?.percent ?? 0}
|
||||||
|
color="#10b981"
|
||||||
|
label="Disk"
|
||||||
|
sub={metrics?.disk ? `${metrics.disk.used} / ${metrics.disk.total} GB` : undefined}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats grid */}
|
{/* Stat cards */}
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
{[
|
<StatCard
|
||||||
{ icon: Cpu, label: "CPU", value: loading ? "..." : `${metrics?.cpu ?? "—"}%`, sub: loading ? "" : `Load: ${metrics?.load1?.toFixed(2) ?? "—"} · ${metrics?.cpuCount ?? "?"} cores`, color: "text-indigo-400", accent: "card-blue" },
|
icon={Cpu} label="CPU Load"
|
||||||
{ icon: MemoryStick, label: "RAM", value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`, color: "text-violet-400", accent: "card-violet" },
|
value={loading ? "..." : `${metrics?.cpu ?? "—"}%`}
|
||||||
{ icon: HardDrive, label: "Disk", value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`, color: "text-emerald-400", accent: "card-emerald" },
|
sub={loading ? "" : `${metrics?.cpuCount ?? "?"} ядер`}
|
||||||
{ icon: Clock, label: "Uptime", value: loading ? "..." : (metrics?.uptime ?? "—"), sub: "Время работы", color: "text-amber-400", accent: "card-amber" },
|
color="#6366f1"
|
||||||
].map((item) => (
|
/>
|
||||||
<div key={item.label} className={`card ${item.accent} p-5`}>
|
<StatCard
|
||||||
<div className="flex items-center gap-2 mb-3">
|
icon={Activity} label="RAM"
|
||||||
<item.icon className={`w-4 h-4 ${item.color}`} />
|
value={loading ? "..." : metrics?.ram ? `${metrics.ram.used} GB` : "—"}
|
||||||
<span className="text-xs font-semibold text-slate-500 uppercase tracking-wider">{item.label}</span>
|
sub={loading ? "" : metrics?.ram ? `из ${metrics.ram.total} GB` : ""}
|
||||||
</div>
|
color="#8b5cf6"
|
||||||
<div className="text-2xl font-bold text-white">{item.value}</div>
|
/>
|
||||||
{item.sub && <div className="text-xs text-slate-600 mt-1">{item.sub}</div>}
|
<StatCard
|
||||||
</div>
|
icon={HardDrive} label="Disk"
|
||||||
))}
|
value={loading ? "..." : metrics?.disk ? `${metrics.disk.used} GB` : "—"}
|
||||||
|
sub={loading ? "" : metrics?.disk ? `из ${metrics.disk.total} GB` : ""}
|
||||||
|
color="#10b981"
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
icon={Clock} label="Uptime"
|
||||||
|
value={loading ? "..." : metrics?.uptime ?? "—"}
|
||||||
|
color="#f59e0b"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Usage bars */}
|
{/* Usage bars */}
|
||||||
{!loading && metrics && (
|
{!loading && metrics && (
|
||||||
<div className="card p-5 space-y-5">
|
<div className="card p-5 space-y-4">
|
||||||
<h3 className="text-sm font-semibold text-white">Использование ресурсов</h3>
|
<h3 className="text-sm font-medium text-slate-300">Детали использования</h3>
|
||||||
<UsageBar label="CPU" value={metrics.cpu ?? 0} color="#6366f1" detail={`${metrics.cpu ?? "—"}%`} />
|
|
||||||
{metrics.ram && (
|
{metrics.ram && (
|
||||||
<UsageBar label="RAM" value={metrics.ram.percent} color="#8b5cf6" detail={`${metrics.ram.used} / ${metrics.ram.total} GB`} />
|
<MetricBar label="RAM" value={metrics.ram.used} max={metrics.ram.total} unit="GB" color="#8b5cf6" />
|
||||||
)}
|
)}
|
||||||
{metrics.disk && (
|
{metrics.disk && (
|
||||||
<UsageBar label="Disk" value={metrics.disk.percent} color="#10b981" detail={`${metrics.disk.used} / ${metrics.disk.total} GB`} />
|
<MetricBar label="Disk" value={metrics.disk.used} max={metrics.disk.total} unit="GB" color="#10b981" />
|
||||||
|
)}
|
||||||
|
{metrics.cpu != null && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<div className="flex justify-between text-xs">
|
||||||
|
<span className="text-slate-400">CPU</span>
|
||||||
|
<span className="text-slate-200 font-medium">{metrics.cpu}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 rounded-full" style={{ background: "rgba(255,255,255,0.05)" }}>
|
||||||
|
<div className="h-full rounded-full transition-all duration-700"
|
||||||
|
style={{ width: `${metrics.cpu}%`, background: "#6366f1", boxShadow: "0 0 8px #6366f160" }} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Network */}
|
{/* Network */}
|
||||||
{!loading && metrics?.network && (
|
{!loading && metrics?.network && (
|
||||||
<div className="card card-cyan p-5">
|
<div className="card p-5" style={{ borderTop: "2px solid #06b6d4" }}>
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<div className="w-7 h-7 rounded-lg bg-cyan-500/15 flex items-center justify-center">
|
<Wifi className="w-4 h-4" style={{ color: "#06b6d4" }} />
|
||||||
<Wifi className="w-3.5 h-3.5 text-cyan-400" />
|
<span className="text-sm font-medium text-slate-300">Сеть (всего с момента запуска)</span>
|
||||||
</div>
|
|
||||||
<span className="text-sm font-semibold text-white">Сеть (всего)</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="rounded-xl p-3 border border-white/5" style={{ background: "rgba(6,182,212,0.05)" }}>
|
<div className="p-3 rounded-xl" style={{ background: "rgba(6,182,212,0.08)" }}>
|
||||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">↓ Получено</div>
|
<div className="text-xs text-slate-500 mb-1">↓ Получено</div>
|
||||||
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.rxBytes)}</div>
|
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.rxBytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl p-3 border border-white/5" style={{ background: "rgba(6,182,212,0.05)" }}>
|
<div className="p-3 rounded-xl" style={{ background: "rgba(6,182,212,0.08)" }}>
|
||||||
<div className="text-[10px] text-slate-500 mb-1 uppercase tracking-wider">↑ Отправлено</div>
|
<div className="text-xs text-slate-500 mb-1">↑ Отправлено</div>
|
||||||
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.txBytes)}</div>
|
<div className="text-xl font-bold text-white">{formatBytes(metrics.network.txBytes)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user