Files
OpenVPN-Monitoring-Simple/UI/js/pages/index.js

366 lines
13 KiB
JavaScript
Raw Normal View History

2026-01-09 01:05:50 +03:00
// 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 <i class="fas fa-sort-down ms-1"></i>';
thSent.classList.remove('active-sort'); thSent.innerHTML = 'Sent';
} else {
thSent.classList.add('active-sort'); thSent.innerHTML = 'Sent <i class="fas fa-sort-down ms-1"></i>';
thRecv.classList.remove('active-sort'); thRecv.innerHTML = 'Received';
}
if (displayData.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-4 text-muted">No clients match your filter</td></tr>';
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 = `
<td colspan="8">
<i class="fas ${iconClass} me-2 small"></i>${c.status} Clients
</td>
`;
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
? `<span class="${c.current_recv_rate_mbps > 0.01 ? 'text-success fw-bold' : 'text-muted opacity-75'}">
${c.current_recv_rate_mbps ? formatRate(c.current_recv_rate_mbps) : '0.000 Mbps'}
</span>`
: '<span class="text-muted opacity-25">-</span>';
const upSpeed = isConnected
? `<span class="${c.current_sent_rate_mbps > 0.01 ? 'text-primary fw-bold' : 'text-muted opacity-75'}">
${c.current_sent_rate_mbps ? formatRate(c.current_sent_rate_mbps) : '0.000 Mbps'}
</span>`
: '<span class="text-muted opacity-25">-</span>';
const row = document.createElement('tr');
row.innerHTML = `
<td>
<a onclick="openHistoryModal('${c.common_name}')" class="client-link">
${c.common_name} <i class="fas fa-chart-area ms-1 small opacity-50"></i>
</a>
</td>
<td class="small text-muted">${c.real_address || '-'}</td>
<td>
<span class="status-badge ${c.status === 'Active' ? 'status-active' : 'status-disconnected'}">
${c.status}
</span>
</td>
<td class="font-monospace small">${formatBytes(c.total_bytes_received)}</td>
<td class="font-monospace small">${formatBytes(c.total_bytes_sent)}</td>
<td class="font-monospace small">${downSpeed}</td>
<td class="font-monospace small">${upSpeed}</td>
<td class="small text-muted">${lastActivity}</td>
`;
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) : []);
}
});
};
});