/** * Main Application - Data Visualization Display Wall */ (function () { 'use strict'; // ---- Config ---- const REFRESH_INTERVAL = 5000; // 5 seconds const NETWORK_HISTORY_INTERVAL = 60000; // 1 minute // ---- DOM Elements ---- const dom = { clock: document.getElementById('clock'), serverCountText: document.getElementById('serverCountText'), sourceCount: document.getElementById('sourceCount'), totalServers: document.getElementById('totalServers'), cpuPercent: document.getElementById('cpuPercent'), cpuDetail: document.getElementById('cpuDetail'), memPercent: document.getElementById('memPercent'), memDetail: document.getElementById('memDetail'), diskPercent: document.getElementById('diskPercent'), diskDetail: document.getElementById('diskDetail'), totalBandwidth: document.getElementById('totalBandwidth'), bandwidthDetail: document.getElementById('bandwidthDetail'), traffic24hRx: document.getElementById('traffic24hRx'), traffic24hTx: document.getElementById('traffic24hTx'), traffic24hTotal: document.getElementById('traffic24hTotal'), networkCanvas: document.getElementById('networkCanvas'), gaugeCpuFill: document.getElementById('gaugeCpuFill'), gaugeRamFill: document.getElementById('gaugeRamFill'), gaugeDiskFill: document.getElementById('gaugeDiskFill'), gaugeCpuValue: document.getElementById('gaugeCpuValue'), gaugeRamValue: document.getElementById('gaugeRamValue'), gaugeDiskValue: document.getElementById('gaugeDiskValue'), serverTableBody: document.getElementById('serverTableBody'), btnSettings: document.getElementById('btnSettings'), settingsModal: document.getElementById('settingsModal'), modalClose: document.getElementById('modalClose'), sourceName: document.getElementById('sourceName'), sourceUrl: document.getElementById('sourceUrl'), sourceDesc: document.getElementById('sourceDesc'), btnTest: document.getElementById('btnTest'), btnAdd: document.getElementById('btnAdd'), formMessage: document.getElementById('formMessage'), sourceItems: document.getElementById('sourceItems') }; // ---- State ---- let previousMetrics = null; let networkChart = null; // ---- Initialize ---- function init() { // Add SVG gradient definitions for gauges addGaugeSvgDefs(); // Clock updateClock(); setInterval(updateClock, 1000); // Network chart networkChart = new AreaChart(dom.networkCanvas); // Event listeners dom.btnSettings.addEventListener('click', openSettings); dom.modalClose.addEventListener('click', closeSettings); dom.settingsModal.addEventListener('click', (e) => { if (e.target === dom.settingsModal) closeSettings(); }); dom.btnTest.addEventListener('click', testConnection); dom.btnAdd.addEventListener('click', addSource); // Keyboard shortcut document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeSettings(); }); // Start data fetching fetchMetrics(); fetchNetworkHistory(); setInterval(fetchMetrics, REFRESH_INTERVAL); setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL); } // ---- Add SVG Gradient Defs ---- function addGaugeSvgDefs() { const svgs = document.querySelectorAll('.gauge svg'); const gradients = [ { id: 'gaugeCpuGrad', colors: ['#6366f1', '#818cf8'] }, { id: 'gaugeRamGrad', colors: ['#06b6d4', '#22d3ee'] }, { id: 'gaugeDiskGrad', colors: ['#f59e0b', '#fbbf24'] } ]; svgs.forEach((svg, i) => { const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); const grad = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient'); grad.setAttribute('id', gradients[i].id); grad.setAttribute('x1', '0%'); grad.setAttribute('y1', '0%'); grad.setAttribute('x2', '100%'); grad.setAttribute('y2', '100%'); gradients[i].colors.forEach((color, ci) => { const stop = document.createElementNS('http://www.w3.org/2000/svg', 'stop'); stop.setAttribute('offset', ci === 0 ? '0%' : '100%'); stop.setAttribute('stop-color', color); grad.appendChild(stop); }); defs.appendChild(grad); svg.insertBefore(defs, svg.firstChild); }); } // ---- Clock ---- function updateClock() { dom.clock.textContent = formatClock(); } // ---- Fetch Metrics ---- async function fetchMetrics() { try { const response = await fetch('/api/metrics/overview'); const data = await response.json(); updateDashboard(data); } catch (err) { console.error('Error fetching metrics:', err); } } // ---- Update Dashboard ---- function updateDashboard(data) { // Server count dom.totalServers.textContent = data.totalServers; dom.serverCountText.textContent = `${data.totalServers} 台服务器`; // CPU const cpuPct = data.cpu.percent; dom.cpuPercent.textContent = formatPercent(cpuPct); dom.cpuDetail.textContent = `${data.cpu.used.toFixed(1)} / ${data.cpu.total.toFixed(0)} 核心`; // Memory const memPct = data.memory.percent; dom.memPercent.textContent = formatPercent(memPct); dom.memDetail.textContent = `${formatBytes(data.memory.used)} / ${formatBytes(data.memory.total)}`; // Disk const diskPct = data.disk.percent; dom.diskPercent.textContent = formatPercent(diskPct); dom.diskDetail.textContent = `${formatBytes(data.disk.used)} / ${formatBytes(data.disk.total)}`; // Bandwidth dom.totalBandwidth.textContent = formatBandwidth(data.network.totalBandwidth); dom.bandwidthDetail.textContent = `↓ ${formatBandwidth(data.network.rx)} ↑ ${formatBandwidth(data.network.tx)}`; // 24h traffic dom.traffic24hRx.textContent = formatBytes(data.traffic24h.rx); dom.traffic24hTx.textContent = formatBytes(data.traffic24h.tx); dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total); // Update gauges updateGauge(dom.gaugeCpuFill, dom.gaugeCpuValue, cpuPct); updateGauge(dom.gaugeRamFill, dom.gaugeRamValue, memPct); updateGauge(dom.gaugeDiskFill, dom.gaugeDiskValue, diskPct); // Update server table updateServerTable(data.servers); // Flash animation if (previousMetrics) { [dom.cpuPercent, dom.memPercent, dom.diskPercent, dom.totalBandwidth].forEach(el => { el.classList.remove('value-update'); void el.offsetWidth; // Force reflow el.classList.add('value-update'); }); } previousMetrics = data; } // ---- Gauge Update ---- const CIRCUMFERENCE = 2 * Math.PI * 52; // r=52 function updateGauge(fillEl, valueEl, percent) { const clamped = Math.min(100, Math.max(0, percent)); const offset = CIRCUMFERENCE - (clamped / 100) * CIRCUMFERENCE; fillEl.style.strokeDashoffset = offset; valueEl.textContent = formatPercent(clamped); // Change color based on usage const color = getUsageColor(clamped); // We keep gradient but could override for critical } // ---- Server Table ---- function updateServerTable(servers) { if (!servers || servers.length === 0) { dom.serverTableBody.innerHTML = `