UNPKG

@hadesz/monitor

Version:

A complete server monitoring system with agents, server and dashboard

508 lines (462 loc) 14.3 kB
class Dashboard { constructor() { this.charts = {}; this.currentAgent = ""; this.data = []; this.init(); } init() { this.createCharts(); this.createInfoCards(); this.loadAgents(); this.startAutoRefresh(); this.updateCurrentTime(); } // 创建系统信息卡片 createInfoCards() { const infoGrid = document.getElementById("systemInfo"); const infoCards = [ { id: "uptime", title: "运行时间", value: "0小时", label: "系统运行时间" }, { id: "dockerStatus", title: "Docker 状态", value: "未运行", label: "Docker 服务状态" }, { id: "containers", title: "容器总数", value: "0", label: "Docker 容器" }, { id: "runningContainers", title: "运行中", value: "0", label: "运行中的容器" }, { id: "processes", title: "进程数", value: "0", label: "系统进程" }, { id: "load", title: "系统负载", value: "0.00", label: "最近1分钟" } ]; infoCards.forEach((card) => { const cardElement = document.createElement("div"); cardElement.className = "info-card"; cardElement.id = `card-${card.id}`; cardElement.innerHTML = ` <h4>${card.title}</h4> <div class="info-value">${card.value}</div> <div class="info-label">${card.label}</div> `; infoGrid.appendChild(cardElement); }); } createCharts() { const chartConfigs = { cpuChart: { label: "CPU使用率", color: "rgba(255, 99, 132, 1)" }, memoryChart: { label: "内存使用率", color: "rgba(54, 162, 235, 1)" }, diskChart: { label: "磁盘使用率", color: "rgba(255, 206, 86, 1)" }, loadChart: { labels: ["1分钟", "5分钟", "15分钟"], colors: [ "rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)", "rgba(255, 206, 86, 1)" ] }, diskIOChart: { labels: ["读取", "写入"], colors: ["rgba(75, 192, 192, 1)", "rgba(153, 102, 255, 1)"] }, networkChart: { labels: ["接收", "发送"], colors: ["rgba(255, 159, 64, 1)", "rgba(199, 199, 199, 1)"] }, dockerContainersChart: { type: "doughnut", labels: ["运行中", "已暂停", "已停止"], colors: [ "rgba(75, 192, 192, 1)", "rgba(255, 205, 86, 1)", "rgba(255, 99, 132, 1)" ] }, processesChart: { type: "bar", labels: ["运行中", "睡眠中", "总计"], colors: [ "rgba(75, 192, 192, 1)", "rgba(54, 162, 235, 1)", "rgba(255, 206, 86, 1)" ] } }; Object.keys(chartConfigs).forEach((chartId) => { const ctx = document.getElementById(chartId).getContext("2d"); const config = chartConfigs[chartId]; if (chartId === "dockerContainersChart") { this.charts[chartId] = new Chart(ctx, { type: "doughnut", data: { labels: config.labels, datasets: [ { data: [0, 0, 0], backgroundColor: config.colors, borderColor: config.colors.map((color) => color.replace("1)", "1)") ), borderWidth: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom" }, tooltip: { callbacks: { label: function (context) { const label = context.label || ""; const value = context.raw || 0; const total = context.dataset.data.reduce( (a, b) => a + b, 0 ); const percentage = total > 0 ? Math.round((value / total) * 100) : 0; return `${label}: ${value} (${percentage}%)`; } } } } } }); } else if (chartId === "processesChart") { this.charts[chartId] = new Chart(ctx, { type: "bar", data: { labels: config.labels, datasets: [ { label: "进程数量", data: [0, 0, 0], backgroundColor: config.colors, borderColor: config.colors.map((color) => color.replace("1)", "1)") ), borderWidth: 1 } ] }, options: this.getChartOptions("数量") }); } else if (chartId === "loadChart") { this.charts[chartId] = new Chart(ctx, { type: "line", data: { labels: [], datasets: config.labels.map((label, i) => ({ label: label, data: [], borderColor: config.colors[i], backgroundColor: config.colors[i].replace("1)", "0.1)"), tension: 0.4 })) }, options: this.getChartOptions("负载") }); } else if (chartId === "diskIOChart" || chartId === "networkChart") { this.charts[chartId] = new Chart(ctx, { type: "line", data: { labels: [], datasets: config.labels.map((label, i) => ({ label: label, data: [], borderColor: config.colors[i], backgroundColor: config.colors[i].replace("1)", "0.1)"), tension: 0.4 })) }, options: this.getChartOptions("KB/s") }); } else { this.charts[chartId] = new Chart(ctx, { type: "line", data: { labels: [], datasets: [ { label: config.label, data: [], borderColor: config.color, backgroundColor: config.color.replace("1)", "0.1)"), tension: 0.4 } ] }, options: this.getChartOptions("%") }); } }); } getChartOptions(unit) { return { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, title: { display: true, text: unit } }, x: { title: { display: true, text: "时间" } } }, plugins: { legend: { display: true } } }; } async loadAgents() { try { const response = await fetch("/api/agents"); const agents = await response.json(); const select = document.getElementById("agentSelect"); select.innerHTML = ""; agents.forEach((agent) => { const option = document.createElement("option"); option.value = agent; option.textContent = agent; select.appendChild(option); }); if (agents.length > 0) { this.currentAgent = agents[0]; this.loadData(); } select.onchange = (e) => { this.currentAgent = e.target.value; this.loadData(); }; } catch (error) { console.error("Failed to load agents:", error); } } async loadData() { if (!this.currentAgent) return; try { const timeRange = document.getElementById("timeRange").value; const response = await fetch( `/api/metrics/${this.currentAgent}?hours=${timeRange}` ); this.data = (await response.json())?.reverse(); this.updateCharts(); this.updateStatus(); this.updateInfoCards(); } catch (error) { console.error("Failed to load data:", error); } } updateCharts() { if (this.data.length === 0) return; const labels = this.data.map((d) => new Date(d.timestamp).toLocaleTimeString() ); // CPU 图表 this.updateChart( "cpuChart", labels, this.data.map((d) => d.cpu?.usage?.toFixed(1)) ); // 内存图表 this.updateChart( "memoryChart", labels, this.data.map((d) => d.memory?.usage?.toFixed(1)) ); // 磁盘图表 this.updateChart( "diskChart", labels, this.data.map((d) => d.disk?.usage?.toFixed(1)) ); // 负载图表 if (this.charts.loadChart) { this.charts.loadChart.data.labels = labels; this.charts.loadChart.data.datasets[0].data = this.data.map((d) => d.cpu?.load1?.toFixed(2) ); this.charts.loadChart.data.datasets[1].data = this.data.map((d) => d.cpu?.load5?.toFixed(2) ); this.charts.loadChart.data.datasets[2].data = this.data.map((d) => d.cpu?.load15?.toFixed(2) ); this.charts.loadChart.update(); } // 磁盘IO图表 if (this.charts.diskIOChart) { this.charts.diskIOChart.data.labels = labels; this.charts.diskIOChart.data.datasets[0].data = this.data.map( (d) => d.disk?.read || 0 ); this.charts.diskIOChart.data.datasets[1].data = this.data.map( (d) => d.disk?.write || 0 ); this.charts.diskIOChart.update(); } // 网络图表 if (this.charts.networkChart) { this.charts.networkChart.data.labels = labels; this.charts.networkChart.data.datasets[0].data = this.data.map( (d) => d.network?.in || 0 ); this.charts.networkChart.data.datasets[1].data = this.data.map( (d) => d.network?.out || 0 ); this.charts.networkChart.update(); } // Docker 容器状态图表(使用最新数据) this.updateDockerChart(); // 进程状态图表(使用最新数据) this.updateProcessesChart(); } updateChart(chartId, labels, data) { if (this.charts[chartId]) { this.charts[chartId].data.labels = labels; this.charts[chartId].data.datasets[0].data = data; this.charts[chartId].update(); } } // 更新 Docker 图表(使用最新数据点) updateDockerChart() { if (this.data.length === 0 || !this.charts.dockerContainersChart) return; const latest = this.data[this.data.length - 1]; if (latest.docker) { const dockerData = latest.docker; this.charts.dockerContainersChart.data.datasets[0].data = [ dockerData.running || 0, dockerData.paused || 0, dockerData.stopped || 0 ]; this.charts.dockerContainersChart.update(); // 显示 Docker 图表 document.getElementById( "dockerContainersChart" ).parentElement.style.display = "block"; } else { // 隐藏 Docker 图表(如果没有 Docker 数据) document.getElementById( "dockerContainersChart" ).parentElement.style.display = "none"; } } // 更新进程状态图表(使用最新数据点) updateProcessesChart() { if (this.data.length === 0 || !this.charts.processesChart) return; const latest = this.data[this.data.length - 1]; if (latest.processes) { const processesData = latest.processes; this.charts.processesChart.data.datasets[0].data = [ processesData.running || 0, processesData.sleeping || 0, processesData.total || 0 ]; this.charts.processesChart.update(); } } // 更新系统信息卡片 updateInfoCards() { if (this.data.length === 0) return; const latest = this.data[this.data.length - 1]; // 运行时间 if (latest.uptime) { const hours = Math.round(latest.uptime / 3600); this.updateInfoCard("uptime", `${hours}小时`); } // Docker 状态和容器信息 if (latest.docker) { this.updateInfoCard("dockerStatus", "运行中"); this.updateInfoCard( "containers", latest.docker.containers?.toString() || "0" ); this.updateInfoCard( "runningContainers", latest.docker.running?.toString() || "0" ); } else { this.updateInfoCard("dockerStatus", "未运行"); this.updateInfoCard("containers", "0"); this.updateInfoCard("runningContainers", "0"); } // 进程信息 if (latest.processes) { this.updateInfoCard( "processes", latest.processes.total?.toString() || "0" ); } // 系统负载 if (latest.cpu && latest.cpu.load1) { this.updateInfoCard("load", latest.cpu.load1.toFixed(2)); } } updateInfoCard(cardId, value) { const cardElement = document.getElementById(`card-${cardId}`); if (cardElement) { const valueElement = cardElement.querySelector(".info-value"); if (valueElement) { valueElement.textContent = value; } } } updateStatus() { const statusElement = document.getElementById("status"); if (this.data.length > 0) { const latest = this.data[this.data.length - 1]; const timeDiff = Date.now() - new Date(latest.timestamp).getTime(); const isOnline = timeDiff < 120000; // 2分钟内更新视为在线 statusElement.innerHTML = ` <span class="status-indicator ${ isOnline ? "status-online" : "status-offline" }"></span> 状态: ${isOnline ? "在线" : "离线"} | 最后更新: ${new Date(latest.timestamp).toLocaleString()} | 运行时间: ${Math.round(latest.uptime / 3600)}小时 `; } else { statusElement.innerHTML = '<span class="status-indicator status-offline"></span>无数据'; } } updateCurrentTime() { document.getElementById("currentTime").textContent = new Date().toLocaleString(); setTimeout(() => this.updateCurrentTime(), 1000); } startAutoRefresh() { setInterval(() => { if (this.currentAgent) { this.loadData(); } }, 10000); // 10秒自动刷新 } } // 全局函数供HTML调用 function refreshData() { dashboard.loadData(); } // 初始化Dashboard const dashboard = new Dashboard();