mirror of
https://github.com/mblanke/Dashboard.git
synced 2026-03-01 12:10:20 -05:00
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:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user