/** * 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 = ` 暂无数据 - 请先配置 Prometheus 数据源 `; return; } // Sort servers: online first, then by cpu usage servers.sort((a, b) => { if (a.up !== b.up) return b.up ? 1 : -1; return b.cpuPercent - a.cpuPercent; }); dom.serverTableBody.innerHTML = servers.map(server => { const memPct = server.memTotal > 0 ? (server.memUsed / server.memTotal * 100) : 0; const diskPct = server.diskTotal > 0 ? (server.diskUsed / server.diskTotal * 100) : 0; return ` ${escapeHtml(server.instance)} ${escapeHtml(server.source)}
${formatPercent(server.cpuPercent)}
${formatPercent(memPct)}
${formatPercent(diskPct)}
${formatBandwidth(server.netRx)} ${formatBandwidth(server.netTx)} `; }).join(''); } // ---- Network History ---- async function fetchNetworkHistory() { try { const response = await fetch('/api/metrics/network-history'); const data = await response.json(); networkChart.setData(data); } catch (err) { console.error('Error fetching network history:', err); } } // ---- Settings Modal ---- function openSettings() { dom.settingsModal.classList.add('active'); loadSources(); } function closeSettings() { dom.settingsModal.classList.remove('active'); hideMessage(); } async function loadSources() { try { const response = await fetch('/api/sources'); const sources = await response.json(); dom.sourceCount.textContent = `${sources.length} 个数据源`; renderSources(sources); } catch (err) { console.error('Error loading sources:', err); } } function renderSources(sources) { if (sources.length === 0) { dom.sourceItems.innerHTML = '
暂无数据源
'; return; } dom.sourceItems.innerHTML = sources.map(source => `
${escapeHtml(source.name)} ${source.status === 'online' ? '在线' : '离线'}
${escapeHtml(source.url)}
${source.description ? `
${escapeHtml(source.description)}
` : ''}
`).join(''); } // ---- Test Connection ---- async function testConnection() { const url = dom.sourceUrl.value.trim(); if (!url) { showMessage('请输入 Prometheus URL', 'error'); return; } dom.btnTest.textContent = '测试中...'; dom.btnTest.disabled = true; try { const response = await fetch('/api/sources/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }) }); const data = await response.json(); if (data.status === 'ok') { showMessage(`连接成功!Prometheus 版本: ${data.version}`, 'success'); } else { showMessage(`连接失败: ${data.message}`, 'error'); } } catch (err) { showMessage(`连接失败: ${err.message}`, 'error'); } finally { dom.btnTest.textContent = '测试连接'; dom.btnTest.disabled = false; } } // ---- Add Source ---- async function addSource() { const name = dom.sourceName.value.trim(); const url = dom.sourceUrl.value.trim(); const description = dom.sourceDesc.value.trim(); if (!name || !url) { showMessage('请填写名称和URL', 'error'); return; } dom.btnAdd.textContent = '添加中...'; dom.btnAdd.disabled = true; try { const response = await fetch('/api/sources', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, url, description }) }); if (response.ok) { showMessage('数据源添加成功', 'success'); dom.sourceName.value = ''; dom.sourceUrl.value = ''; dom.sourceDesc.value = ''; loadSources(); // Refresh metrics immediately fetchMetrics(); fetchNetworkHistory(); } else { const err = await response.json(); showMessage(`添加失败: ${err.error}`, 'error'); } } catch (err) { showMessage(`添加失败: ${err.message}`, 'error'); } finally { dom.btnAdd.textContent = '添加'; dom.btnAdd.disabled = false; } } // ---- Delete Source ---- window.deleteSource = async function (id) { if (!confirm('确定要删除这个数据源吗?')) return; try { const response = await fetch(`/api/sources/${id}`, { method: 'DELETE' }); if (response.ok) { loadSources(); fetchMetrics(); fetchNetworkHistory(); } } catch (err) { console.error('Error deleting source:', err); } }; // ---- Messages ---- function showMessage(text, type) { dom.formMessage.textContent = text; dom.formMessage.className = `form-message ${type}`; setTimeout(hideMessage, 5000); } function hideMessage() { dom.formMessage.className = 'form-message'; } // ---- Escape HTML ---- function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ---- Load source count on page load ---- async function loadSourceCount() { try { const response = await fetch('/api/sources'); const sources = await response.json(); dom.sourceCount.textContent = `${sources.length} 个数据源`; } catch (err) { // ignore } } // ---- Start ---- loadSourceCount(); init(); })();