// dashboard.js - OpenVPN Analytics Dashboard Logic // Access globals defined in PHP via window.AppConfig const { apiAnalytics, apiCerts } = window.AppConfig; let mainChart = null; let pieChart = null; let globalHistory = []; let vizMode = 'volume'; // 'volume' or 'speed' // --- Data Loading --- async function loadData() { const icon = document.getElementById('refreshIcon'); icon.classList.add('refresh-indicator'); // Get selected range const range = document.getElementById('globalRange').value; try { const [resA, resC] = await Promise.all([ fetch(apiAnalytics + '?range=' + range), fetch(apiCerts) ]); const jsonA = await resA.json(); const jsonC = await resC.json(); if (jsonA.success) updateDashboard(jsonA.data); if (jsonC.success) updateCerts(jsonC.data); } catch (e) { console.error("Dashboard Load Error:", e); } finally { icon.classList.remove('refresh-indicator'); } } function updateDashboard(data) { // 1. KPI document.getElementById('kpiMaxClients').textContent = data.max_concurrent_24h; const totalT = (data.traffic_distribution.rx + data.traffic_distribution.tx); document.getElementById('kpiTotalTraffic').textContent = formatBytes(totalT); // 2. Main Chart globalHistory = data.global_history_24h; renderMainChart(); // 3. Top Clients const tbody = document.getElementById('topClientsTable'); tbody.innerHTML = ''; if (data.top_clients_24h.length === 0) { tbody.innerHTML = 'No activity recorded'; } else { const maxVal = data.top_clients_24h[0].total_traffic; data.top_clients_24h.forEach(c => { const pct = (c.total_traffic / maxVal) * 100; const row = ` ${c.common_name} ${formatBytes(c.total_traffic)}
`; tbody.innerHTML += row; }); } // 4. Pie Chart renderPieChart(data.traffic_distribution); } function updateCerts(certs) { const container = document.getElementById('certsList'); const alertCountEl = document.getElementById('kpiExpiringCerts'); // Filter: Active only, Expires in <= 45 days let expiring = certs.filter(c => { if (c.is_expired) return false; // Hide expired as requested const days = parseInt(c.days_remaining); return !isNaN(days) && days <= 45 && days >= 0; }); expiring.sort((a, b) => parseInt(a.days_remaining) - parseInt(b.days_remaining)); alertCountEl.textContent = expiring.length; if (expiring.length > 0) alertCountEl.style.color = 'var(--warning-text)'; else alertCountEl.style.color = 'var(--text-heading)'; if (expiring.length === 0) { container.innerHTML = `

No certificates expiring soon

`; return; } let html = ''; expiring.forEach(c => { html += `
${c.common_name || 'Unknown'}
Expires: ${c.not_after}
${c.days_remaining}
`; }); container.innerHTML = html; } // --- Charts --- function toggleMainChart() { vizMode = document.getElementById('vizToggle').checked ? 'speed' : 'volume'; document.getElementById('vizLabel').textContent = vizMode === 'volume' ? 'Data Volume' : 'Speed (Mbps)'; renderMainChart(); } function getChartColors(isDark) { return { grid: isDark ? 'rgba(240, 246, 252, 0.05)' : 'rgba(0, 0, 0, 0.04)', text: isDark ? '#8b949e' : '#6c757d', bg: isDark ? '#161b22' : '#ffffff', border: isDark ? '#30363d' : '#d0d7de', title: isDark ? '#c9d1d9' : '#24292f' }; } function renderMainChart() { const ctx = document.getElementById('mainChart').getContext('2d'); if (mainChart) mainChart.destroy(); // Downsample const MAX_POINTS = 48; let displayData = []; if (globalHistory.length > MAX_POINTS) { const blockSize = Math.ceil(globalHistory.length / MAX_POINTS); for (let i = 0; i < globalHistory.length; i += blockSize) { const chunk = globalHistory.slice(i, i + blockSize); let rx = 0, tx = 0, rxR = 0, txR = 0; chunk.forEach(p => { rx += p.total_rx; tx += p.total_tx; rxR = Math.max(rxR, p.total_rx_rate || 0); txR = Math.max(txR, p.total_tx_rate || 0); }); displayData.push({ timestamp: chunk[0].timestamp, rx: rx, tx: tx, rxR: rxR, txR: txR }); } } else { displayData = globalHistory.map(p => ({ timestamp: p.timestamp, rx: p.total_rx, tx: p.total_tx, rxR: p.total_rx_rate, txR: p.total_tx_rate })); } const labels = displayData.map(p => { const d = parseServerDate(p.timestamp); return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }); const dsRx = displayData.map(p => vizMode === 'volume' ? p.rx / (1024 * 1024) : p.rxR); const dsTx = displayData.map(p => vizMode === 'volume' ? p.tx / (1024 * 1024) : p.txR); const isDark = currentTheme === 'dark'; const colors = getChartColors(isDark); mainChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: 'Total Download', data: dsRx, borderColor: '#3fb950', backgroundColor: 'rgba(63, 185, 80, 0.15)', borderWidth: 2, fill: true, tension: 0.3, pointRadius: 0, pointHoverRadius: 4 }, { label: 'Total Upload', data: dsTx, borderColor: '#58a6ff', backgroundColor: 'rgba(88, 166, 255, 0.15)', borderWidth: 2, fill: true, tension: 0.3, pointRadius: 0, pointHoverRadius: 4 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false }, plugins: { legend: { labels: { color: colors.text } } }, scales: { x: { ticks: { color: colors.text, maxTicksLimit: 8 }, grid: { color: colors.grid, borderColor: 'transparent' } }, y: { beginAtZero: true, ticks: { color: colors.text }, grid: { color: colors.grid, borderColor: 'transparent' }, title: { display: true, text: vizMode === 'volume' ? 'MB' : 'Mbps', color: colors.text } } } } }); } function renderPieChart(dist) { const ctx = document.getElementById('pieChart').getContext('2d'); if (pieChart) pieChart.destroy(); document.getElementById('pieRxVal').textContent = formatBytes(dist.rx); document.getElementById('pieTxVal').textContent = formatBytes(dist.tx); const isDark = currentTheme === 'dark'; const colors = getChartColors(isDark); pieChart = new Chart(ctx, { type: 'doughnut', data: { labels: ['Download', 'Upload'], datasets: [{ data: [dist.rx, dist.tx], backgroundColor: ['rgba(63, 185, 80, 0.8)', 'rgba(88, 166, 255, 0.8)'], borderColor: colors.bg, // Add border to match background for better separation borderWidth: 2 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, cutout: '70%' } }); } function renderCharts() { if (globalHistory.length) renderMainChart(); // Trigger load to refresh Pie Chart colors loadData(); } // Expose functions to window for HTML attributes window.loadData = loadData; window.toggleMainChart = toggleMainChart; window.renderCharts = renderCharts; document.addEventListener('DOMContentLoaded', () => { initTheme(); loadData(); setInterval(loadData, 60000); // Override global toggleTheme to update charts // We hook into the global toggleTheme defined in utils.js // but the actual onclick calls toggleTheme(). // We need to overwrite window.toggleTheme const _baseToggleTheme = window.toggleTheme; window.toggleTheme = function () { _baseToggleTheme(() => { // Re-render charts with new theme colors if (globalHistory.length) renderMainChart(); loadData(); // This refreshes everything including pie chart }); }; });