增加磁盘和网卡硬件信息显示

This commit is contained in:
Alex Yang
2025-12-07 10:30:10 +08:00
parent 611c8dac0e
commit 5112ebe2e0
11 changed files with 6170 additions and 112 deletions

View File

@@ -1,10 +1,16 @@
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"time"
"github.com/gin-gonic/gin"
@@ -34,10 +40,119 @@ func isDefaultDBConfig(cfg *config.Config) bool {
cfg.DB.Database == "monitor"
}
// daemonize 实现守护进程功能
func daemonize() error {
// 检查是否已经是守护进程模式
if os.Getenv("DAEMONIZED") == "1" {
// 设置工作目录
if err := os.Chdir("/"); err != nil {
return fmt.Errorf("failed to chdir to /: %v", err)
}
// 重设文件权限掩码
syscall.Umask(0)
return nil
}
// 获取可执行文件的绝对路径
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to get executable path: %v", err)
}
// 创建环境变量,标记为守护进程模式
env := append(os.Environ(), "DAEMONIZED=1")
// 启动新进程
cmd := exec.Command(execPath, os.Args[1:]...)
cmd.Env = env
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil
cmd.Dir = "/"
cmd.SysProcAttr = &syscall.SysProcAttr{
Setsid: true,
}
// 启动进程
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start daemon: %v", err)
}
// 父进程退出
os.Exit(0)
return nil
}
// savePID 保存进程ID到文件
func savePID() error {
if pidFile == "" {
return nil
}
// 获取当前进程ID
pid := strconv.Itoa(os.Getpid())
// 写入PID文件
if err := os.WriteFile(pidFile, []byte(pid), 0644); err != nil {
return fmt.Errorf("failed to write PID file: %v", err)
}
return nil
}
// removePID 删除PID文件
func removePID() {
if pidFile != "" {
os.Remove(pidFile)
}
}
// 命令行参数
var (
// 是否以守护进程模式运行
daemonMode bool
// 日志文件路径
logFilePath string
// 进程ID文件路径
pidFile string
)
// main 函数启动服务器
func main() {
// 解析命令行参数
flag.BoolVar(&daemonMode, "daemon", false, "Run as daemon (background process)")
flag.BoolVar(&daemonMode, "d", false, "Run as daemon (background process) - shorthand")
flag.StringVar(&logFilePath, "log-file", "", "Path to log file")
flag.StringVar(&logFilePath, "l", "", "Path to log file - shorthand")
flag.StringVar(&pidFile, "pid-file", "/tmp/monitor-backend.pid", "Path to PID file")
flag.StringVar(&pidFile, "p", "/tmp/monitor-backend.pid", "Path to PID file - shorthand")
flag.Parse()
// 配置日志:同时输出到文件和标准输出
logFileName := fmt.Sprintf("monitor-backend-%s.log", time.Now().Format("2006-01-02"))
// 处理日志文件路径
if logFilePath != "" {
// 如果是相对路径,则使用可执行文件所在的目录作为基准目录
if !filepath.IsAbs(logFilePath) {
// 获取可执行文件的目录
execPath, err := os.Executable()
if err != nil {
log.Printf("Warning: Failed to get executable path, using current directory for log file")
logFileName = logFilePath
} else {
execDir := filepath.Dir(execPath)
logFileName = filepath.Join(execDir, logFilePath)
}
} else {
logFileName = logFilePath
}
}
// 打开日志文件
logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Printf("Warning: Failed to open log file %s, logging only to stdout: %v", logFileName, err)
@@ -73,6 +188,28 @@ func main() {
os.Exit(0)
}
// 如果指定了守护进程模式,则启动守护进程
if daemonMode {
if err := daemonize(); err != nil {
log.Fatalf("Failed to daemonize: %v", err)
}
}
// 保存PID文件
if err := savePID(); err != nil {
log.Printf("Warning: Failed to save PID file: %v", err)
}
// 注册信号处理确保进程退出时删除PID文件
go func() {
sigCh := make(chan os.Signal, 1)
// 只处理可以捕获的信号
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
<-sigCh
removePID()
os.Exit(0)
}()
// 创建存储实例
store := storage.NewStorage(cfg)
defer store.Close()
@@ -156,6 +293,19 @@ func main() {
handler.RegisterRoutes(r)
// 处理所有未匹配的路由返回静态文件或index.html
// 静态文件服务的路径处理:如果是相对路径,使用可执行文件所在目录作为基准目录
staticDir := "./static"
if !filepath.IsAbs(staticDir) {
// 获取可执行文件的目录
execPath, err := os.Executable()
if err != nil {
log.Printf("Warning: Failed to get executable path, using current directory for static files")
} else {
execDir := filepath.Dir(execPath)
staticDir = filepath.Join(execDir, staticDir)
}
}
r.NoRoute(func(c *gin.Context) {
// 尝试提供静态文件
file := c.Request.URL.Path
@@ -164,7 +314,7 @@ func main() {
}
// 从static目录提供文件
c.File("./static" + file)
c.File(staticDir + file)
})
// 启动服务器

Binary file not shown.

Binary file not shown.

5040
backend/nohup.out Normal file

File diff suppressed because it is too large Load Diff

0
backend/server.log Normal file
View File

157
backend/start-monitor.sh Executable file
View File

@@ -0,0 +1,157 @@
#!/bin/bash
# 启动/停止/重启脚本
# ===================== 配置区 =====================
# 程序路径
AGENT_PATH="/root/monitor/monitor-agent"
# 日志文件路径
LOG_FILE="/root/monitor/agent.log"
# PID文件路径记录进程ID
PID_FILE="/root/monitor/agent.pid"
# 启动参数(根据实际需求调整)
START_ARGS=""
# ==================== 配置区结束 ====================
# 检查程序文件是否存在
check_agent_exists() {
if [ ! -f "${AGENT_PATH}" ]; then
echo "错误:程序文件 ${AGENT_PATH} 不存在!"
exit 1
fi
if [ ! -x "${AGENT_PATH}" ]; then
echo "错误:程序文件 ${AGENT_PATH} 没有执行权限,正在尝试添加..."
chmod +x "${AGENT_PATH}"
if [ $? -ne 0 ]; then
echo "错误:添加执行权限失败,请手动执行 chmod +x ${AGENT_PATH}"
exit 1
fi
fi
}
# 检查进程是否运行
check_running() {
if [ -f "${PID_FILE}" ]; then
PID=$(cat "${PID_FILE}")
if ps -p "${PID}" > /dev/null 2>&1; then
return 0 # 运行中
else
rm -f "${PID_FILE}" # PID文件存在但进程已死清理PID文件
fi
fi
return 1 # 未运行
}
# 启动程序
start_agent() {
if check_running; then
echo "✅ monitor-agent 已在运行PID: $(cat ${PID_FILE})"
return 0
fi
echo "🚀 正在启动 monitor-agent..."
# 创建日志目录(如果不存在)
mkdir -p "$(dirname ${LOG_FILE})"
# 后台启动程序重定向日志记录PID
nohup "${AGENT_PATH}" ${START_ARGS} > "${LOG_FILE}" 2>&1 &
AGENT_PID=$!
echo "${AGENT_PID}" > "${PID_FILE}"
# 等待2秒检查是否启动成功
sleep 2
if check_running; then
echo "✅ monitor-agent 启动成功PID: ${AGENT_PID}"
echo "日志文件:${LOG_FILE}"
else
echo "❌ monitor-agent 启动失败!请查看日志:${LOG_FILE}"
rm -f "${PID_FILE}"
exit 1
fi
}
# 停止程序
stop_agent() {
if ! check_running; then
echo " monitor-agent 未运行"
return 0
fi
PID=$(cat "${PID_FILE}")
echo "🛑 正在停止 monitor-agentPID: ${PID}..."
# 优雅停止先尝试TERM信号失败则强制KILL
kill "${PID}" > /dev/null 2>&1
sleep 3
if ps -p "${PID}" > /dev/null 2>&1; then
echo "⚠️ 优雅停止失败,强制杀死进程..."
kill -9 "${PID}" > /dev/null 2>&1
sleep 1
fi
# 清理PID文件
rm -f "${PID_FILE}"
echo "✅ monitor-agent 已停止"
}
# 查看状态
status_agent() {
if check_running; then
echo "✅ monitor-agent 运行中PID: $(cat ${PID_FILE})"
else
echo " monitor-agent 未运行"
fi
}
# 重启程序
restart_agent() {
echo "🔄 正在重启 monitor-agent..."
stop_agent
sleep 2
start_agent
}
# 帮助信息
show_help() {
echo "使用方法:$0 [start|stop|restart|status|help]"
echo " start - 启动 monitor-agent"
echo " stop - 停止 monitor-agent"
echo " restart - 重启 monitor-agent"
echo " status - 查看 monitor-agent 运行状态"
echo " help - 显示帮助信息"
}
# 主逻辑
main() {
# 检查是否为root用户可选根据需求调整
if [ "$(id -u)" -ne 0 ]; then
echo "警告建议使用root用户运行此脚本当前用户$(whoami)"
# exit 1 # 如果强制要求root取消注释
fi
check_agent_exists
case "$1" in
start)
start_agent
;;
stop)
stop_agent
;;
restart)
restart_agent
;;
status)
status_agent
;;
help|--help|-h)
show_help
;;
*)
echo "错误:无效参数 '$1'"
show_help
exit 1
;;
esac
}
# 执行主逻辑
main "$@"

