fix: UniFi, Synology, Network widgets + Grafana HTTPS

- Rewrite UniFi API: port 443, /api/auth/login, /proxy/network/ prefix,
  native https module for self-signed cert, cookie-based session
- Update credentials to Vault-Admin (view-only local account)
- Rewrite NetworkWidget to show UniFi devices with clients/uptime
- Fix Synology API: correct field mappings (size.total/size.used),
  add CPU/memory utilization endpoint
- Fix network API: use prometheus:9090 container DNS
- Add NODE_TLS_REJECT_UNAUTHORIZED=0 for UniFi self-signed cert
- Expand AI container group (rag, litellm, qdrant)
- Add force-dynamic to API routes
This commit is contained in:
2026-02-13 13:54:48 -05:00
parent e00c6efcda
commit 1102f27f45
8 changed files with 635 additions and 255 deletions

View File

@@ -1,11 +1,12 @@
import { NextResponse } from "next/server";
import axios from "axios";
const SYNOLOGY_HOST = process.env.SYNOLOGY_HOST;
const SYNOLOGY_PORT = process.env.SYNOLOGY_PORT || "5001";
const SYNOLOGY_PORT = process.env.SYNOLOGY_PORT || "5000";
const SYNOLOGY_USERNAME = process.env.SYNOLOGY_USERNAME;
const SYNOLOGY_PASSWORD = process.env.SYNOLOGY_PASSWORD;
export const dynamic = "force-dynamic";
export async function GET() {
if (!SYNOLOGY_HOST || !SYNOLOGY_USERNAME || !SYNOLOGY_PASSWORD) {
return NextResponse.json(
@@ -15,56 +16,92 @@ export async function GET() {
}
try {
const protocol = SYNOLOGY_PORT === "5000" ? "http" : "https";
const baseUrl = `${protocol}://${SYNOLOGY_HOST}:${SYNOLOGY_PORT}`;
const protocol = SYNOLOGY_PORT === "5001" ? "https" : "http";
const baseUrl = `${protocol}://${SYNOLOGY_HOST}:${SYNOLOGY_PORT}/webapi`;
// Login to Synology
const loginResponse = await axios.get(`${baseUrl}/webapi/auth.cgi`, {
params: {
api: "SYNO.API.Auth",
version: 3,
method: "login",
account: SYNOLOGY_USERNAME,
passwd: SYNOLOGY_PASSWORD,
session: "FileStation",
format: "sid",
},
httpsAgent: new (require("https").Agent)({
rejectUnauthorized: false,
}),
});
// Login
const loginUrl = `${baseUrl}/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${encodeURIComponent(
SYNOLOGY_USERNAME
)}&passwd=${encodeURIComponent(SYNOLOGY_PASSWORD)}&session=FileStation&format=sid`;
const sid = loginResponse.data.data.sid;
const loginResp = await fetch(loginUrl, { cache: "no-store" });
const loginData = await loginResp.json();
if (!loginData.success) {
console.error("Synology login failed:", JSON.stringify(loginData));
return NextResponse.json(
{ error: "Synology login failed" },
{ status: 401 }
);
}
const sid = loginData.data.sid;
// Get storage info
const storageResponse = await axios.get(`${baseUrl}/webapi/entry.cgi`, {
params: {
api: "SYNO.Storage.CGI.Storage",
version: 1,
method: "load_info",
_sid: sid,
},
httpsAgent: new (require("https").Agent)({
rejectUnauthorized: false,
}),
const storageResp = await fetch(
`${baseUrl}/entry.cgi?api=SYNO.Storage.CGI.Storage&version=1&method=load_info&_sid=${sid}`,
{ cache: "no-store" }
);
const storageData = await storageResp.json();
// Get system utilization
let utilization = null;
try {
const utilResp = await fetch(
`${baseUrl}/entry.cgi?api=SYNO.Core.System.Utilization&version=1&method=get&_sid=${sid}`,
{ cache: "no-store" }
);
const utilData = await utilResp.json();
if (utilData.success) {
utilization = {
cpu: utilData.data?.cpu?.user_load ?? null,
memory: utilData.data?.memory?.real_usage ?? null,
};
}
} catch (e) {
console.error("Synology utilization error:", e);
}
// Parse volumes - Synology returns size.total / size.used as string byte counts
const volumes = (storageData.data?.volumes || []).map((vol: any) => {
const total = parseInt(vol.size?.total || "0", 10);
const used = parseInt(vol.size?.used || "0", 10);
const free = total - used;
return {
volume: vol.vol_path || vol.id || "unknown",
id: vol.id || vol.vol_path || "unknown",
size: total,
used: used,
available: free,
percentUsed: total > 0 ? ((used / total) * 100).toFixed(1) : "0",
status: vol.status || "unknown",
fsType: vol.fs_type || "unknown",
};
});
const volumes = storageResponse.data.data.volumes.map((vol: any) => ({
volume: vol.volume_path,
size: vol.size_total_byte,
used: vol.size_used_byte,
available: vol.size_free_byte,
percentUsed: ((vol.size_used_byte / vol.size_total_byte) * 100).toFixed(
2
),
// Parse disks
const disks = (storageData.data?.disks || []).map((disk: any) => ({
name: disk.longName || disk.name || disk.id || "Unknown",
model: disk.model || "Unknown",
serial: disk.serial || "",
sizeBytes: parseInt(disk.size_total || "0", 10),
status: disk.smart_status || disk.overview_status || "unknown",
isSsd: disk.isSsd || false,
temp: disk.temp || null,
}));
return NextResponse.json(volumes);
} catch (error) {
console.error("Synology API error:", error);
// Logout (non-blocking)
fetch(
`${baseUrl}/auth.cgi?api=SYNO.API.Auth&version=3&method=logout&session=FileStation&_sid=${sid}`,
{ cache: "no-store" }
).catch(() => {});
return NextResponse.json({ volumes, disks, utilization });
} catch (error: any) {
console.error("Synology API error:", error?.message || error);
return NextResponse.json(
{ error: "Failed to fetch Synology storage" },
{ status: 500 }
);
}
}
}