/** * 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'), // Site Settings modalTabs: document.querySelectorAll('.modal-tab'), tabContents: document.querySelectorAll('.tab-content'), pageNameInput: document.getElementById('pageNameInput'), siteTitleInput: document.getElementById('siteTitleInput'), logoUrlInput: document.getElementById('logoUrlInput'), btnSaveSiteSettings: document.getElementById('btnSaveSiteSettings'), siteSettingsMessage: document.getElementById('siteSettingsMessage'), logoText: document.getElementById('logoText'), logoIconContainer: document.getElementById('logoIconContainer'), defaultThemeInput: document.getElementById('defaultThemeInput'), // Auth & Theme elements themeToggle: document.getElementById('themeToggle'), sunIcon: document.querySelector('.sun-icon'), moonIcon: document.querySelector('.moon-icon'), userSection: document.getElementById('userSection'), btnLogin: document.getElementById('btnLogin'), loginModal: document.getElementById('loginModalOverlay'), closeLoginModal: document.getElementById('closeLoginModal'), loginForm: document.getElementById('loginForm'), loginError: document.getElementById('loginError') }; // ---- State ---- let previousMetrics = null; let networkChart = null; let user = null; // Currently logged in user // ---- Initialize ---- function init() { // Add SVG gradient definitions for gauges addGaugeSvgDefs(); // Clock updateClock(); setInterval(updateClock, 1000); // Initial theme check (localStorage handled after site settings load to ensure priority) // 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); // Auth & Theme listeners dom.themeToggle.addEventListener('click', toggleTheme); dom.btnLogin.addEventListener('click', openLoginModal); dom.closeLoginModal.addEventListener('click', closeLoginModal); dom.loginForm.addEventListener('submit', handleLogin); dom.loginModal.addEventListener('click', (e) => { if (e.target === dom.loginModal) closeLoginModal(); }); // Tab switching dom.modalTabs.forEach(tab => { tab.addEventListener('click', () => { const targetTab = tab.getAttribute('data-tab'); switchTab(targetTab); }); }); // Site settings dom.btnSaveSiteSettings.addEventListener('click', saveSiteSettings); // Keyboard shortcut document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeSettings(); closeLoginModal(); } }); // Check auth status checkAuthStatus(); // Start data fetching fetchMetrics(); fetchNetworkHistory(); loadSiteSettings(); setInterval(fetchMetrics, REFRESH_INTERVAL); setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL); } // ---- Theme Switching ---- function toggleTheme() { const isLight = document.documentElement.classList.toggle('light-theme'); const theme = isLight ? 'light' : 'dark'; localStorage.setItem('theme', theme); updateThemeIcons(theme); } function applyTheme(theme) { const isLight = theme === 'light'; document.documentElement.classList.toggle('light-theme', isLight); updateThemeIcons(theme); } function updateThemeIcons(theme) { if (theme === 'light') { dom.sunIcon.style.display = 'none'; dom.moonIcon.style.display = 'block'; } else { dom.sunIcon.style.display = 'block'; dom.moonIcon.style.display = 'none'; } } // ---- Auth Logic ---- async function checkAuthStatus() { try { const resp = await fetch('/api/auth/status'); const data = await resp.json(); if (data.authenticated) { updateUserUI(data.username); } else { updateUserUI(null); } } catch (err) { updateUserUI(null); } } function updateUserUI(username) { if (username) { user = username; dom.userSection.innerHTML = `
`; document.getElementById('btnLogout').addEventListener('click', handleLogout); } else { user = null; dom.userSection.innerHTML = ``; document.getElementById('btnLogin').addEventListener('click', openLoginModal); } } function openLoginModal() { dom.loginModal.classList.add('active'); dom.loginError.style.display = 'none'; } function closeLoginModal() { dom.loginModal.classList.remove('active'); dom.loginForm.reset(); } async function handleLogin(e) { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const resp = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await resp.json(); if (resp.ok) { updateUserUI(data.username); closeLoginModal(); } else { dom.loginError.textContent = data.error || '登录失败'; dom.loginError.style.display = 'block'; } } catch (err) { dom.loginError.textContent = '服务器错误'; dom.loginError.style.display = 'block'; } } async function handleLogout() { try { await fetch('/api/auth/logout', { method: 'POST' }); updateUserUI(null); } catch (err) { console.error('Logout failed:', err); } } // ---- 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('x1', '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.total || 0); 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 || (data.traffic24h.rx + data.traffic24h.tx)); // 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); } // ---- Server Table ---- function updateServerTable(servers) { if (!servers || servers.length === 0) { dom.serverTableBody.innerHTML = `