Files
OpenVPN-Monitoring-Simple/UI/js/pages/dashboard.js
Антон c9af0a5bb1 init commit
2026-01-09 01:05:50 +03:00

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
});
};
});