| "use client"; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { useState, useEffect, useRef } from "react"; |
| import Card from "./Card"; |
|
|
| const REFRESH_INTERVAL = 10000; |
|
|
| 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> |
| ); |
| } |
|
|