mirror of
https://github.com/mblanke/Dashboard.git
synced 2026-03-01 12:10:20 -05:00
fix: Docker socket support, Synology port default, defensive container fetch
- containers/route.ts: Use Unix socket (/var/run/docker.sock) via axios socketPath instead of HTTP to localhost:2375 (TCP API not exposed) - synology/route.ts: Default port to 5001 (HTTPS) and auto-detect protocol based on port (5000=HTTP, 5001+=HTTPS) - page.tsx: Defensive check on container API response shape before setting state
This commit is contained in:
@@ -1,63 +1,122 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import Docker from "dockerode";
|
import axios from "axios";
|
||||||
|
import http from "http";
|
||||||
|
|
||||||
const docker = new Docker({ socketPath: "/var/run/docker.sock" });
|
// Support both Unix socket and TCP
|
||||||
|
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || "/var/run/docker.sock";
|
||||||
|
const DOCKER_HOST = process.env.DOCKER_HOST;
|
||||||
|
|
||||||
|
function getAxiosConfig(path: string, params?: Record<string, any>) {
|
||||||
|
if (DOCKER_HOST) {
|
||||||
|
// TCP mode: DOCKER_HOST is set (e.g., http://localhost:2375)
|
||||||
|
return { url: `${DOCKER_HOST}${path}`, params };
|
||||||
|
}
|
||||||
|
// Unix socket mode (default)
|
||||||
|
return {
|
||||||
|
url: `http://localhost${path}`,
|
||||||
|
params,
|
||||||
|
socketPath: DOCKER_SOCKET,
|
||||||
|
httpAgent: new http.Agent({ socketPath: DOCKER_SOCKET } as any),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const containers = await docker.listContainers({ all: true });
|
const config = getAxiosConfig("/containers/json", { all: true });
|
||||||
|
const response = await axios.get(config.url, {
|
||||||
|
params: config.params,
|
||||||
|
socketPath: (config as any).socketPath,
|
||||||
|
httpAgent: (config as any).httpAgent,
|
||||||
|
});
|
||||||
|
|
||||||
const enriched = await Promise.all(
|
// Enhanced container data with stats
|
||||||
containers.map(async (c: any) => {
|
const containers = await Promise.all(
|
||||||
let statsText = "";
|
response.data.map(async (container: any) => {
|
||||||
let cpu = "0%";
|
let stats = { cpu: "0%", memory: "0 MB" };
|
||||||
let memory = "0 MB";
|
|
||||||
|
|
||||||
if (c.State === "running") {
|
if (container.State === "running") {
|
||||||
try {
|
try {
|
||||||
const container = docker.getContainer(c.Id);
|
const statsConfig = getAxiosConfig(
|
||||||
const stats = await container.stats({ stream: false });
|
`/containers/${container.Id}/stats`,
|
||||||
|
{ stream: false }
|
||||||
|
);
|
||||||
|
const statsResponse = await axios.get(statsConfig.url, {
|
||||||
|
params: statsConfig.params,
|
||||||
|
socketPath: (statsConfig as any).socketPath,
|
||||||
|
httpAgent: (statsConfig as any).httpAgent,
|
||||||
|
});
|
||||||
|
const data = statsResponse.data;
|
||||||
|
|
||||||
const cpuDelta =
|
const cpuDelta =
|
||||||
stats.cpu_stats?.cpu_usage?.total_usage -
|
data.cpu_stats.cpu_usage.total_usage -
|
||||||
(stats.precpu_stats?.cpu_usage?.total_usage || 0);
|
(data.precpu_stats?.cpu_usage?.total_usage || 0);
|
||||||
const systemDelta =
|
const systemDelta =
|
||||||
stats.cpu_stats?.system_cpu_usage -
|
data.cpu_stats.system_cpu_usage -
|
||||||
(stats.precpu_stats?.system_cpu_usage || 0);
|
(data.precpu_stats?.system_cpu_usage || 0);
|
||||||
const online = stats.cpu_stats?.online_cpus || 1;
|
const cpuPercent =
|
||||||
const cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * 100 * online : 0;
|
systemDelta > 0
|
||||||
|
? (cpuDelta / systemDelta) *
|
||||||
|
100 *
|
||||||
|
(data.cpu_stats.online_cpus || 1)
|
||||||
|
: 0;
|
||||||
|
|
||||||
const memUsage = stats.memory_stats?.usage || 0;
|
const memoryUsage = data.memory_stats?.usage || 0;
|
||||||
const memLimit = stats.memory_stats?.limit || 0;
|
const memoryLimit = data.memory_stats?.limit || 0;
|
||||||
const memMB = (memUsage / 1024 / 1024).toFixed(1);
|
const memoryMB = (memoryUsage / 1024 / 1024).toFixed(1);
|
||||||
const memLimitMB = (memLimit / 1024 / 1024).toFixed(0);
|
const memoryLimitMB = (memoryLimit / 1024 / 1024).toFixed(0);
|
||||||
|
|
||||||
cpu = `${cpuPercent.toFixed(1)}%`;
|
stats = {
|
||||||
memory = `${memMB} MB / ${memLimitMB} MB`;
|
cpu: cpuPercent.toFixed(1) + "%",
|
||||||
statsText = `${cpu}, ${memory}`;
|
memory: `${memoryMB} MB / ${memoryLimitMB} MB`,
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
statsText = "n/a";
|
console.warn(
|
||||||
|
`Failed to fetch stats for ${container.Names[0]}:`,
|
||||||
|
err instanceof Error ? err.message : err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c.Id.slice(0, 12),
|
id: container.Id.slice(0, 12),
|
||||||
name: c.Names?.[0]?.replace(/^\//, "") || "unknown",
|
name: container.Names[0]?.replace(/^\//, "") || "unknown",
|
||||||
image: c.Image,
|
image: container.Image,
|
||||||
state: c.State,
|
imageId: container.ImageID,
|
||||||
status: c.Status,
|
status: container.Status,
|
||||||
ports: c.Ports || [],
|
state: container.State,
|
||||||
labels: c.Labels || {},
|
created: container.Created,
|
||||||
cpu,
|
ports: (container.Ports || []).map((port: any) => {
|
||||||
memory,
|
if (port.PublicPort) {
|
||||||
stats: statsText,
|
return `${port.PublicPort}:${port.PrivatePort}/${port.Type}`;
|
||||||
|
}
|
||||||
|
return `${port.PrivatePort}/${port.Type}`;
|
||||||
|
}),
|
||||||
|
labels: container.Labels || {},
|
||||||
|
mounts: (container.Mounts || []).map((mount: any) => ({
|
||||||
|
source: mount.Source,
|
||||||
|
destination: mount.Destination,
|
||||||
|
mode: mount.Mode,
|
||||||
|
})),
|
||||||
|
stats,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return NextResponse.json(enriched);
|
return NextResponse.json({
|
||||||
|
total: containers.length,
|
||||||
|
running: containers.filter((c) => c.state === "running").length,
|
||||||
|
containers: containers.sort((a: any, b: any) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
),
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Containers API error:", error);
|
console.error("Docker API error:", error instanceof Error ? error.message : error);
|
||||||
return NextResponse.json({ error: "Failed to fetch containers" }, { status: 500 });
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : "Failed to fetch containers";
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: errorMessage, total: 0, running: 0, containers: [] },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
import axios from "axios";
|
||||||
|
import https from "https";
|
||||||
|
|
||||||
const SYNOLOGY_HOST = process.env.SYNOLOGY_HOST;
|
const SYNOLOGY_HOST = process.env.SYNOLOGY_HOST;
|
||||||
const SYNOLOGY_PORT = process.env.SYNOLOGY_PORT || "5000";
|
const SYNOLOGY_PORT = process.env.SYNOLOGY_PORT || "5001";
|
||||||
const SYNOLOGY_USERNAME = process.env.SYNOLOGY_USERNAME;
|
const SYNOLOGY_USERNAME = process.env.SYNOLOGY_USERNAME;
|
||||||
const SYNOLOGY_PASSWORD = process.env.SYNOLOGY_PASSWORD;
|
const SYNOLOGY_PASSWORD = process.env.SYNOLOGY_PASSWORD;
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
// Port 5000 = HTTP, 5001+ = HTTPS (Synology convention)
|
||||||
|
const protocol = SYNOLOGY_PORT === "5000" ? "http" : "https";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
if (!SYNOLOGY_HOST || !SYNOLOGY_USERNAME || !SYNOLOGY_PASSWORD) {
|
if (!SYNOLOGY_HOST || !SYNOLOGY_USERNAME || !SYNOLOGY_PASSWORD) {
|
||||||
@@ -16,89 +19,54 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const protocol = SYNOLOGY_PORT === "5001" ? "https" : "http";
|
const baseUrl = `${protocol}://${SYNOLOGY_HOST}:${SYNOLOGY_PORT}`;
|
||||||
const baseUrl = `${protocol}://${SYNOLOGY_HOST}:${SYNOLOGY_PORT}/webapi`;
|
const httpsConfig =
|
||||||
|
protocol === "https"
|
||||||
// Login
|
? {
|
||||||
const loginUrl = `${baseUrl}/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${encodeURIComponent(
|
httpsAgent: new https.Agent({ rejectUnauthorized: false }),
|
||||||
SYNOLOGY_USERNAME
|
|
||||||
)}&passwd=${encodeURIComponent(SYNOLOGY_PASSWORD)}&session=FileStation&format=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;
|
// Login to Synology
|
||||||
|
const loginResponse = await axios.get(`${baseUrl}/webapi/auth.cgi`, {
|
||||||
// Get storage info
|
params: {
|
||||||
const storageResp = await fetch(
|
api: "SYNO.API.Auth",
|
||||||
`${baseUrl}/entry.cgi?api=SYNO.Storage.CGI.Storage&version=1&method=load_info&_sid=${sid}`,
|
version: 3,
|
||||||
{ cache: "no-store" }
|
method: "login",
|
||||||
);
|
account: SYNOLOGY_USERNAME,
|
||||||
const storageData = await storageResp.json();
|
passwd: SYNOLOGY_PASSWORD,
|
||||||
|
session: "FileStation",
|
||||||
// Get system utilization
|
format: "sid",
|
||||||
let utilization = null;
|
},
|
||||||
try {
|
...httpsConfig,
|
||||||
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",
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse disks
|
const sid = loginResponse.data.data.sid;
|
||||||
const disks = (storageData.data?.disks || []).map((disk: any) => ({
|
|
||||||
name: disk.longName || disk.name || disk.id || "Unknown",
|
// Get storage info
|
||||||
model: disk.model || "Unknown",
|
const storageResponse = await axios.get(`${baseUrl}/webapi/entry.cgi`, {
|
||||||
serial: disk.serial || "",
|
params: {
|
||||||
sizeBytes: parseInt(disk.size_total || "0", 10),
|
api: "SYNO.Storage.CGI.Storage",
|
||||||
status: disk.smart_status || disk.overview_status || "unknown",
|
version: 1,
|
||||||
isSsd: disk.isSsd || false,
|
method: "load_info",
|
||||||
temp: disk.temp || null,
|
_sid: sid,
|
||||||
|
},
|
||||||
|
...httpsConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Logout (non-blocking)
|
return NextResponse.json(volumes);
|
||||||
fetch(
|
} catch (error) {
|
||||||
`${baseUrl}/auth.cgi?api=SYNO.API.Auth&version=3&method=logout&session=FileStation&_sid=${sid}`,
|
console.error("Synology API error:", error instanceof Error ? error.message : error);
|
||||||
{ cache: "no-store" }
|
|
||||||
).catch(() => {});
|
|
||||||
|
|
||||||
return NextResponse.json({ volumes, disks, utilization });
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Synology API error:", error?.message || error);
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Failed to fetch Synology storage" },
|
{ error: "Failed to fetch Synology storage" },
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
|
|||||||
190
src/app/page.tsx
190
src/app/page.tsx
@@ -1,15 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Server, Activity } from "lucide-react";
|
import { Search, Server, Activity } from "lucide-react";
|
||||||
import ContainerGroup from "@/components/ContainerGroup";
|
import ContainerGroup from "@/components/ContainerGroup";
|
||||||
import SearchBar from "@/components/SearchBar";
|
import SearchBar from "@/components/SearchBar";
|
||||||
import GrafanaWidget from "@/components/GrafanaWidget";
|
import GrafanaWidget from "@/components/GrafanaWidget";
|
||||||
import NetworkWidget from "@/components/NetworkWidget";
|
import UnifiWidget from "@/components/UnifiWidget";
|
||||||
import SynologyWidget from "@/components/SynologyWidget";
|
import SynologyWidget from "@/components/SynologyWidget";
|
||||||
import ServerStatsWidget from "@/components/ServerStatsWidget";
|
import SemanticSearch from "@/components/SemanticSearch";
|
||||||
import GPUStatsWidget from "@/components/GPUStatsWidget";
|
|
||||||
import RAGWidget from "@/components/RAGWidget";
|
|
||||||
import { Container } from "@/types";
|
import { Container } from "@/types";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -19,7 +17,7 @@ export default function Home() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchContainers();
|
fetchContainers();
|
||||||
const interval = setInterval(fetchContainers, 10000);
|
const interval = setInterval(fetchContainers, 10000); // Refresh every 10s
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -27,10 +25,19 @@ export default function Home() {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch("/api/containers");
|
const response = await fetch("/api/containers");
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
// Defensive: ensure we always set an array
|
||||||
|
if (data && Array.isArray(data.containers)) {
|
||||||
|
setContainers(data.containers);
|
||||||
|
} else if (Array.isArray(data)) {
|
||||||
setContainers(data);
|
setContainers(data);
|
||||||
|
} else {
|
||||||
|
console.warn("Unexpected container API response:", data);
|
||||||
|
setContainers([]);
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to fetch containers:", error);
|
console.error("Failed to fetch containers:", error);
|
||||||
|
setContainers([]);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -39,46 +46,86 @@ export default function Home() {
|
|||||||
return {
|
return {
|
||||||
media: containers.filter((c) =>
|
media: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"sonarr", "radarr", "lidarr", "whisparr", "prowlarr", "bazarr",
|
"sonarr",
|
||||||
"tautulli", "overseerr", "ombi", "jellyfin", "plex", "audiobookshelf",
|
"radarr",
|
||||||
|
"lidarr",
|
||||||
|
"whisparr",
|
||||||
|
"prowlarr",
|
||||||
|
"bazarr",
|
||||||
|
"tautulli",
|
||||||
|
"overseerr",
|
||||||
|
"ombi",
|
||||||
|
"jellyfin",
|
||||||
|
"plex",
|
||||||
|
"audiobookshelf",
|
||||||
"lazylibrarian",
|
"lazylibrarian",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
download: containers.filter((c) =>
|
download: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"qbittorrent", "transmission", "sabnzbd", "nzbget", "deluge",
|
"qbittorrent",
|
||||||
"gluetun", "flaresolverr",
|
"transmission",
|
||||||
|
"sabnzbd",
|
||||||
|
"nzbget",
|
||||||
|
"deluge",
|
||||||
|
"gluetun",
|
||||||
|
"flaresolverr",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
infrastructure: containers.filter((c) =>
|
infrastructure: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"traefik", "portainer", "heimdall", "homepage", "nginx", "caddy",
|
"traefik",
|
||||||
"pihole", "adguard", "unbound", "mosquitto",
|
"portainer",
|
||||||
|
"heimdall",
|
||||||
|
"homepage",
|
||||||
|
"nginx",
|
||||||
|
"caddy",
|
||||||
|
"pihole",
|
||||||
|
"adguard",
|
||||||
|
"unbound",
|
||||||
|
"mosquitto",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
monitoring: containers.filter((c) =>
|
monitoring: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"grafana", "prometheus", "cadvisor", "node-exporter", "dozzle",
|
"grafana",
|
||||||
"uptime-kuma", "beszel", "dockmon", "docker-stats-exporter", "diun",
|
"prometheus",
|
||||||
|
"cadvisor",
|
||||||
|
"node-exporter",
|
||||||
|
"dozzle",
|
||||||
|
"uptime-kuma",
|
||||||
|
"beszel",
|
||||||
|
"dockmon",
|
||||||
|
"docker-stats-exporter",
|
||||||
|
"diun",
|
||||||
"container-census",
|
"container-census",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
automation: containers.filter((c) =>
|
automation: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"homeassistant", "home-assistant", "n8n", "nodered", "node-red",
|
"homeassistant",
|
||||||
|
"home-assistant",
|
||||||
|
"n8n",
|
||||||
|
"nodered",
|
||||||
|
"node-red",
|
||||||
"duplicati",
|
"duplicati",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
productivity: containers.filter((c) =>
|
productivity: containers.filter((c) =>
|
||||||
[
|
[
|
||||||
"nextcloud", "openproject", "gitea", "gitlab", "code-server", "vscode",
|
"nextcloud",
|
||||||
|
"openproject",
|
||||||
|
"gitea",
|
||||||
|
"gitlab",
|
||||||
|
"code-server",
|
||||||
|
"vscode",
|
||||||
].some((app) => c.name.toLowerCase().includes(app))
|
].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
media_processing: containers.filter((c) =>
|
media_processing: containers.filter((c) =>
|
||||||
["tdarr"].some((app) => c.name.toLowerCase().includes(app))
|
["tdarr"].some((app) => c.name.toLowerCase().includes(app))
|
||||||
),
|
),
|
||||||
ai: containers.filter((c) =>
|
ai: containers.filter((c) =>
|
||||||
["openwebui", "open-webui", "ollama", "stable-diffusion", "mcp", "rag", "litellm", "llm-router", "qdrant", "chromadb"].some(
|
["openwebui", "open-webui", "ollama", "stable-diffusion", "mcp"].some(
|
||||||
(app) => c.name.toLowerCase().includes(app)
|
(app) => c.name.toLowerCase().includes(app)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -105,6 +152,7 @@ export default function Home() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900">
|
||||||
|
{/* Header */}
|
||||||
<header className="border-b border-gray-700 bg-gray-900/50 backdrop-blur-lg sticky top-0 z-50">
|
<header className="border-b border-gray-700 bg-gray-900/50 backdrop-blur-lg sticky top-0 z-50">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -126,65 +174,117 @@ export default function Home() {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
{/* Server & GPU Stats */}
|
{/* Widgets Section */}
|
||||||
<div className="space-y-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<ServerStatsWidget />
|
<UnifiWidget />
|
||||||
<GPUStatsWidget />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Network, NAS & Server Overview */}
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
||||||
<NetworkWidget />
|
|
||||||
<SynologyWidget />
|
<SynologyWidget />
|
||||||
|
<GrafanaWidget
|
||||||
|
title="Server Stats"
|
||||||
|
dashboardId="server-overview"
|
||||||
|
panelId={1}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grafana Dashboards */}
|
{/* Grafana Dashboards */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
||||||
<GrafanaWidget
|
<GrafanaWidget
|
||||||
title="Docker Containers"
|
title="Docker Stats"
|
||||||
dashboardUid="docker-containers"
|
dashboardId="docker-monitoring"
|
||||||
panelId={8}
|
panelId={2}
|
||||||
|
/>
|
||||||
|
<GrafanaWidget
|
||||||
|
title="LLM Metrics"
|
||||||
|
dashboardId="llm-monitoring"
|
||||||
|
panelId={3}
|
||||||
|
/>
|
||||||
|
<GrafanaWidget
|
||||||
|
title="System Load"
|
||||||
|
dashboardId="system-metrics"
|
||||||
|
panelId={4}
|
||||||
/>
|
/>
|
||||||
<RAGWidget />
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{/* Semantic Search */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<SemanticSearch />
|
||||||
|
</div>
|
||||||
|
{/* Container Groups */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{grouped.ai.length > 0 && (
|
|
||||||
<ContainerGroup title="AI Services" containers={grouped.ai} icon="🤖" />
|
|
||||||
)}
|
|
||||||
{grouped.media.length > 0 && (
|
{grouped.media.length > 0 && (
|
||||||
<ContainerGroup title="Media Management" containers={grouped.media} icon="📺" />
|
<ContainerGroup
|
||||||
|
title="Media Management"
|
||||||
|
containers={grouped.media}
|
||||||
|
icon="📺"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.download.length > 0 && (
|
{grouped.download.length > 0 && (
|
||||||
<ContainerGroup title="Download Clients" containers={grouped.download} icon="⬇️" />
|
<ContainerGroup
|
||||||
|
title="Download Clients"
|
||||||
|
containers={grouped.download}
|
||||||
|
icon="⬇️"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{grouped.ai.length > 0 && (
|
||||||
|
<ContainerGroup
|
||||||
|
title="AI Services"
|
||||||
|
containers={grouped.ai}
|
||||||
|
icon="🤖"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.photos.length > 0 && (
|
{grouped.photos.length > 0 && (
|
||||||
<ContainerGroup title="Photo Management" containers={grouped.photos} icon="📷" />
|
<ContainerGroup
|
||||||
|
title="Photo Management"
|
||||||
|
containers={grouped.photos}
|
||||||
|
icon="📷"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.media_processing.length > 0 && (
|
{grouped.media_processing.length > 0 && (
|
||||||
<ContainerGroup title="Media Processing" containers={grouped.media_processing} icon="🎬" />
|
<ContainerGroup
|
||||||
|
title="Media Processing"
|
||||||
|
containers={grouped.media_processing}
|
||||||
|
icon="🎬"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.automation.length > 0 && (
|
{grouped.automation.length > 0 && (
|
||||||
<ContainerGroup title="Automation" containers={grouped.automation} icon="⚡" />
|
<ContainerGroup
|
||||||
|
title="Automation"
|
||||||
|
containers={grouped.automation}
|
||||||
|
icon="⚡"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.productivity.length > 0 && (
|
{grouped.productivity.length > 0 && (
|
||||||
<ContainerGroup title="Productivity" containers={grouped.productivity} icon="💼" />
|
<ContainerGroup
|
||||||
|
title="Productivity"
|
||||||
|
containers={grouped.productivity}
|
||||||
|
icon="💼"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.infrastructure.length > 0 && (
|
{grouped.infrastructure.length > 0 && (
|
||||||
<ContainerGroup title="Infrastructure" containers={grouped.infrastructure} icon="🔧" />
|
<ContainerGroup
|
||||||
|
title="Infrastructure"
|
||||||
|
containers={grouped.infrastructure}
|
||||||
|
icon="🔧"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.monitoring.length > 0 && (
|
{grouped.monitoring.length > 0 && (
|
||||||
<ContainerGroup title="Monitoring" containers={grouped.monitoring} icon="📊" />
|
<ContainerGroup
|
||||||
|
title="Monitoring"
|
||||||
|
containers={grouped.monitoring}
|
||||||
|
icon="📊"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{grouped.databases.length > 0 && (
|
{grouped.databases.length > 0 && (
|
||||||
<ContainerGroup title="Databases" containers={grouped.databases} icon="🗄️" />
|
<ContainerGroup
|
||||||
|
title="Databases"
|
||||||
|
containers={grouped.databases}
|
||||||
|
icon="🗄️"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user