diff --git a/src/app/(dashboard)/system/page.tsx b/src/app/(dashboard)/system/page.tsx index 21f174d..e47f8ea 100644 --- a/src/app/(dashboard)/system/page.tsx +++ b/src/app/(dashboard)/system/page.tsx @@ -2,16 +2,21 @@ export const dynamic = "force-dynamic"; 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; type TabName = typeof TABS[number]; const MACHINE_MAP: Record = { - "Openclaw": "ocplatform", + "Openclaw": "openclaw", "Сервисы": "services", }; +const MACHINE_INFO: Record = { + "Openclaw": { ip: "192.168.31.103", os: "Ubuntu 22.04" }, + "Сервисы": { ip: "192.168.31.60", os: "Ubuntu 22.04" }, +}; + interface Metrics { cpu: number | null; load1: number | null; @@ -22,57 +27,95 @@ interface Metrics { network: { rxBytes: number | null; txBytes: number | null }; } -function CircularGauge({ value, size = 96, color, label }: { value: number; size?: number; color: string; label: string }) { - const radius = (size - 14) / 2; - const circumference = 2 * Math.PI * radius; - const offset = circumference - (value / 100) * circumference; +function formatBytes(bytes: number | null): string { + if (!bytes) return "—"; + const gb = bytes / 1024 / 1024 / 1024; + 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 (
- - - - - - - - + + -
- {value}% +
+ {value}%
- {label} +
+
{label}
+ {sub &&
{sub}
} +
); } -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 ( -
+
- {label} - {detail || `${value}%`} + {label} + {value.toFixed(1)} / {max.toFixed(1)} {unit}
-
+
); } +function StatCard({ icon: Icon, label, value, sub, color }: { + icon: React.ElementType; label: string; value: string; sub?: string; color: string; +}) { + return ( +
+
+ +
+
+
{label}
+
{value}
+ {sub &&
{sub}
} +
+
+ ); +} + export default function SystemPage() { const [activeTab, setActiveTab] = useState("Openclaw"); const [metrics, setMetrics] = useState(null); @@ -84,46 +127,46 @@ export default function SystemPage() { setLoading(true); setError(false); 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(); const data = await res.json(); setMetrics(data); setLastUpdated(new Date()); } catch { setError(true); + setMetrics(null); } finally { setLoading(false); } }, [activeTab]); useEffect(() => { + setMetrics(null); + setError(false); fetchMetrics(); const interval = setInterval(fetchMetrics, 30000); return () => clearInterval(interval); }, [fetchMetrics]); - const formatBytes = (bytes: number | null) => { - 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`; - }; + const info = MACHINE_INFO[activeTab]; return ( -
+
{/* Header */} -
+

System Monitor

-

- {lastUpdated ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` : "Загрузка..."} +

+ {lastUpdated + ? `Обновлено: ${lastUpdated.toLocaleTimeString("ru-RU")}` + : "Загрузка..."}

{/* Tabs */} -
- {TABS.map((tab) => ( +
+ {TABS.map(tab => ( ))}
+ {/* Machine info badge */} + {!loading && !error && ( +
+
+ + Online +
+ · + {info.ip} + · + {info.os} + {metrics?.cpuCount ? <>·{metrics.cpuCount} CPU cores : null} +
+ )} + {error ? ( -
-
⚠️ Не удалось получить метрики
-
Убедитесь что node_exporter запущен на хосте
-
) : ( <> - {/* Circular gauges */} -
+ {/* Gauges */} +
-
- -
- Загрузка системы + + Загрузка системы
{loading ? ( -
- {[1,2,3].map(i =>
)} +
+ {[1,2,3].map(i =>
)}
) : ( -
- - - +
+ + +
)}
- {/* Stats grid */} + {/* Stat cards */}
- {[ - { 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: MemoryStick, label: "RAM", value: loading ? "..." : `${metrics?.ram?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.ram?.total ?? "?"} GB`, color: "text-violet-400", accent: "card-violet" }, - { icon: HardDrive, label: "Disk", value: loading ? "..." : `${metrics?.disk?.used ?? "—"} GB`, sub: loading ? "" : `из ${metrics?.disk?.total ?? "?"} GB`, color: "text-emerald-400", accent: "card-emerald" }, - { icon: Clock, label: "Uptime", value: loading ? "..." : (metrics?.uptime ?? "—"), sub: "Время работы", color: "text-amber-400", accent: "card-amber" }, - ].map((item) => ( -
-
- - {item.label} -
-
{item.value}
- {item.sub &&
{item.sub}
} -
- ))} + + + +
{/* Usage bars */} {!loading && metrics && ( -
-

Использование ресурсов

- +
+

Детали использования

{metrics.ram && ( - + )} {metrics.disk && ( - + + )} + {metrics.cpu != null && ( +
+
+ CPU + {metrics.cpu}% +
+
+
+
+
)}
)} {/* Network */} {!loading && metrics?.network && ( -
+
-
- -
- Сеть (всего) + + Сеть (всего с момента запуска)
-
-
↓ Получено
+
+
↓ Получено
{formatBytes(metrics.network.rxBytes)}
-
-
↑ Отправлено
+
+
↑ Отправлено
{formatBytes(metrics.network.txBytes)}