/** * 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'), // May be null if removed from UI 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'), trafficP95: document.getElementById('trafficP95'), networkCanvas: document.getElementById('networkCanvas'), 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'), footerTime: document.getElementById('footerTime'), legendP95: document.getElementById('legendP95'), // Server Details Modal serverDetailModal: document.getElementById('serverDetailModal'), serverDetailClose: document.getElementById('serverDetailClose'), serverDetailTitle: document.getElementById('serverDetailTitle'), detailGrid: document.getElementById('detailGrid'), detailLoading: document.getElementById('detailLoading'), detailCpuBusy: document.getElementById('detailCpuBusy'), detailSysLoad: document.getElementById('detailSysLoad'), detailMemUsedPct: document.getElementById('detailMemUsedPct'), detailSwapUsedPct: document.getElementById('detailSwapUsedPct'), detailRootFsUsedPct: document.getElementById('detailRootFsUsedPct'), detailCpuCores: document.getElementById('detailCpuCores'), detailMemTotal: document.getElementById('detailMemTotal'), detailUptime: document.getElementById('detailUptime'), detailNetRx: document.getElementById('detailNetRx'), detailNetTx: document.getElementById('detailNetTx') }; // ---- State ---- let previousMetrics = null; let networkChart = null; let user = null; // Currently logged in user // ---- Initialize ---- function init() { // Resource Gauges Time updateGaugesTime(); setInterval(updateGaugesTime, 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('change', 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(); closeServerDetail(); } }); // Server detail modal listeners dom.serverDetailClose.addEventListener('click', closeServerDetail); dom.serverDetailModal.addEventListener('click', (e) => { if (e.target === dom.serverDetailModal) closeServerDetail(); }); // Server table row click delegator dom.serverTableBody.addEventListener('click', (e) => { const row = e.target.closest('tr'); if (row && !row.classList.contains('empty-row')) { const instance = row.getAttribute('data-instance'); const job = row.getAttribute('data-job'); const source = row.getAttribute('data-source'); if (instance && job && source) { openServerDetail(instance, job, source); } } }); // P95 Toggle if (dom.legendP95) { dom.legendP95.addEventListener('click', () => { networkChart.showP95 = !networkChart.showP95; dom.legendP95.classList.toggle('disabled', !networkChart.showP95); networkChart.draw(); }); } // Check auth status checkAuthStatus(); // Start data fetching fetchMetrics(); fetchNetworkHistory(); // Site settings if (window.SITE_SETTINGS) { applySiteSettings(window.SITE_SETTINGS); // Actual theme class already applied in head, just update icons and inputs const savedTheme = localStorage.getItem('theme'); const currentTheme = savedTheme || window.SITE_SETTINGS.default_theme || 'dark'; updateThemeIcons(currentTheme); // Still populate inputs dom.pageNameInput.value = window.SITE_SETTINGS.page_name || ''; dom.siteTitleInput.value = window.SITE_SETTINGS.title || ''; dom.logoUrlInput.value = window.SITE_SETTINGS.logo_url || ''; dom.defaultThemeInput.value = window.SITE_SETTINGS.default_theme || 'dark'; } loadSiteSettings(); setInterval(fetchMetrics, REFRESH_INTERVAL); setInterval(fetchNetworkHistory, NETWORK_HISTORY_INTERVAL); } // ---- Theme Switching ---- function toggleTheme() { const theme = dom.themeToggle.checked ? 'light' : 'dark'; document.documentElement.classList.toggle('light-theme', theme === 'light'); localStorage.setItem('theme', theme); updateThemeIcons(theme); } function applyTheme(theme) { let actualTheme = theme; if (theme === 'auto') { actualTheme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; } const isLight = actualTheme === 'light'; dom.themeToggle.checked = isLight; document.documentElement.classList.toggle('light-theme', isLight); updateThemeIcons(actualTheme); } function updateThemeIcons(theme) { // Icons are in the slider, we adjust their opacity to show which one is active if (theme === 'light') { dom.sunIcon.style.opacity = '0.3'; dom.moonIcon.style.opacity = '1'; } else { dom.sunIcon.style.opacity = '1'; dom.moonIcon.style.opacity = '0.3'; } } // ---- 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.btnSettings.style.display = 'flex'; dom.userSection.innerHTML = `
`; document.getElementById('btnLogout').addEventListener('click', handleLogout); } else { user = null; dom.btnSettings.style.display = 'none'; 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); } } // ---- Clock ---- function updateClock() { if (dom.clock) { dom.clock.textContent = formatClock(); } } function updateGaugesTime() { const clockStr = formatClock(); if (dom.footerTime) { dom.footerTime.textContent = clockStr; } } // ---- 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 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; } // ---- Server Table ---- function updateServerTable(servers) { if (!servers || servers.length === 0) { dom.serverTableBody.innerHTML = `