feat: GB10 GPU panels + enhanced UniFi widget with health data

- Added GB10 Fabric row: GPU Utilization A/B + Network Throughput
- UniFi widget now shows WAN/WLAN/LAN/Internet health subsystems
- Displays ISP name, bandwidth rates, latency, client counts per subsystem
- Status dots (green/yellow) for each subsystem
This commit is contained in:
2026-02-17 09:16:30 -05:00
parent df1f0700b0
commit ed125f613d
2 changed files with 102 additions and 14 deletions

View File

@@ -204,6 +204,25 @@ export default function Home() {
/>
</div>
{/* GB10 Fabric Overview */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<GrafanaWidget
title="GPU Utilization - GB10-A"
dashboardUid="56d02d73-c5f5-4869-83fe-113483dc0e67"
panelId={11}
/>
<GrafanaWidget
title="GPU Utilization - GB10-B"
dashboardUid="56d02d73-c5f5-4869-83fe-113483dc0e67"
panelId={12}
/>
<GrafanaWidget
title="Network Throughput"
dashboardUid="56d02d73-c5f5-4869-83fe-113483dc0e67"
panelId={17}
/>
</div>
{/* Semantic Search */}
<div className="mb-8">

View File

@@ -1,33 +1,53 @@
"use client";
import { useEffect, useState } from "react";
import { Wifi, WifiOff } from "lucide-react";
import { Wifi, WifiOff, Globe, Radio, Network, ArrowDown, ArrowUp } from "lucide-react";
interface Health {
wan?: { status: string; wanIp: string; isp: string; txRate: number; rxRate: number; gateways: number; clients: number };
www?: { status: string; latency: number; uptime: number; downSpeed: number; upSpeed: number };
wlan?: { status: string; adopted: number; clients: number; aps: number; txRate: number; rxRate: number };
lan?: { status: string; adopted: number; clients: number; switches: number; txRate: number; rxRate: number };
vpn?: { status: string };
}
function formatRate(bytesPerSec: number): string {
if (bytesPerSec >= 1_000_000) return (bytesPerSec / 1_000_000).toFixed(1) + " MB/s";
if (bytesPerSec >= 1_000) return (bytesPerSec / 1_000).toFixed(0) + " KB/s";
return bytesPerSec + " B/s";
}
function StatusDot({ status }: { status: string }) {
const color = status === "ok" ? "bg-green-400" : status === "unknown" ? "bg-gray-500" : "bg-yellow-400";
return <span className={`inline-block w-1.5 h-1.5 rounded-full ${color}`} />;
}
export default function UnifiWidget() {
const [devices, setDevices] = useState<any[]>([]);
const [health, setHealth] = useState<Health | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
useEffect(() => {
fetchDevices();
const interval = setInterval(fetchDevices, 30000);
fetchData();
const interval = setInterval(fetchData, 30000);
return () => clearInterval(interval);
}, []);
const fetchDevices = async () => {
const fetchData = async () => {
try {
const response = await fetch("/api/unifi");
if (response.ok) {
const data = await response.json();
// API returns { devices, health, totalClients } — extract the array
const devArr = Array.isArray(data) ? data : Array.isArray(data?.devices) ? data.devices : [];
setDevices(devArr);
setHealth(data?.health || null);
setError(false);
} else {
setError(true);
}
setLoading(false);
} catch (err) {
} catch {
setError(true);
setLoading(false);
}
@@ -43,6 +63,9 @@ export default function UnifiWidget() {
<Wifi className="w-5 h-5 text-blue-500" />
UniFi Network
</h3>
{health?.wan && (
<span className="text-xs text-gray-500">{health.wan.isp}</span>
)}
</div>
{loading ? (
@@ -52,24 +75,70 @@ export default function UnifiWidget() {
) : error ? (
<div className="text-center py-8">
<WifiOff className="w-12 h-12 text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-400">
Configure UniFi credentials in .env
</p>
<p className="text-sm text-gray-400">Configure UniFi credentials in .env</p>
</div>
) : (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-3">
{/* Top stats */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-gray-900/50 rounded-lg p-3">
<p className="text-xs text-gray-400">Devices Online</p>
<p className="text-2xl font-bold text-green-500">
{onlineDevices}/{devices.length}
</p>
<p className="text-2xl font-bold text-green-500">{onlineDevices}/{devices.length}</p>
</div>
<div className="bg-gray-900/50 rounded-lg p-3">
<p className="text-xs text-gray-400">Connected Clients</p>
<p className="text-2xl font-bold text-blue-500">{totalClients}</p>
</div>
</div>
{/* Subsystem health */}
{health && (
<div className="pt-2 border-t border-gray-700/50 space-y-1.5">
{health.wan && (
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1.5 text-gray-300">
<Globe className="w-3 h-3" />
<StatusDot status={health.wan.status} />
WAN
</span>
<span className="flex items-center gap-2 text-gray-500">
<span className="flex items-center gap-0.5"><ArrowDown className="w-2.5 h-2.5 text-green-500" />{formatRate(health.wan.rxRate)}</span>
<span className="flex items-center gap-0.5"><ArrowUp className="w-2.5 h-2.5 text-blue-500" />{formatRate(health.wan.txRate)}</span>
</span>
</div>
)}
{health.wlan && (
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1.5 text-gray-300">
<Radio className="w-3 h-3" />
<StatusDot status={health.wlan.status} />
WLAN
</span>
<span className="text-gray-500">{health.wlan.clients} clients · {health.wlan.aps} APs</span>
</div>
)}
{health.lan && (
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1.5 text-gray-300">
<Network className="w-3 h-3" />
<StatusDot status={health.lan.status} />
LAN
</span>
<span className="text-gray-500">{health.lan.clients} clients · {health.lan.switches} switches</span>
</div>
)}
{health.www && (
<div className="flex items-center justify-between text-xs">
<span className="flex items-center gap-1.5 text-gray-300">
<Globe className="w-3 h-3" />
<StatusDot status={health.www.status} />
Internet
</span>
<span className="text-gray-500">{health.www.latency}ms latency</span>
</div>
)}
</div>
)}
</div>
)}
</div>