270 lines
9.5 KiB
JavaScript
270 lines
9.5 KiB
JavaScript
// 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 = '<tr><td colspan="3" class="text-center text-muted py-4">No activity recorded</td></tr>';
|
|
} 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 = `
|
|
<tr>
|
|
<td class="fw-bold" style="color:var(--text-heading)">${c.common_name}</td>
|
|
<td class="small font-monospace" style="color:var(--text-main)">${formatBytes(c.total_traffic)}</td>
|
|
<td style="width: 35%; vertical-align:middle;">
|
|
<div class="progress">
|
|
<div class="progress-bar" style="width:${pct}%"></div>
|
|
</div>
|
|
</td>
|
|
</tr>`;
|
|
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 = `
|
|
<div class="text-center py-3">
|
|
<i class="fas fa-check-circle fa-2x mb-2" style="color: var(--success-text); opacity:0.5;"></i>
|
|
<p class="text-muted small mb-0">No certificates expiring soon</p>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
let html = '';
|
|
expiring.forEach(c => {
|
|
html += `
|
|
<div class="cert-item">
|
|
<div style="flex:1;">
|
|
<div style="font-weight:600; color:var(--text-heading); font-size:0.9rem;">${c.common_name || 'Unknown'}</div>
|
|
<div class="small text-muted">Expires: ${c.not_after}</div>
|
|
</div>
|
|
<div><span class="badge badge-soft-warning">${c.days_remaining}</span></div>
|
|
</div>`;
|
|
});
|
|
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
|
|
});
|
|
};
|
|
});
|