File size: 5,432 Bytes
9e4583c | 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 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | "use client";
/**
* SystemMonitor — Real-time system metrics widget
*
* Displays CPU/memory/uptime and token throughput metrics.
* Fetches from /api/monitoring/health periodically.
*
* @module shared/components/SystemMonitor
*/
import { useState, useEffect, useRef } from "react";
import Card from "./Card";
const REFRESH_INTERVAL = 10000; // 10s
function formatUptime(seconds) {
if (!seconds) return "N/A";
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
if (d > 0) return `${d}d ${h}h`;
if (h > 0) return `${h}h ${m}m`;
return `${m}m`;
}
function formatBytes(bytes) {
if (!bytes) return "0 B";
const units = ["B", "KB", "MB", "GB"];
let i = 0;
let val = bytes;
while (val >= 1024 && i < units.length - 1) {
val /= 1024;
i++;
}
return `${val.toFixed(1)} ${units[i]}`;
}
function MetricRow({ icon, label, value, color = "text-text-main" }) {
return (
<div className="flex items-center justify-between py-1.5">
<div className="flex items-center gap-2 text-text-muted">
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
{icon}
</span>
<span className="text-sm">{label}</span>
</div>
<span className={`text-sm font-mono font-medium ${color}`}>{value}</span>
</div>
);
}
export default function SystemMonitor({ compact = false }) {
const [metrics, setMetrics] = useState(null);
const [error, setError] = useState(false);
const mountedRef = useRef(true);
useEffect(() => {
mountedRef.current = true;
async function poll() {
try {
const res = await fetch("/api/monitoring/health");
if (!mountedRef.current) return;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (!mountedRef.current) return;
setMetrics(data);
setError(false);
} catch {
if (mountedRef.current) setError(true);
}
}
poll();
const interval = setInterval(poll, REFRESH_INTERVAL);
return () => {
mountedRef.current = false;
clearInterval(interval);
};
}, []);
if (error && !metrics) {
return (
<Card className="p-4">
<div className="flex items-center gap-2 text-text-muted text-sm">
<span className="material-symbols-outlined text-[18px] text-red-400">error</span>
<span>Unable to load system metrics</span>
</div>
</Card>
);
}
if (!metrics) {
return (
<Card className="p-4 animate-pulse">
<div className="h-24 bg-surface rounded-lg" />
</Card>
);
}
const memUsed = metrics.memory?.heapUsed || metrics.memoryUsage?.heapUsed || 0;
const memTotal = metrics.memory?.heapTotal || metrics.memoryUsage?.heapTotal || 1;
const memPercent = Math.round((memUsed / memTotal) * 100);
const memColor =
memPercent > 80 ? "text-red-400" : memPercent > 60 ? "text-amber-400" : "text-green-400";
if (compact) {
return (
<div
className="flex items-center gap-4 text-xs text-text-muted"
role="status"
aria-label="System metrics"
>
<span title={`Memory: ${memPercent}%`}>
<span className={`font-mono font-medium ${memColor}`}>{memPercent}%</span> mem
</span>
<span title={`Uptime: ${formatUptime(metrics.uptime)}`}>
⏱ {formatUptime(metrics.uptime)}
</span>
</div>
);
}
return (
<Card className="p-4" role="region" aria-label="System monitoring">
<h3 className="text-sm font-semibold text-text-main mb-3 flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-primary" aria-hidden="true">
monitoring
</span>
System Monitor
</h3>
<div className="space-y-0.5">
<MetricRow icon="timer" label="Uptime" value={formatUptime(metrics.uptime)} />
<MetricRow
icon="memory"
label="Memory"
value={`${formatBytes(memUsed)} / ${formatBytes(memTotal)} (${memPercent}%)`}
color={memColor}
/>
{metrics.version && (
<MetricRow icon="info" label="Version" value={metrics.version} color="text-primary" />
)}
{metrics.activeConnections !== undefined && (
<MetricRow icon="lan" label="Connections" value={String(metrics.activeConnections)} />
)}
{metrics.circuitBreakers && (
<MetricRow
icon="health_and_safety"
label="Circuit Breakers"
value={`${metrics.circuitBreakers.open || 0} open`}
color={metrics.circuitBreakers.open > 0 ? "text-amber-400" : "text-green-400"}
/>
)}
</div>
{/* Memory bar */}
<div className="mt-3">
<div className="h-1.5 bg-surface rounded-full overflow-hidden">
<div
className={`h-full rounded-full transition-all duration-500 ${
memPercent > 80 ? "bg-red-400" : memPercent > 60 ? "bg-amber-400" : "bg-green-400"
}`}
style={{ width: `${memPercent}%` }}
role="progressbar"
aria-valuenow={memPercent}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Memory usage: ${memPercent}%`}
/>
</div>
</div>
</Card>
);
}
|