From 7cf923179a5d636b4d00f5efe2429af5af383aec Mon Sep 17 00:00:00 2001 From: Cleboost Date: Wed, 1 Oct 2025 21:35:32 +0200 Subject: [PATCH] Add system monitor page and backend logic Introduces a new system monitor page for servers, accessible via the UI and router. Implements SystemMonitor and Docker classes in Server.ts to provide system and Docker stats. Updates the server tools to enable the System Monitor, and registers the new route in the router. --- src/class/Server.ts | 345 +++++++++++++++++++++++- src/pages/server/index.vue | 4 +- src/pages/server/monitor.vue | 495 +++++++++++++++++++++++++++++++++++ src/router/index.ts | 28 +- 4 files changed, 866 insertions(+), 6 deletions(-) create mode 100644 src/pages/server/monitor.vue diff --git a/src/class/Server.ts b/src/class/Server.ts index 93096de..47a8ede 100644 --- a/src/class/Server.ts +++ b/src/class/Server.ts @@ -5,14 +5,18 @@ import Key from "./Class"; export default class Server { public readonly id: ServerType["id"]; - configClass: ConfigServer | undefined; - console: ServerConsole | undefined; + configClass: ConfigServer; + console: ServerConsole; + docker: Docker; + systemMonitor: SystemMonitor; readonly serversStore = useServerConfigStore(); constructor(id: string) { this.id = id; this.configClass = new ConfigServer(this.serversStore.getServer(id)); this.console = new ServerConsole(this); + this.docker = new Docker(this); + this.systemMonitor = new SystemMonitor(this); } config(): ConfigServer { @@ -124,3 +128,340 @@ export class ServerConsole { throw lastErr ?? new Error("No terminal emulator available"); } } + +class Docker { + readonly server: Server; + + constructor(server: Server) { + this.server = server; + } + + async getVersion(): Promise { + try { + const output = await this.server.console.execute("docker version"); + const lines = output.split("\n"); + let foundCommunity = false; + let version = null; + + for (let i = 0; i < lines.length; i++) { + if (!foundCommunity && lines[i].includes("Docker Engine - Community")) { + foundCommunity = true; + for (let j = i + 1; j < lines.length; j++) { + const versionMatch = lines[j].match(/Version:\s*([^\s]+)/); + if (versionMatch) { + version = versionMatch[1]; + break; + } + } + break; + } + } + + if (foundCommunity && version) { + return `Community Version ${version}`; + } else { + return "Unknown version"; + } + } catch (error) { + console.error("Error retrieving Docker version:", error); + if (error instanceof Error) { + return `Error: ${error.message}`; + } + return "Connection error"; + } + } + + async getAllDockerData(): Promise<{ + containers: { + running: number; + stopped: number; + total: number; + }; + images: { + local: number; + size: string; + dangling: number; + }; + }> { + try { + const command = `echo "===CONTAINERS===" && docker ps -a --format "{{.Status}}" && echo "===IMAGES===" && docker images --format "{{.Repository}}" && echo "===DANGLING===" && docker images -f dangling=true --format "{{.Repository}}" && echo "===SIZE===" && docker system df`; + + const output = await this.server.console.execute(command); + console.log("Docker command output:", output); // Debug log + + const lines = output.split("\n"); + let currentSection = ""; + const containers: string[] = []; + const images: string[] = []; + const dangling: string[] = []; + let size = "0B"; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine === "===CONTAINERS===") { + currentSection = "containers"; + continue; + } else if (trimmedLine === "===IMAGES===") { + currentSection = "images"; + continue; + } else if (trimmedLine === "===DANGLING===") { + currentSection = "dangling"; + continue; + } else if (trimmedLine === "===SIZE===") { + currentSection = "size"; + continue; + } + + if (trimmedLine && !trimmedLine.includes("REPOSITORY")) { + switch (currentSection) { + case "containers": + containers.push(trimmedLine); + break; + case "images": + images.push(trimmedLine); + break; + case "dangling": + dangling.push(trimmedLine); + break; + case "size": + if (trimmedLine.includes("Images")) { + const sizeMatch = trimmedLine.match(/(\d+\.?\d*[GMK]?B)/); + if (sizeMatch) { + size = sizeMatch[1]; + } + } + break; + } + } + } + + let running = 0; + let stopped = 0; + for (const container of containers) { + if (container.includes("Up")) { + running++; + } else { + stopped++; + } + } + + return { + containers: { + running, + stopped, + total: running + stopped, + }, + images: { + local: images.length, + size, + dangling: dangling.length, + }, + }; + } catch (error) { + console.error("Error retrieving Docker data:", error); + return { + containers: { running: 0, stopped: 0, total: 0 }, + images: { local: 0, size: "0B", dangling: 0 }, + }; + } + } +} + +class SystemMonitor { + readonly server: Server; + + constructor(server: Server) { + this.server = server; + } + + async getSystemInfo(): Promise<{ + hostname: string; + uptime: string; + os: string; + kernel: string; + architecture: string; + }> { + try { + const commands = [ + "hostname", + "uptime -p", + "uname -s", + "uname -r", + "uname -m", + ]; + + const results = await Promise.all( + commands.map((cmd) => this.server.console.execute(cmd)), + ); + + return { + hostname: results[0].trim(), + uptime: results[1].trim(), + os: results[2].trim(), + kernel: results[3].trim(), + architecture: results[4].trim(), + }; + } catch (error) { + console.error("Error getting system info:", error); + return { + hostname: "Unknown", + uptime: "Unknown", + os: "Unknown", + kernel: "Unknown", + architecture: "Unknown", + }; + } + } + + async getSystemStats(): Promise<{ + cpu: { + usage: number; + cores: number; + load: number[]; + }; + memory: { + total: number; + used: number; + free: number; + cached: number; + percentage: number; + }; + disk: { + total: number; + used: number; + free: number; + percentage: number; + }; + network: { + rx: number; + tx: number; + rxRate: number; + txRate: number; + }; + }> { + try { + const command = ` + echo "===CPU===" + # CPU usage from /proc/stat + head -1 /proc/stat | awk '{idle=\$5+\$6; total=\$2+\$3+\$4+\$5+\$6+\$7+\$8; print (1-idle/total)*100}' + nproc + uptime | awk -F'load average:' '{print \$2}' | awk '{print \$1","\$2","\$3}' | sed 's/,/ /g' + echo "===MEMORY===" + free -m | grep Mem | awk '{print \$2","\$3","\$4","\$6}' + echo "===DISK===" + df / | tail -1 | awk '{total=\$2/1024/1024; used=\$3/1024/1024; free=\$4/1024/1024; print total","used","free}' + echo "===NETWORK===" + # Get network stats from first active interface + cat /proc/net/dev | grep -E "(eth|ens|enp|wlan|wlp)" | head -1 | awk '{print \$2","\$10}' || echo "0,0" + `; + + const output = await this.server.console.execute(command); + console.log("System stats command output:", output); // Debug log + + const lines = output.split("\n"); + let currentSection = ""; + const cpuData: string[] = []; + const memoryData: string[] = []; + const diskData: string[] = []; + const networkData: string[] = []; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine === "===CPU===") { + currentSection = "cpu"; + continue; + } else if (trimmedLine === "===MEMORY===") { + currentSection = "memory"; + continue; + } else if (trimmedLine === "===DISK===") { + currentSection = "disk"; + continue; + } else if (trimmedLine === "===NETWORK===") { + currentSection = "network"; + continue; + } + + if (trimmedLine && !trimmedLine.includes("===")) { + switch (currentSection) { + case "cpu": + cpuData.push(trimmedLine); + break; + case "memory": + memoryData.push(trimmedLine); + break; + case "disk": + diskData.push(trimmedLine); + break; + case "network": + networkData.push(trimmedLine); + break; + } + } + } + + const cpuUsage = parseFloat(cpuData[0] || "0"); + const cores = parseInt(cpuData[1] || "1"); + const loadAvgStr = cpuData[2] || "0 0 0"; + const loadAvg = loadAvgStr + .split(" ") + .map(parseFloat) + .filter((n) => !isNaN(n)); + + const memoryStr = memoryData[0] || "0,0,0,0"; + const memoryValues = memoryStr.split(",").map(Number); + const [totalMB, usedMB, freeMB, cachedMB] = memoryValues; + const memoryPercentage = totalMB > 0 ? (usedMB / totalMB) * 100 : 0; + + const diskStr = diskData[0] || "0,0,0"; + const diskValues = diskStr.split(",").map(Number); + const [totalGB, usedGB, freeGB] = diskValues; + const diskPercentage = totalGB > 0 ? (usedGB / totalGB) * 100 : 0; + + const networkStr = networkData[0] || "0,0"; + const networkValues = networkStr.split(",").map((val) => { + const num = Number(val); + return isNaN(num) ? 0 : num; + }); + const [rxBytes, txBytes] = networkValues; + + return { + cpu: { + usage: cpuUsage, + cores: cores, + load: loadAvg.length >= 3 ? loadAvg : [0, 0, 0], + }, + memory: { + total: totalMB, + used: usedMB, + free: freeMB, + cached: cachedMB, + percentage: memoryPercentage, + }, + disk: { + total: totalGB, + used: usedGB, + free: freeGB, + percentage: diskPercentage, + }, + network: { + rx: rxBytes, + tx: txBytes, + rxRate: 0, + txRate: 0, + }, + }; + } catch (error) { + console.error("Error getting system stats:", error); + return { + cpu: { usage: 0, cores: 1, load: [0, 0, 0] }, + memory: { total: 0, used: 0, free: 0, cached: 0, percentage: 0 }, + disk: { total: 0, used: 0, free: 0, percentage: 0 }, + network: { rx: 0, tx: 0, rxRate: 0, txRate: 0 }, + }; + } + } +} + +export type { ConfigServer, Docker, SystemMonitor }; diff --git a/src/pages/server/index.vue b/src/pages/server/index.vue index 77fc3aa..d7fc3e7 100644 --- a/src/pages/server/index.vue +++ b/src/pages/server/index.vue @@ -40,8 +40,8 @@ const tools = [ name: "System Monitor", desc: "Monitor system resources", icon: "mdi:chart-line", - disabled: true, - click: () => {}, + tag: "Beta", + click: () => router.push(`/server/${server.value?.id}/monitor`), }, { name: "Logs Viewer", diff --git a/src/pages/server/monitor.vue b/src/pages/server/monitor.vue new file mode 100644 index 0000000..1676aac --- /dev/null +++ b/src/pages/server/monitor.vue @@ -0,0 +1,495 @@ + + + diff --git a/src/router/index.ts b/src/router/index.ts index 7ff8619..6c720a0 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -20,10 +20,34 @@ const routes = [ { path: "/server/:id", name: "server", - children: [ - ], + children: [], component: () => import("@/pages/server/index.vue"), }, + { + path: "/server/:id/docker", + name: "docker", + component: () => import("@/pages/server/docker.vue"), + }, + { + path: "/server/:id/docker/images", + name: "images", + component: () => import("@/pages/server/docker/images.vue"), + }, + { + path: "/server/:id/docker/containers", + name: "containers", + component: () => import("@/pages/server/docker/containers.vue"), + }, + { + path: "/server/:id/docker/container/:cid", + name: "container-details", + component: () => import("@/pages/server/docker/container/index.vue"), + }, + { + path: "/server/:id/monitor", + name: "system-monitor", + component: () => import("@/pages/server/monitor.vue"), + }, { path: "/settings", name: "settings",