// index.js - OpenVPN Monitor Client List Logic // Access globals defined in PHP const { apiUrl, refreshTime } = window.AppConfig; const MAX_CHART_POINTS = 48; let allClientsData = []; let currentSort = 'sent'; let hideDisconnected = false; let searchQuery = ''; let refreshIntervalId; // Chart Globals let trafficChart = null; let currentClientName = ''; let currentRange = '24h'; let vizMode = 'volume'; let cachedHistoryData = null; // --- MAIN DASHBOARD LOGIC --- async function fetchData() { document.getElementById('refreshIcon').classList.add('refresh-indicator'); try { const response = await fetch(apiUrl); const json = await response.json(); if (json.success) { allClientsData = json.data; renderDashboard(); } } catch (e) { console.error("Fetch error:", e); } finally { document.getElementById('refreshIcon').classList.remove('refresh-indicator'); } } function handleSearch(val) { searchQuery = val.toLowerCase().trim(); renderDashboard(); } function renderDashboard() { let totalRx = 0, totalTx = 0, activeCnt = 0; allClientsData.forEach(c => { totalRx += c.total_bytes_received || 0; totalTx += c.total_bytes_sent || 0; if (c.status === 'Active') activeCnt++; }); document.getElementById('totalReceived').textContent = formatBytes(totalRx); document.getElementById('totalSent').textContent = formatBytes(totalTx); document.getElementById('activeClients').textContent = activeCnt; document.getElementById('clientCount').textContent = allClientsData.length + ' clients'; const now = new Date(); document.getElementById('lastUpdated').textContent = 'Updated: ' + now.toLocaleTimeString(); let displayData = allClientsData.filter(c => { if (hideDisconnected && c.status !== 'Active') return false; if (searchQuery && !c.common_name.toLowerCase().includes(searchQuery)) return false; return true; }); displayData.sort((a, b) => { if (a.status === 'Active' && b.status !== 'Active') return -1; if (a.status !== 'Active' && b.status === 'Active') return 1; const valA = currentSort === 'received' ? a.total_bytes_received : a.total_bytes_sent; const valB = currentSort === 'received' ? b.total_bytes_received : b.total_bytes_sent; return valB - valA; }); const tbody = document.getElementById('statsTable'); tbody.innerHTML = ''; const thRecv = document.getElementById('thRecv'); const thSent = document.getElementById('thSent'); if (currentSort === 'received') { thRecv.classList.add('active-sort'); thRecv.innerHTML = 'Received '; thSent.classList.remove('active-sort'); thSent.innerHTML = 'Sent'; } else { thSent.classList.add('active-sort'); thSent.innerHTML = 'Sent '; thRecv.classList.remove('active-sort'); thRecv.innerHTML = 'Received'; } if (displayData.length === 0) { tbody.innerHTML = 'No clients match your filter'; return; } let currentStatus = null; displayData.forEach(c => { if (c.status !== currentStatus) { currentStatus = c.status; const dividerRow = document.createElement('tr'); dividerRow.className = 'section-divider'; const iconClass = c.status === 'Active' ? 'fa-circle text-success' : 'fa-circle text-danger'; dividerRow.innerHTML = ` ${c.status} Clients `; tbody.appendChild(dividerRow); } let lastActivity = 'N/A'; if (c.last_activity && c.last_activity !== 'N/A') { const d = parseServerDate(c.last_activity); if (!isNaN(d)) lastActivity = d.toLocaleString(); } const isConnected = c.status === 'Active'; // Speed Visualization Logic const downSpeed = isConnected ? ` ${c.current_recv_rate_mbps ? formatRate(c.current_recv_rate_mbps) : '0.000 Mbps'} ` : '-'; const upSpeed = isConnected ? ` ${c.current_sent_rate_mbps ? formatRate(c.current_sent_rate_mbps) : '0.000 Mbps'} ` : '-'; const row = document.createElement('tr'); row.innerHTML = ` ${c.common_name} ${c.real_address || '-'} ${c.status} ${formatBytes(c.total_bytes_received)} ${formatBytes(c.total_bytes_sent)} ${downSpeed} ${upSpeed} ${lastActivity} `; tbody.appendChild(row); }); } function changeSort(mode) { currentSort = mode; document.getElementById('sortRecv').className = `sort-btn ${mode === 'received' ? 'active' : ''}`; document.getElementById('sortSent').className = `sort-btn ${mode === 'sent' ? 'active' : ''}`; renderDashboard(); } function toggleDisconnected() { hideDisconnected = document.getElementById('hideDisconnected').checked; renderDashboard(); } // --- HISTORY CHART LOGIC --- // Expose openHistoryModal to global scope because it's called from HTML inline onclick window.openHistoryModal = function (clientName) { currentClientName = clientName; document.getElementById('modalClientName').textContent = clientName; document.getElementById('historyRange').value = '3h'; document.getElementById('vizToggle').checked = false; vizMode = 'volume'; document.getElementById('vizLabel').textContent = 'Data Volume'; currentRange = '24h'; const modal = new bootstrap.Modal(document.getElementById('historyModal')); modal.show(); loadHistoryData(); }; window.updateHistoryRange = function () { currentRange = document.getElementById('historyRange').value; loadHistoryData(); }; window.toggleVizMode = function () { const isChecked = document.getElementById('vizToggle').checked; vizMode = isChecked ? 'speed' : 'volume'; document.getElementById('vizLabel').textContent = isChecked ? 'Speed (Mbps)' : 'Data Volume'; if (cachedHistoryData) { const downsampled = downsampleData(cachedHistoryData, MAX_CHART_POINTS); renderChart(downsampled); } else { loadHistoryData(); } }; async function loadHistoryData() { const loader = document.getElementById('chartLoader'); loader.style.display = 'block'; const url = `${apiUrl}/${currentClientName}?range=${currentRange}`; try { const res = await fetch(url); const json = await res.json(); if (json.success && json.data.history) { cachedHistoryData = json.data.history; const downsampled = downsampleData(cachedHistoryData, MAX_CHART_POINTS); renderChart(downsampled); } else { console.warn("No history data found"); if (trafficChart) trafficChart.destroy(); } } catch (e) { console.error("History fetch error:", e); } finally { loader.style.display = 'none'; } } function downsampleData(data, maxPoints) { if (!data || data.length === 0) return []; if (data.length <= maxPoints) return data; const blockSize = Math.ceil(data.length / maxPoints); const result = []; for (let i = 0; i < data.length; i += blockSize) { const chunk = data.slice(i, i + blockSize); if (chunk.length === 0) continue; let sumRx = 0, sumTx = 0; let maxRxRate = 0, maxTxRate = 0; chunk.forEach(pt => { sumRx += (pt.bytes_received || 0); sumTx += (pt.bytes_sent || 0); maxRxRate = Math.max(maxRxRate, pt.bytes_received_rate_mbps || 0); maxTxRate = Math.max(maxTxRate, pt.bytes_sent_rate_mbps || 0); }); result.push({ timestamp: chunk[0].timestamp, bytes_received: sumRx, bytes_sent: sumTx, bytes_received_rate_mbps: maxRxRate, bytes_sent_rate_mbps: maxTxRate }); } return result; } function renderChart(history) { const ctx = document.getElementById('trafficChart').getContext('2d'); if (trafficChart) trafficChart.destroy(); const labels = []; const dataRx = []; const dataTx = []; for (let i = 0; i < history.length; i++) { const point = history[i]; const d = parseServerDate(point.timestamp); let label = ''; if (currentRange.includes('h') || currentRange === '1d') { label = d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } else { label = d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } labels.push(label); if (vizMode === 'volume') { dataRx.push(point.bytes_received / (1024 * 1024)); // MB dataTx.push(point.bytes_sent / (1024 * 1024)); // MB } else { let rxRate = point.bytes_received_rate_mbps; let txRate = point.bytes_sent_rate_mbps; dataRx.push(rxRate); dataTx.push(txRate); } } const isDark = currentTheme === 'dark'; const gridColor = isDark ? 'rgba(240, 246, 252, 0.1)' : 'rgba(0,0,0,0.05)'; const textColor = isDark ? '#8b949e' : '#6c757d'; trafficChart = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: [ { label: vizMode === 'volume' ? 'Received (MB)' : 'RX Mbps', data: dataRx, borderColor: '#27ae60', backgroundColor: 'rgba(39, 174, 96, 0.1)', borderWidth: 2, fill: true, tension: 0.3 }, { label: vizMode === 'volume' ? 'Sent (MB)' : 'TX Mbps', data: dataTx, borderColor: '#2980b9', backgroundColor: 'rgba(41, 128, 185, 0.1)', borderWidth: 2, fill: true, tension: 0.3 } ] }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { labels: { color: textColor } } }, scales: { x: { ticks: { color: textColor, maxTicksLimit: 8 }, grid: { color: gridColor } }, y: { beginAtZero: true, ticks: { color: textColor }, grid: { color: gridColor } } } } }); } // Expose other functions used in HTML onclick attributes window.changeSort = changeSort; window.toggleDisconnected = toggleDisconnected; window.handleSearch = handleSearch; window.fetchData = fetchData; document.addEventListener('DOMContentLoaded', () => { initTheme(); fetchData(); // Start Auto-Refresh refreshIntervalId = setInterval(fetchData, refreshTime); // Re-render chart on theme change // The helper 'toggleTheme' in utils.js takes a callback // Override global toggleTheme to include chart re-rendering const _baseToggleTheme = window.toggleTheme; window.toggleTheme = function () { _baseToggleTheme(() => { if (trafficChart) { renderChart(cachedHistoryData ? downsampleData(cachedHistoryData, MAX_CHART_POINTS) : []); } }); }; });