View File

@@ -442,12 +442,10 @@
<!-- 磁盘详细信息展示 -->
<div id="diskDetailsContainer" class="mt-8 hidden">
<h3 class="text-lg font-semibold text-gray-900 mb-4">磁盘详细信息</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- 磁盘详细信息将通过JavaScript动态添加 -->
<div id="diskDetailsContent"></div>
<!-- 磁盘信息分页容器 -->
<div id="diskPaginationContainer" class="mt-4"></div>
</div>
<!-- 磁盘详细信息将通过JavaScript动态添加 -->
<div id="diskDetailsContent"></div>
<!-- 磁盘信息分页容器 -->
<div id="diskPaginationContainer" class="mt-4"></div>
</div>
<br>
<!-- 网络 图表 -->
@@ -490,6 +488,13 @@
</div>
</div>
<br>
<!-- 网卡详细信息展示 -->
<div id="networkInterfaceContainer" class="mt-8 hidden">
<h3 class="text-lg font-semibold text-gray-900 mb-4">网卡详细信息</h3>
<div id="networkInterfaceContent"></div>
<div id="networkInterfacePaginationContainer" class="mt-4"></div>
</div>
<br>
<!-- 系统日志 信息 -->
<div id="logInfoContainer" class="mt-8 hidden">
<h3 class="text-lg font-semibold text-gray-900 mb-4">系统日志</h3>
@@ -511,7 +516,7 @@
<!-- 日志信息分页容器 -->
<div id="logPaginationContainer" class="mt-4"></div>
</div>
<br>
<!-- 缩放控件已移动到各个图表容器内 -->
</div>
</div>

