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>
|
</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 */}
|
{/* Semantic Search */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
|||||||
@@ -1,33 +1,53 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
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() {
|
export default function UnifiWidget() {
|
||||||
const [devices, setDevices] = useState<any[]>([]);
|
const [devices, setDevices] = useState<any[]>([]);
|
||||||
|
const [health, setHealth] = useState<Health | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDevices();
|
fetchData();
|
||||||
const interval = setInterval(fetchDevices, 30000);
|
const interval = setInterval(fetchData, 30000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchDevices = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/unifi");
|
const response = await fetch("/api/unifi");
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
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 : [];
|
const devArr = Array.isArray(data) ? data : Array.isArray(data?.devices) ? data.devices : [];
|
||||||
setDevices(devArr);
|
setDevices(devArr);
|
||||||
|
setHealth(data?.health || null);
|
||||||
setError(false);
|
setError(false);
|
||||||
} else {
|
} else {
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (err) {
|
} catch {
|
||||||
setError(true);
|
setError(true);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -43,6 +63,9 @@ export default function UnifiWidget() {
|
|||||||
<Wifi className="w-5 h-5 text-blue-500" />
|
<Wifi className="w-5 h-5 text-blue-500" />
|
||||||
UniFi Network
|
UniFi Network
|
||||||
</h3>
|
</h3>
|
||||||
|
{health?.wan && (
|
||||||
|
<span className="text-xs text-gray-500">{health.wan.isp}</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -52,24 +75,70 @@ export default function UnifiWidget() {
|
|||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<WifiOff className="w-12 h-12 text-gray-600 mx-auto mb-2" />
|
<WifiOff className="w-12 h-12 text-gray-600 mx-auto mb-2" />
|
||||||
<p className="text-sm text-gray-400">
|
<p className="text-sm text-gray-400">Configure UniFi credentials in .env</p>
|
||||||
Configure UniFi credentials in .env
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* Top stats */}
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<div className="bg-gray-900/50 rounded-lg p-3">
|
<div className="bg-gray-900/50 rounded-lg p-3">
|
||||||
<p className="text-xs text-gray-400">Devices Online</p>
|
<p className="text-xs text-gray-400">Devices Online</p>
|
||||||
<p className="text-2xl font-bold text-green-500">
|
<p className="text-2xl font-bold text-green-500">{onlineDevices}/{devices.length}</p>
|
||||||
{onlineDevices}/{devices.length}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-gray-900/50 rounded-lg p-3">
|
<div className="bg-gray-900/50 rounded-lg p-3">
|
||||||
<p className="text-xs text-gray-400">Connected Clients</p>
|
<p className="text-xs text-gray-400">Connected Clients</p>
|
||||||
<p className="text-2xl font-bold text-blue-500">{totalClients}</p>
|
<p className="text-2xl font-bold text-blue-500">{totalClients}</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user