Files
Dashboard/src/app/api/servers/route.ts
mblanke b14489ff59 feat: add server stats, GPU stats, container CPU/memory display
- Add /api/servers endpoint querying Prometheus for CPU, RAM, disk, uptime, load
- Add /api/gpu endpoint for NVIDIA Jetson GPU utilization, temp, power
- Add ServerStatsWidget with animated bars for Atlas, Wile, RoadRunner
- Add GPUStatsWidget with GPU util, memory, temp color-coding, power draw
- Update ContainerGroup to show CPU bar and memory for running containers
- Fix docker-compose.yml: traefik network external: true
- Fix getTraefikUrl to scan all router labels (not just 'https')
2026-02-13 13:08:39 -05:00

94 lines
3.3 KiB
TypeScript

import { NextResponse } from "next/server";
const PROMETHEUS_URL = "http://prometheus:9090";
const INSTANCE_MAP: Record<string, { name: string; role: string }> = {
"192.168.1.21": { name: "Atlas", role: "Control Node" },
"192.168.1.50": { name: "Wile", role: "GPU Node - Heavy" },
"192.168.1.51": { name: "RoadRunner", role: "GPU Node - Fast" },
};
async function queryPrometheus(query: string): Promise<any[]> {
try {
const url = `${PROMETHEUS_URL}/api/v1/query?query=${encodeURIComponent(query)}`;
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) return [];
const json = await res.json();
if (json.status === "success" && json.data?.result) {
return json.data.result;
}
return [];
} catch {
return [];
}
}
function extractByInstance(results: any[]): Record<string, number> {
const map: Record<string, number> = {};
for (const r of results) {
const instance: string = r.metric?.instance || "";
const ip = instance.replace(/:\d+$/, "");
const val = parseFloat(r.value?.[1] || "0");
if (!isNaN(val)) {
map[ip] = val;
}
}
return map;
}
export async function GET() {
try {
const [cpuRes, memPercentRes, memTotalRes, memAvailRes, diskRes, uptimeBootRes, uptimeNowRes, loadRes] =
await Promise.all([
queryPrometheus(
'100 - (avg by(instance) (rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100)'
),
queryPrometheus(
"(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100"
),
queryPrometheus("node_memory_MemTotal_bytes"),
queryPrometheus("node_memory_MemAvailable_bytes"),
queryPrometheus(
'100 - (node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} * 100)'
),
queryPrometheus("node_boot_time_seconds"),
queryPrometheus("node_time_seconds"),
queryPrometheus("node_load1"),
]);
const cpuMap = extractByInstance(cpuRes);
const memPercentMap = extractByInstance(memPercentRes);
const memTotalMap = extractByInstance(memTotalRes);
const memAvailMap = extractByInstance(memAvailRes);
const diskMap = extractByInstance(diskRes);
const bootMap = extractByInstance(uptimeBootRes);
const nowMap = extractByInstance(uptimeNowRes);
const loadMap = extractByInstance(loadRes);
const servers = Object.entries(INSTANCE_MAP).map(([ip, info]) => {
const memTotalBytes = memTotalMap[ip] || 0;
const memAvailBytes = memAvailMap[ip] || 0;
const memUsedBytes = memTotalBytes - memAvailBytes;
const uptimeSeconds = (nowMap[ip] || 0) - (bootMap[ip] || 0);
return {
name: info.name,
role: info.role,
ip,
cpu: parseFloat((cpuMap[ip] || 0).toFixed(1)),
memoryPercent: parseFloat((memPercentMap[ip] || 0).toFixed(1)),
memoryUsedGB: parseFloat((memUsedBytes / 1073741824).toFixed(1)),
memoryTotalGB: parseFloat((memTotalBytes / 1073741824).toFixed(1)),
diskPercent: parseFloat((diskMap[ip] || 0).toFixed(1)),
uptimeSeconds: Math.floor(uptimeSeconds > 0 ? uptimeSeconds : 0),
load1: parseFloat((loadMap[ip] || 0).toFixed(2)),
};
});
return NextResponse.json(servers);
} catch (error) {
console.error("Servers API error:", error);
return NextResponse.json([]);
}
}