diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts index f74095dc..42da3d13 100644 --- a/src/backend/ssh/server-stats.ts +++ b/src/backend/ssh/server-stats.ts @@ -690,6 +690,36 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ percent: number | null; usedHuman: string | null; totalHuman: string | null; + availableHuman: string | null; + }; + network: { + interfaces: Array<{ + name: string; + ip: string; + state: string; + rxBytes: string | null; + txBytes: string | null; + }>; + }; + uptime: { + seconds: number | null; + formatted: string | null; + }; + processes: { + total: number | null; + running: number | null; + top: Array<{ + pid: string; + user: string; + cpu: string; + mem: string; + command: string; + }>; + }; + system: { + hostname: string | null; + kernel: string | null; + os: string | null; }; }> { const cached = metricsCache.get(host.id); @@ -842,6 +872,159 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ availableHuman = null; } + // Collect network interfaces + let interfaces: Array<{ + name: string; + ip: string; + state: string; + rxBytes: string | null; + txBytes: string | null; + }> = []; + try { + const ifconfigOut = await execCommand( + client, + "ip -o addr show | awk '{print $2,$4}' | grep -v '^lo'", + ); + const netStatOut = await execCommand( + client, + "ip -o link show | awk '{print $2,$9}' | sed 's/:$//'", + ); + + const addrs = ifconfigOut.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + const states = netStatOut.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + + const ifMap = new Map(); + for (const line of addrs) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + const name = parts[0]; + const ip = parts[1].split("/")[0]; + if (!ifMap.has(name)) ifMap.set(name, { ip, state: "UNKNOWN" }); + } + } + for (const line of states) { + const parts = line.split(/\s+/); + if (parts.length >= 2) { + const name = parts[0]; + const state = parts[1]; + const existing = ifMap.get(name); + if (existing) { + existing.state = state; + } + } + } + + for (const [name, data] of ifMap.entries()) { + interfaces.push({ + name, + ip: data.ip, + state: data.state, + rxBytes: null, + txBytes: null, + }); + } + } catch (e) { + statsLogger.warn( + `Failed to collect network metrics for host ${host.id}`, + e, + ); + } + + // Collect uptime + let uptimeSeconds: number | null = null; + let uptimeFormatted: string | null = null; + try { + const uptimeOut = await execCommand(client, "cat /proc/uptime"); + const uptimeParts = uptimeOut.stdout.trim().split(/\s+/); + if (uptimeParts.length >= 1) { + uptimeSeconds = Number(uptimeParts[0]); + if (Number.isFinite(uptimeSeconds)) { + const days = Math.floor(uptimeSeconds / 86400); + const hours = Math.floor((uptimeSeconds % 86400) / 3600); + const minutes = Math.floor((uptimeSeconds % 3600) / 60); + uptimeFormatted = `${days}d ${hours}h ${minutes}m`; + } + } + } catch (e) { + statsLogger.warn(`Failed to collect uptime for host ${host.id}`, e); + } + + // Collect process information + let totalProcesses: number | null = null; + let runningProcesses: number | null = null; + let topProcesses: Array<{ + pid: string; + user: string; + cpu: string; + mem: string; + command: string; + }> = []; + try { + const psOut = await execCommand( + client, + "ps aux --sort=-%cpu | head -n 11", + ); + const psLines = psOut.stdout + .split("\n") + .map((l) => l.trim()) + .filter(Boolean); + if (psLines.length > 1) { + for (let i = 1; i < Math.min(psLines.length, 11); i++) { + const parts = psLines[i].split(/\s+/); + if (parts.length >= 11) { + topProcesses.push({ + pid: parts[1], + user: parts[0], + cpu: parts[2], + mem: parts[3], + command: parts.slice(10).join(" ").substring(0, 50), + }); + } + } + } + + const procCount = await execCommand(client, "ps aux | wc -l"); + const runningCount = await execCommand( + client, + "ps aux | grep -c ' R '", + ); + totalProcesses = Number(procCount.stdout.trim()) - 1; + runningProcesses = Number(runningCount.stdout.trim()); + } catch (e) { + statsLogger.warn( + `Failed to collect process info for host ${host.id}`, + e, + ); + } + + // Collect system information + let hostname: string | null = null; + let kernel: string | null = null; + let os: string | null = null; + try { + const hostnameOut = await execCommand(client, "hostname"); + const kernelOut = await execCommand(client, "uname -r"); + const osOut = await execCommand( + client, + "cat /etc/os-release | grep '^PRETTY_NAME=' | cut -d'\"' -f2", + ); + + hostname = hostnameOut.stdout.trim() || null; + kernel = kernelOut.stdout.trim() || null; + os = osOut.stdout.trim() || null; + } catch (e) { + statsLogger.warn( + `Failed to collect system info for host ${host.id}`, + e, + ); + } + const result = { cpu: { percent: toFixedNum(cpuPercent, 0), cores, load: loadTriplet }, memory: { @@ -855,6 +1038,23 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{ totalHuman, availableHuman, }, + network: { + interfaces, + }, + uptime: { + seconds: uptimeSeconds, + formatted: uptimeFormatted, + }, + processes: { + total: totalProcesses, + running: runningProcesses, + top: topProcesses, + }, + system: { + hostname, + kernel, + os, + }, }; metricsCache.set(host.id, result); @@ -1033,7 +1233,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => { error: "Host is offline", cpu: { percent: null, cores: null, load: null }, memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedHuman: null, totalHuman: null }, + disk: { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + }, + network: { interfaces: [] }, + uptime: { seconds: null, formatted: null }, + processes: { total: null, running: null, top: [] }, + system: { hostname: null, kernel: null, os: null }, lastChecked: new Date().toISOString(), }); } @@ -1050,7 +1259,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => { message: "Server Stats unavailable for TOTP-enabled servers", cpu: { percent: null, cores: null, load: null }, memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedHuman: null, totalHuman: null }, + disk: { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + }, + network: { interfaces: [] }, + uptime: { seconds: null, formatted: null }, + processes: { total: null, running: null, top: [] }, + system: { hostname: null, kernel: null, os: null }, lastChecked: new Date().toISOString(), }); } @@ -1062,7 +1280,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => { error: "Metrics collection timeout", cpu: { percent: null, cores: null, load: null }, memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedHuman: null, totalHuman: null }, + disk: { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + }, + network: { interfaces: [] }, + uptime: { seconds: null, formatted: null }, + processes: { total: null, running: null, top: [] }, + system: { hostname: null, kernel: null, os: null }, lastChecked: new Date().toISOString(), }); } @@ -1071,7 +1298,16 @@ app.get("/metrics/:id", validateHostId, async (req, res) => { error: "Failed to collect metrics", cpu: { percent: null, cores: null, load: null }, memory: { percent: null, usedGiB: null, totalGiB: null }, - disk: { percent: null, usedHuman: null, totalHuman: null }, + disk: { + percent: null, + usedHuman: null, + totalHuman: null, + availableHuman: null, + }, + network: { interfaces: [] }, + uptime: { seconds: null, formatted: null }, + processes: { total: null, running: null, top: [] }, + system: { hostname: null, kernel: null, os: null }, lastChecked: new Date().toISOString(), }); } diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index b8904ee2..62abc045 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -1169,7 +1169,18 @@ "saveLayout": "Save Layout", "unsavedChanges": "Unsaved changes", "layoutSaved": "Layout saved successfully", - "failedToSaveLayout": "Failed to save layout" + "failedToSaveLayout": "Failed to save layout", + "systemInfo": "System Information", + "hostname": "Hostname", + "operatingSystem": "Operating System", + "kernel": "Kernel", + "totalUptime": "Total Uptime", + "seconds": "seconds", + "networkInterfaces": "Network Interfaces", + "noInterfacesFound": "No network interfaces found", + "totalProcesses": "Total Processes", + "running": "Running", + "noProcessesFound": "No processes found" }, "auth": { "loginTitle": "Login to Termix", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index ec58bb9e..606d2728 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -1149,7 +1149,18 @@ "saveLayout": "保存布局", "unsavedChanges": "有未保存的更改", "layoutSaved": "布局保存成功", - "failedToSaveLayout": "保存布局失败" + "failedToSaveLayout": "保存布局失败", + "systemInfo": "系统信息", + "hostname": "主机名", + "operatingSystem": "操作系统", + "kernel": "内核", + "totalUptime": "总运行时间", + "seconds": "秒", + "networkInterfaces": "网络接口", + "noInterfacesFound": "未找到网络接口", + "totalProcesses": "总进程数", + "running": "运行中", + "noProcessesFound": "未找到进程" }, "auth": { "loginTitle": "登录 Termix", diff --git a/src/types/stats-widgets.ts b/src/types/stats-widgets.ts index c5f86cef..3f083f17 100644 --- a/src/types/stats-widgets.ts +++ b/src/types/stats-widgets.ts @@ -1,9 +1,16 @@ -export type WidgetType = "cpu" | "memory" | "disk"; +export type WidgetType = + | "cpu" + | "memory" + | "disk" + | "network" + | "uptime" + | "processes" + | "system"; export interface StatsConfig { enabledWidgets: WidgetType[]; } export const DEFAULT_STATS_CONFIG: StatsConfig = { - enabledWidgets: ["cpu", "memory", "disk"], + enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"], }; diff --git a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx index 660c1009..bf89992f 100644 --- a/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx +++ b/src/ui/Desktop/Apps/Host Manager/HostManagerEditor.tsx @@ -217,10 +217,29 @@ export function HostManagerEditor({ statsConfig: z .object({ enabledWidgets: z - .array(z.enum(["cpu", "memory", "disk"])) - .default(["cpu", "memory", "disk"]), + .array( + z.enum([ + "cpu", + "memory", + "disk", + "network", + "uptime", + "processes", + "system", + ]), + ) + .default(["cpu", "memory", "disk", "network", "uptime", "system"]), }) - .default({ enabledWidgets: ["cpu", "memory", "disk"] }), + .default({ + enabledWidgets: [ + "cpu", + "memory", + "disk", + "network", + "uptime", + "system", + ], + }), }) .superRefine((data, ctx) => { if (data.authType === "password") { @@ -1562,7 +1581,17 @@ export function HostManagerEditor({ {t("hosts.enabledWidgetsDesc")}
- {(["cpu", "memory", "disk"] as const).map((widget) => ( + {( + [ + "cpu", + "memory", + "disk", + "network", + "uptime", + "processes", + "system", + ] as const + ).map((widget) => (
))} diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx index e6f482f3..619f5a03 100644 --- a/src/ui/Desktop/Apps/Server/Server.tsx +++ b/src/ui/Desktop/Apps/Server/Server.tsx @@ -17,7 +17,15 @@ import { type StatsConfig, DEFAULT_STATS_CONFIG, } from "@/types/stats-widgets"; -import { CpuWidget, MemoryWidget, DiskWidget } from "./widgets"; +import { + CpuWidget, + MemoryWidget, + DiskWidget, + NetworkWidget, + UptimeWidget, + ProcessesWidget, + SystemWidget, +} from "./widgets"; interface ServerProps { hostConfig?: any; @@ -82,6 +90,26 @@ export function Server({ case "disk": return ; + case "network": + return ( + + ); + + case "uptime": + return ( + + ); + + case "processes": + return ( + + ); + + case "system": + return ( + + ); + default: return null; } @@ -373,7 +401,7 @@ export function Server({ ) : (
{enabledWidgets.map((widgetType) => ( -
+
{renderWidget(widgetType)}
))} diff --git a/src/ui/Desktop/Apps/Server/widgets/NetworkWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/NetworkWidget.tsx new file mode 100644 index 00000000..f79372fc --- /dev/null +++ b/src/ui/Desktop/Apps/Server/widgets/NetworkWidget.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Network, Wifi, WifiOff } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ServerMetrics } from "@/ui/main-axios.ts"; + +interface NetworkWidgetProps { + metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; +} + +export function NetworkWidget({ metrics }: NetworkWidgetProps) { + const { t } = useTranslation(); + + const network = (metrics as any)?.network; + const interfaces = network?.interfaces || []; + + return ( +
+
+ +

+ {t("serverStats.networkInterfaces")} +

+
+ +
+ {interfaces.length === 0 ? ( +
+ +

{t("serverStats.noInterfacesFound")}

+
+ ) : ( + interfaces.map((iface: any, index: number) => ( +
+
+
+ + + {iface.name} + +
+ + {iface.state} + +
+
{iface.ip}
+
+ )) + )} +
+
+ ); +} diff --git a/src/ui/Desktop/Apps/Server/widgets/ProcessesWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/ProcessesWidget.tsx new file mode 100644 index 00000000..464fdf83 --- /dev/null +++ b/src/ui/Desktop/Apps/Server/widgets/ProcessesWidget.tsx @@ -0,0 +1,76 @@ +import React from "react"; +import { List, Activity } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ServerMetrics } from "@/ui/main-axios.ts"; + +interface ProcessesWidgetProps { + metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; +} + +export function ProcessesWidget({ metrics }: ProcessesWidgetProps) { + const { t } = useTranslation(); + + const processes = (metrics as any)?.processes; + const topProcesses = processes?.top || []; + + return ( +
+
+ +

+ {t("serverStats.processes")} +

+
+ +
+
+ {t("serverStats.totalProcesses")}:{" "} + + {processes?.total ?? "N/A"} + +
+
+ {t("serverStats.running")}:{" "} + + {processes?.running ?? "N/A"} + +
+
+ +
+ {topProcesses.length === 0 ? ( +
+ +

{t("serverStats.noProcessesFound")}

+
+ ) : ( +
+ {topProcesses.map((proc: any, index: number) => ( +
+
+ + PID: {proc.pid} + +
+ CPU: {proc.cpu}% + MEM: {proc.mem}% +
+
+
+ {proc.command} +
+
+ User: {proc.user} +
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/src/ui/Desktop/Apps/Server/widgets/SystemWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/SystemWidget.tsx new file mode 100644 index 00000000..fd7e8e01 --- /dev/null +++ b/src/ui/Desktop/Apps/Server/widgets/SystemWidget.tsx @@ -0,0 +1,64 @@ +import React from "react"; +import { Server, Info } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ServerMetrics } from "@/ui/main-axios.ts"; + +interface SystemWidgetProps { + metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; +} + +export function SystemWidget({ metrics }: SystemWidgetProps) { + const { t } = useTranslation(); + + const system = (metrics as any)?.system; + + return ( +
+
+ +

+ {t("serverStats.systemInfo")} +

+
+ +
+
+ +
+

+ {t("serverStats.hostname")} +

+

+ {system?.hostname || "N/A"} +

+
+
+ +
+ +
+

+ {t("serverStats.operatingSystem")} +

+

+ {system?.os || "N/A"} +

+
+
+ +
+ +
+

+ {t("serverStats.kernel")} +

+

+ {system?.kernel || "N/A"} +

+
+
+
+
+ ); +} diff --git a/src/ui/Desktop/Apps/Server/widgets/UptimeWidget.tsx b/src/ui/Desktop/Apps/Server/widgets/UptimeWidget.tsx new file mode 100644 index 00000000..9f47382f --- /dev/null +++ b/src/ui/Desktop/Apps/Server/widgets/UptimeWidget.tsx @@ -0,0 +1,49 @@ +import React from "react"; +import { Clock, Activity } from "lucide-react"; +import { useTranslation } from "react-i18next"; +import type { ServerMetrics } from "@/ui/main-axios.ts"; + +interface UptimeWidgetProps { + metrics: ServerMetrics | null; + metricsHistory: ServerMetrics[]; +} + +export function UptimeWidget({ metrics }: UptimeWidgetProps) { + const { t } = useTranslation(); + + const uptime = (metrics as any)?.uptime; + + return ( +
+
+ +

+ {t("serverStats.uptime")} +

+
+ +
+
+
+ +
+
+ +
+
+ {uptime?.formatted || "N/A"} +
+
+ {t("serverStats.totalUptime")} +
+ {uptime?.seconds && ( +
+ {Math.floor(uptime.seconds).toLocaleString()}{" "} + {t("serverStats.seconds")} +
+ )} +
+
+
+ ); +} diff --git a/src/ui/Desktop/Apps/Server/widgets/index.ts b/src/ui/Desktop/Apps/Server/widgets/index.ts index 37078036..2d227299 100644 --- a/src/ui/Desktop/Apps/Server/widgets/index.ts +++ b/src/ui/Desktop/Apps/Server/widgets/index.ts @@ -1,3 +1,7 @@ export { CpuWidget } from "./CpuWidget"; export { MemoryWidget } from "./MemoryWidget"; export { DiskWidget } from "./DiskWidget"; +export { NetworkWidget } from "./NetworkWidget"; +export { UptimeWidget } from "./UptimeWidget"; +export { ProcessesWidget } from "./ProcessesWidget"; +export { SystemWidget } from "./SystemWidget";