export const dynamic = "force-dynamic"; "use client"; import { useState, useEffect, useCallback } from "react"; import { Cpu, MemoryStick, HardDrive, Clock, RefreshCw, Wifi, Activity } from "lucide-react"; const TABS = ["Openclaw", "Сервисы"] as const; type TabName = typeof TABS[number]; const MACHINE_MAP: Record = { "Openclaw": "ocplatform", "Сервисы": "services", }; interface Metrics { cpu: number | null; load1: number | null; cpuCount: number; ram: { used: number; total: number; percent: number } | null; disk: { used: number; total: number; percent: number } | null; uptime: string | null; 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; return (
{value}%
{label}
); } function UsageBar({ label, value, color, detail }: { label: string; value: number; color: string; detail?: string }) { return (
{label} {detail || `${value}%`}
); } export default function SystemPage() { const [activeTab, setActiveTab] = useState("Openclaw"); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(false); const [lastUpdated, setLastUpdated] = useState(null); const fetchMetrics = useCallback(async () => { setLoading(true); setError(false); try { const res = await fetch(`/api/metrics?machine=${MACHINE_MAP[activeTab]}`); if (!res.ok) throw new Error(); const data = await res.json(); setMetrics(data); setLastUpdated(new Date()); } catch { setError(true); } finally { setLoading(false); } }, [activeTab]); useEffect(() => { 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`; }; return (
{/* Header */}

System Monitor

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

{/* Tabs */}
{TABS.map((tab) => ( ))}
{error ? (
⚠️ Не удалось получить метрики
Убедитесь что node_exporter запущен на хосте
) : ( <> {/* Circular gauges */}
Загрузка системы
{loading ? (
{[1,2,3].map(i =>
)}
) : (
)}
{/* Stats grid */}
{[ { 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 && ( )}
)} {/* Network */} {!loading && metrics?.network && (
Сеть (всего)
↓ Получено
{formatBytes(metrics.network.rxBytes)}
↑ Отправлено
{formatBytes(metrics.network.txBytes)}
)} )}
); }