View File

@@ -1,5 +1,57 @@
// API 基础 URL
const API_BASE_URL = '/api';
// 自动检测当前页面的协议和域名构建完整的API URL
const API_BASE_URL = `${window.location.protocol}//${window.location.host}/api`;
// 带超时的fetch请求封装
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
// 通用的数据验证和转换函数
function validateAndTransformData(data, expectedType, defaultValue = null) {
// 检查数据类型是否匹配
if (typeof data === expectedType) {
return data;
}
// 特殊处理数组类型
if (expectedType === 'array' && Array.isArray(data)) {
return data;
}
// 特殊处理对象类型
if (expectedType === 'object' && typeof data === 'object' && data !== null) {
return data;
}
// 类型转换逻辑
switch (expectedType) {
case 'string':
// 将非字符串类型转换为字符串,布尔值转换为默认值
return typeof data === 'boolean' ? defaultValue : String(data);
case 'number':
// 将非数字类型转换为数字布尔值转换为0或1
const num = typeof data === 'boolean' ? (data ? 1 : 0) : Number(data);
return isNaN(num) ? defaultValue : num;
case 'boolean':
// 将非布尔值转换为布尔值
return Boolean(data);
case 'array':
// 将非数组转换为默认值
return defaultValue;
case 'object':
// 将非对象转换为默认值
return defaultValue;
default:
// 未知类型,返回默认值
return defaultValue;
}
}
// 全局状态
let state = {
@@ -207,23 +259,102 @@ async function loadHomeData() {
// 加载业务视图数据
async function loadBusinessViewData() {
// 显示加载状态
const tableBody = document.getElementById('businessViewTableBody');
if (tableBody) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-12 text-center text-gray-500">
<div class="flex items-center justify-center">
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
加载中...
</div>
</td>
</tr>
`;
}
try {
const response = await fetch(`${API_BASE_URL}/devices/`);
const response = await fetchWithTimeout(`${API_BASE_URL}/devices/`, {}, 3000);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
const devices = data.devices || [];
const devices = Array.isArray(data.devices) ? data.devices : [];
renderBusinessView(devices);
// 添加重试机制的fetch请求
const fetchWithRetry = async (url, options = {}, timeout = 5000, maxRetries = 1) => {
for (let i = 0; i <= maxRetries; i++) {
try {
return await fetchWithTimeout(url, options, timeout);
} catch (error) {
if (i === maxRetries) {
// 最后一次重试失败,抛出错误
throw error;
}
// 重试之间的延迟
await new Promise(resolve => setTimeout(resolve, 500));
}
}
};
// 为每个设备获取完整信息使用并行API调用优化加载速度
const devicesWithDetails = await Promise.allSettled(devices.map(async (device) => {
let ip = '未知';
let os = '未知';
// 获取设备详情包括IP地址最多重试1次
try {
const deviceDetailResponse = await fetchWithRetry(`${API_BASE_URL}/devices/${device.id}`, {}, 3000, 1);
if (deviceDetailResponse.ok) {
const deviceDetailData = await deviceDetailResponse.json();
if (deviceDetailData && deviceDetailData.device) {
ip = deviceDetailData.device.ip || '未知';
}
}
} catch (error) {
console.error(`获取设备 ${device.id} IP地址失败:`, error);
}
// 获取硬件信息包括操作系统最多重试1次
try {
const hardwareResponse = await fetchWithRetry(`${API_BASE_URL}/metrics/hardware?device_id=${device.id}`, {}, 8000, 1);
if (hardwareResponse.ok) {
const hardwareData = await hardwareResponse.json();
if (hardwareData && hardwareData.hardware && hardwareData.hardware.os) {
// 确保操作系统信息始终是字符串类型
const fullname = hardwareData.hardware.os.fullname;
os = typeof fullname === 'string' ? fullname : '未知';
}
}
} catch (error) {
console.error(`获取设备 ${device.id} 操作系统失败:`, error);
}
return {
...device,
ip: ip,
os: os
};
}));
// 处理Promise.allSettled的结果只保留成功的结果
const successfulDevices = devicesWithDetails
.filter(result => result.status === 'fulfilled')
.map(result => result.value);
renderBusinessView(successfulDevices);
} catch (error) {
console.error('加载业务视图数据失败:', error);
// 显示错误信息,不使用模拟数据
const tableBody = document.getElementById('businessViewTableBody');
// 显示错误信息
if (tableBody) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-red-500">加载业务视图数据失败</td>
<td colspan="4" class="px-6 py-8 text-center text-red-500">
<i class="fa fa-exclamation-circle text-xl mb-2"></i>
<p>加载业务视图数据失败</p>
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
</td>
</tr>
`;
}
@@ -237,12 +368,25 @@ function renderBusinessView(devices) {
tableBody.innerHTML = '';
devices.forEach(device => {
// 确保devices是数组类型
const deviceList = Array.isArray(devices) ? devices : [];
// 处理空数组情况
if (deviceList.length === 0) {
tableBody.innerHTML = `
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500">暂无设备数据</td>
</tr>
`;
return;
}
deviceList.forEach(device => {
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50 transition-colors cursor-pointer';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${device.name || '未知设备'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.ip || '未知'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${device.os || '未知'}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
@@ -253,7 +397,9 @@ function renderBusinessView(devices) {
// 为表格行添加点击事件
row.addEventListener('click', () => {
goToServerMonitor(device.id);
if (device.id) {
goToServerMonitor(device.id);
}
});
tableBody.appendChild(row);
@@ -3124,9 +3270,9 @@ function initChartTabs() {
tab.classList.add('active', 'text-blue-600', 'border-blue-600');
tab.classList.remove('text-gray-600', 'border-transparent');
// 隐藏所有图表容器
// 隐藏所有服务器监控页面中的图表容器
const tabId = tab.dataset.tab;
const chartContainers = document.querySelectorAll('.chart-container');
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
chartContainers.forEach(container => {
container.classList.add('hidden');
});
@@ -3140,10 +3286,11 @@ function initChartTabs() {
console.error(`Chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息系统日志
// 显示/隐藏进程信息、磁盘详细信息系统日志和网卡详细信息
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
@@ -3155,6 +3302,9 @@ function initChartTabs() {
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
@@ -3178,6 +3328,13 @@ function initChartTabs() {
// 加载系统日志
loadSystemLogs();
}
} else if (tabId === 'network' || tabId === 'speed') {
// 显示网卡详细信息
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.remove('hidden');
// 加载网卡详细信息
loadNetworkInterfaceDetails();
}
}
// 显示/隐藏网卡选择下拉框
@@ -3199,50 +3356,65 @@ function initChartTabs() {
if (activeTab) {
const tabId = activeTab.dataset.tab;
// 显示当前选中的图表容器
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
} else {
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
}
// 显示当前选中的图表容器,隐藏其他服务器监控图表容器
const chartContainers = document.querySelectorAll('#cpuChartContainer, #memoryChartContainer, #diskChartContainer, #networkChartContainer, #speedChartContainer');
chartContainers.forEach(container => {
container.classList.add('hidden');
});
const activeContainer = document.getElementById(`${tabId}ChartContainer`);
if (activeContainer) {
activeContainer.classList.remove('hidden');
} else {
console.error(`Initial chart container not found: ${tabId}ChartContainer`);
}
// 显示/隐藏进程信息、磁盘详细信息系统日志
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.add('hidden');
}
if (diskDetailsContainer) {
diskDetailsContainer.classList.add('hidden');
}
if (logInfoContainer) {
logInfoContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
// 显示/隐藏进程信息、磁盘详细信息系统日志和网卡详细信息
const processInfoContainer = document.getElementById('processInfoContainer');
const diskDetailsContainer = document.getElementById('diskDetailsContainer');
const logInfoContainer = document.getElementById('logInfoContainer');
const networkInterfaceContainer = document.getElementById('networkInterfaceContainer');
// 隐藏所有附加信息容器
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
processInfoContainer.classList.add('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
diskDetailsContainer.classList.add('hidden');
}
loadDiskDetails();
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
logInfoContainer.classList.add('hidden');
}
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.add('hidden');
}
// 根据选项卡显示相应的附加信息
if (tabId === 'cpu') {
// 显示进程信息
if (processInfoContainer) {
processInfoContainer.classList.remove('hidden');
}
loadProcessInfo();
} else if (tabId === 'disk') {
// 显示磁盘详细信息
if (diskDetailsContainer) {
diskDetailsContainer.classList.remove('hidden');
}
loadDiskDetails();
} else if (tabId === 'logs') {
// 显示系统日志
if (logInfoContainer) {
logInfoContainer.classList.remove('hidden');
}
loadSystemLogs();
} else if (tabId === 'network' || tabId === 'speed') {
// 显示网卡详细信息
if (networkInterfaceContainer) {
networkInterfaceContainer.classList.remove('hidden');
}
loadNetworkInterfaceDetails();
}
loadSystemLogs();
}
// 显示/隐藏网卡选择下拉框
if (interfaceContainer) {
@@ -3446,6 +3618,16 @@ let diskPagination = {
lastDeviceID: '' // 上次请求数据的设备ID
};
// 网卡信息分页状态
let networkInterfacePagination = {
currentPage: 1,
itemsPerPage: 5,
totalItems: 0,
totalPages: 0,
allInterfaces: [],
lastDeviceID: '' // 上次请求数据的设备ID
};
// 创建分页控件
function createPaginationControls(container, paginationState, loadFunction) {
// 清空容器
@@ -3585,35 +3767,79 @@ async function loadDiskDetails(page = 1) {
if (!diskDetailsContent) return;
// 显示加载状态
diskDetailsContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-12 text-center text-gray-500">
<div class="flex items-center justify-center">
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
加载磁盘详细信息中...
</div>
</div>
`;
try {
// 构建查询参数
const params = new URLSearchParams();
if (state.currentDeviceID) {
params.append('device_id', state.currentDeviceID);
if (!state.currentDeviceID) {
diskDetailsContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
<i class="fa fa-info-circle text-2xl mb-2"></i>
<p>请选择设备查看磁盘详细信息</p>
</div>
`;
if (diskPaginationContainer) {
diskPaginationContainer.innerHTML = '';
}
return;
}
// 检查设备ID是否变化
if (diskPagination.lastDeviceID !== state.currentDeviceID) {
// 设备ID变化清空旧数据
diskPagination.allDisks = [];
diskPagination.totalItems = 0;
diskPagination.totalPages = 0;
diskPagination.currentPage = 1;
}
// 检查是否已经有缓存数据,且设备ID没有变化
const shouldReload = !diskPagination.allDisks || diskPagination.lastDeviceID !== state.currentDeviceID;
// 如果是第一次加载或设备ID变化获取全部数据
if (diskPagination.allDisks.length === 0) {
// 发送请求
const response = await fetch(`${API_BASE_URL}/metrics/disk_details?${params.toString()}`);
if (shouldReload) {
// 使用hardware API获取磁盘详细信息
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
if (!response.ok) {
throw new Error('Failed to fetch disk details');
throw new Error('Failed to fetch hardware details');
}
const data = await response.json();
diskPagination.allDisks = data.data || [];
diskPagination.totalItems = diskPagination.allDisks.length;
let diskDetails = [];
// 提取磁盘详细信息并进行严格的数据验证
if (data.hardware && Array.isArray(data.hardware.disk_details)) {
diskDetails = data.hardware.disk_details.map(disk => {
// 确保每个磁盘信息是对象类型
if (typeof disk !== 'object' || disk === null) {
return null;
}
// 验证和转换各个字段
return {
id: typeof disk.id === 'string' ? disk.id : Math.random().toString(36).substr(2, 9),
// 确保状态是字符串类型,不是布尔值
status: typeof disk.status === 'boolean' ? 'N/A' : (disk.status || 'N/A'),
// 确保路径是字符串类型
path: typeof disk.path === 'string' ? disk.path : 'N/A',
// 确保类型是字符串类型
type: typeof disk.type === 'boolean' ? 'N/A' : (disk.type || 'N/A'),
// 确保大小是数字类型,不是布尔值
size_gb: typeof disk.size_gb === 'boolean' ? 0 : (disk.size_gb || 0),
// 确保文件系统是字符串类型
file_system: typeof disk.file_system === 'string' ? disk.file_system : 'N/A',
// 确保厂商是字符串类型
vendor: typeof disk.vendor === 'string' ? disk.vendor : 'N/A',
// 确保型号是字符串类型
model: typeof disk.model === 'string' ? disk.model : 'N/A',
// 确保接口类型是字符串类型
interface_type: typeof disk.interface_type === 'boolean' ? 'N/A' : (disk.interface_type || 'N/A')
};
}).filter(Boolean); // 过滤掉null值
}
// 更新分页信息
diskPagination.allDisks = diskDetails || [];
diskPagination.totalItems = diskDetails.length;
diskPagination.totalPages = Math.ceil(diskPagination.totalItems / diskPagination.itemsPerPage);
// 更新上次请求数据的设备ID
diskPagination.lastDeviceID = state.currentDeviceID;
}
@@ -3645,52 +3871,121 @@ async function loadDiskDetails(page = 1) {
const endIndex = Math.min(startIndex + diskPagination.itemsPerPage, diskPagination.totalItems);
const paginatedDisks = diskPagination.allDisks.slice(startIndex, endIndex);
// 填充磁盘详细信息卡片
// 创建磁盘详细信息容器
const container = document.createElement('div');
container.className = 'space-y-4';
// 创建响应式表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'overflow-x-auto';
// 创建表格
const table = document.createElement('table');
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
// 创建表头
const thead = document.createElement('thead');
thead.className = 'bg-gray-50';
thead.innerHTML = `
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">状态</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">路径</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">类型</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">大小 (GB)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">文件系统</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">厂商</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">型号</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">接口类型</th>
</tr>
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
tbody.className = 'divide-y divide-gray-200';
paginatedDisks.forEach(disk => {
const diskCard = document.createElement('div');
diskCard.className = 'bg-white rounded-lg shadow-md p-6 border border-gray-100';
diskCard.innerHTML = `
<h4 class="text-md font-semibold text-gray-900 mb-3">${disk.device_id || 'Unknown Device'}</h4>
<div class="space-y-2 text-sm">
<div class="flex justify-between">
<span class="text-gray-500">状态:</span>
<span class="text-gray-900 font-medium">${disk.status || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">类型:</span>
<span class="text-gray-900 font-medium">${disk.type || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">大小:</span>
<span class="text-gray-900 font-medium">${disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00'} GB</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">型号:</span>
<span class="text-gray-900 font-medium">${disk.model || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">接口类型:</span>
<span class="text-gray-900 font-medium">${disk.interface_type || 'Unknown'}</span>
</div>
<div class="flex justify-between">
<span class="text-gray-500">描述:</span>
<span class="text-gray-900 font-medium truncate max-w-xs">${disk.description || 'Unknown'}</span>
</div>
</div>
// 跳过无效的磁盘数据
if (!disk || !disk.id) return;
// 格式化数据空值显示为N/A
const status = disk.status || 'N/A';
const path = disk.path || 'N/A';
const type = disk.type || 'N/A';
const size = disk.size_gb ? parseFloat(disk.size_gb).toFixed(2) : '0.00';
const fileSystem = disk.file_system || 'N/A';
const vendor = disk.vendor || 'N/A';
const model = disk.model || 'N/A';
const interfaceType = disk.interface_type || 'N/A';
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${status}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${path}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${type}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${size}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${fileSystem}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${vendor}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${model}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${interfaceType}</td>
`;
diskDetailsContent.appendChild(diskCard);
tbody.appendChild(row);
});
// 创建分页控件
table.appendChild(tbody);
tableContainer.appendChild(table);
container.appendChild(tableContainer);
// 创建分页信息和控件容器
const paginationContainer = document.createElement('div');
paginationContainer.className = 'flex justify-between items-center';
// 创建每页显示数量选择
const pageSizeDiv = document.createElement('div');
pageSizeDiv.className = 'flex items-center gap-2';
pageSizeDiv.innerHTML = `
<label for="diskPageSize" class="text-sm text-gray-600">每页显示:</label>
<select id="diskPageSize" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="5" ${diskPagination.itemsPerPage === 5 ? 'selected' : ''}>5条</option>
<option value="10" ${diskPagination.itemsPerPage === 10 ? 'selected' : ''}>10条</option>
<option value="20" ${diskPagination.itemsPerPage === 20 ? 'selected' : ''}>20条</option>
<option value="50" ${diskPagination.itemsPerPage === 50 ? 'selected' : ''}>50条</option>
</select>
`;
// 添加每页显示数量变化事件
const pageSizeSelect = pageSizeDiv.querySelector('#diskPageSize');
pageSizeSelect.addEventListener('change', (e) => {
const newPageSize = parseInt(e.target.value, 10);
if (newPageSize !== diskPagination.itemsPerPage) {
diskPagination.itemsPerPage = newPageSize;
loadDiskDetails(1); // 重置为第一页
}
});
paginationContainer.appendChild(pageSizeDiv);
// 创建显示当前页和总条数的文本
const infoText = document.createElement('div');
infoText.className = 'text-sm text-gray-500';
infoText.textContent = `显示 ${startIndex + 1}${endIndex},共 ${diskPagination.totalItems}`;
paginationContainer.appendChild(infoText);
// 添加分页控件
if (diskPaginationContainer) {
createPaginationControls(diskPaginationContainer, diskPagination, loadDiskDetails);
}
container.appendChild(paginationContainer);
diskDetailsContent.appendChild(container);
} catch (error) {
console.error('Error loading disk details:', error);
diskDetailsContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-red-500">
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
<p>加载磁盘详细信息失败</p>
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
</div>
`;
@@ -3700,6 +3995,222 @@ async function loadDiskDetails(page = 1) {
}
}
// 加载网卡详细信息
async function loadNetworkInterfaceDetails(page = 1) {
const networkInterfaceContent = document.getElementById('networkInterfaceContent');
const networkInterfacePaginationContainer = document.getElementById('networkInterfacePaginationContainer');
if (!networkInterfaceContent) return;
// 显示加载状态
networkInterfaceContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-12 text-center text-gray-500">
<div class="flex items-center justify-center">
<i class="fa fa-spinner fa-spin text-xl mr-2"></i>
加载网卡详细信息中...
</div>
</div>
`;
try {
if (!state.currentDeviceID) {
networkInterfaceContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
<i class="fa fa-info-circle text-2xl mb-2"></i>
<p>请选择设备查看网卡详细信息</p>
</div>
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
return;
}
// 检查是否已经有缓存数据且设备ID没有变化
const shouldReload = !networkInterfacePagination.allInterfaces || networkInterfacePagination.lastDeviceID !== state.currentDeviceID;
if (shouldReload) {
// 使用hardware API获取网卡详细信息
const response = await fetchWithTimeout(`${API_BASE_URL}/metrics/hardware?device_id=${state.currentDeviceID}`, {}, 8000);
if (!response.ok) {
throw new Error('Failed to fetch hardware details');
}
const data = await response.json();
let networkInterfaces = [];
// 提取网卡详细信息并进行严格的数据验证
if (data.hardware && Array.isArray(data.hardware.network_cards)) {
networkInterfaces = data.hardware.network_cards.map(iface => {
// 确保每个网卡信息是对象类型
if (typeof iface !== 'object' || iface === null) {
return null;
}
// 验证和转换各个字段
return {
name: typeof iface.name === 'string' ? iface.name : 'N/A',
mac: typeof iface.mac === 'string' ? iface.mac : 'N/A',
// 确保ip_addresses始终是数组类型
ip_addresses: Array.isArray(iface.ip_addresses) ? iface.ip_addresses : [],
// 确保mtu是数字或字符串不是布尔值
mtu: typeof iface.mtu === 'boolean' ? 'N/A' : (iface.mtu || 'N/A'),
// 确保speed是数字或字符串不是布尔值
speed: typeof iface.speed === 'boolean' ? 'N/A' : (iface.speed || 'N/A')
};
}).filter(Boolean); // 过滤掉null值
}
// 更新分页信息
networkInterfacePagination.allInterfaces = networkInterfaces || [];
networkInterfacePagination.totalItems = networkInterfaces.length;
networkInterfacePagination.totalPages = Math.ceil(networkInterfacePagination.totalItems / networkInterfacePagination.itemsPerPage);
networkInterfacePagination.lastDeviceID = state.currentDeviceID;
}
// 更新当前页码
networkInterfacePagination.currentPage = page;
// 清空内容
networkInterfaceContent.innerHTML = '';
if (networkInterfacePagination.totalItems === 0) {
// 没有网卡数据,显示提示
networkInterfaceContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-gray-500">
<i class="fa fa-wifi text-2xl mb-2"></i>
<p>暂无网卡详细信息</p>
</div>
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
return;
}
// 计算分页数据
const startIndex = (networkInterfacePagination.currentPage - 1) * networkInterfacePagination.itemsPerPage;
const endIndex = Math.min(startIndex + networkInterfacePagination.itemsPerPage, networkInterfacePagination.totalItems);
const paginatedInterfaces = networkInterfacePagination.allInterfaces.slice(startIndex, endIndex);
// 创建网卡详细信息容器
const container = document.createElement('div');
container.className = 'space-y-4';
// 创建响应式表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'overflow-x-auto';
// 创建表格
const table = document.createElement('table');
table.className = 'w-full bg-white border border-gray-200 rounded-lg';
// 创建表头
const thead = document.createElement('thead');
thead.className = 'bg-gray-50';
thead.innerHTML = `
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">名称</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MAC地址</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">IP地址</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">MTU</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">速度</th>
</tr>
`;
table.appendChild(thead);
// 创建表体
const tbody = document.createElement('tbody');
tbody.className = 'divide-y divide-gray-200';
paginatedInterfaces.forEach(iface => {
// 跳过无效的网卡数据
if (!iface) return;
// 格式化数据空值显示为N/A
const name = iface.name || 'N/A';
const mac = iface.mac || 'N/A';
const ipAddresses = iface.ip_addresses ? iface.ip_addresses.join(', ') : 'N/A';
const mtu = iface.mtu || 'N/A';
const speed = iface.speed || 'N/A';
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${name}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${mac}</td>
<td class="px-6 py-4 text-sm text-gray-900">${ipAddresses}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${mtu}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${speed}</td>
`;
tbody.appendChild(row);
});
table.appendChild(tbody);
tableContainer.appendChild(table);
container.appendChild(tableContainer);
// 创建分页信息和控件容器
const paginationContainer = document.createElement('div');
paginationContainer.className = 'flex justify-between items-center';
// 创建每页显示数量选择
const pageSizeDiv = document.createElement('div');
pageSizeDiv.className = 'flex items-center gap-2';
pageSizeDiv.innerHTML = `
<label for="networkInterfacePageSize" class="text-sm text-gray-600">每页显示:</label>
<select id="networkInterfacePageSize" class="px-2 py-1 border border-gray-300 rounded text-sm">
<option value="5" ${networkInterfacePagination.itemsPerPage === 5 ? 'selected' : ''}>5条</option>
<option value="10" ${networkInterfacePagination.itemsPerPage === 10 ? 'selected' : ''}>10条</option>
<option value="20" ${networkInterfacePagination.itemsPerPage === 20 ? 'selected' : ''}>20条</option>
<option value="50" ${networkInterfacePagination.itemsPerPage === 50 ? 'selected' : ''}>50条</option>
</select>
`;
// 添加每页显示数量变化事件
const pageSizeSelect = pageSizeDiv.querySelector('#networkInterfacePageSize');
pageSizeSelect.addEventListener('change', (e) => {
const newPageSize = parseInt(e.target.value, 10);
if (newPageSize !== networkInterfacePagination.itemsPerPage) {
networkInterfacePagination.itemsPerPage = newPageSize;
loadNetworkInterfaceDetails(1); // 重置为第一页
}
});
paginationContainer.appendChild(pageSizeDiv);
// 创建显示当前页和总条数的文本
const infoText = document.createElement('div');
infoText.className = 'text-sm text-gray-500';
infoText.textContent = `显示 ${startIndex + 1}${endIndex},共 ${networkInterfacePagination.totalItems}`;
paginationContainer.appendChild(infoText);
// 添加分页控件
if (networkInterfacePaginationContainer) {
createPaginationControls(networkInterfacePaginationContainer, networkInterfacePagination, loadNetworkInterfaceDetails);
}
container.appendChild(paginationContainer);
networkInterfaceContent.appendChild(container);
} catch (error) {
console.error('Error loading network interface details:', error);
networkInterfaceContent.innerHTML = `
<div class="col-span-full bg-white rounded-lg shadow-md p-6 text-center text-red-500">
<i class="fa fa-exclamation-circle text-2xl mb-2"></i>
<p>加载网卡详细信息失败</p>
<p class="text-sm text-gray-500 mt-1">${error.message}</p>
</div>
`;
if (networkInterfacePaginationContainer) {
networkInterfacePaginationContainer.innerHTML = '';
}
}
}
// 格式化时间为易识别的日期样式
function formatLogTime(timeString) {
if (!timeString) return new Date().toLocaleString('zh-CN');