/* ========================================================= reclip admin — dashboard.js Chart.js polling + admin actions ========================================================= */ 'use strict'; // --------------------------------------------------------------------------- // Utilities // --------------------------------------------------------------------------- function fmt(n) { if (n == null) return '--'; return Number(n).toLocaleString(); } function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.innerHTML = ` ${message} `; const closeBtn = toast.querySelector('.toast-close'); closeBtn.addEventListener('click', () => removeToast(toast)); container.appendChild(toast); // Auto-remove after 5 seconds setTimeout(() => removeToast(toast), 5000); } function removeToast(toast) { if (!toast.parentElement) return; toast.style.opacity = '0'; toast.style.transform = 'translateX(20px)'; toast.style.transition = 'opacity 0.2s ease-in, transform 0.2s ease-in'; setTimeout(() => toast.remove(), 200); } // --------------------------------------------------------------------------- // Chart configuration helpers // --------------------------------------------------------------------------- const CHART_DEFAULTS = { color: '#ff6b35', gridColor: '#2a2a40', tickColor: '#8888a0', fontFamily: 'Inter, system-ui, -apple-system, sans-serif', }; function commonScaleOptions() { return { grid: { color: CHART_DEFAULTS.gridColor }, ticks: { color: CHART_DEFAULTS.tickColor, font: { family: CHART_DEFAULTS.fontFamily, size: 11 }, }, border: { color: CHART_DEFAULTS.gridColor }, }; } // --------------------------------------------------------------------------- // Chart instances (module-level so we can update them) // --------------------------------------------------------------------------- let downloadsChart = null; let platformChart = null; function initCharts() { const dlCtx = document.getElementById('downloads-chart'); const plCtx = document.getElementById('platform-chart'); if (dlCtx) { downloadsChart = new Chart(dlCtx, { type: 'line', data: { labels: [], datasets: [{ label: 'Downloads', data: [], borderColor: CHART_DEFAULTS.color, backgroundColor: 'rgba(255, 107, 53, 0.08)', borderWidth: 2, pointRadius: 3, pointHoverRadius: 5, tension: 0.3, fill: true, }], }, options: { responsive: true, maintainAspectRatio: false, animation: { duration: 400, easing: 'easeOut' }, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1a1a2e', borderColor: '#2a2a40', borderWidth: 1, titleColor: '#e0e0e0', bodyColor: '#8888a0', titleFont: { family: CHART_DEFAULTS.fontFamily, size: 12 }, bodyFont: { family: CHART_DEFAULTS.fontFamily, size: 12 }, }, }, scales: { x: commonScaleOptions(), y: { ...commonScaleOptions(), beginAtZero: true, ticks: { ...commonScaleOptions().ticks, precision: 0, }, }, }, }, }); } if (plCtx) { platformChart = new Chart(plCtx, { type: 'bar', data: { labels: [], datasets: [{ label: 'Downloads', data: [], backgroundColor: 'rgba(255, 107, 53, 0.7)', borderColor: CHART_DEFAULTS.color, borderWidth: 1, borderRadius: 3, }], }, options: { indexAxis: 'y', responsive: true, maintainAspectRatio: false, animation: { duration: 400, easing: 'easeOut' }, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1a1a2e', borderColor: '#2a2a40', borderWidth: 1, titleColor: '#e0e0e0', bodyColor: '#8888a0', titleFont: { family: CHART_DEFAULTS.fontFamily, size: 12 }, bodyFont: { family: CHART_DEFAULTS.fontFamily, size: 12 }, }, }, scales: { x: { ...commonScaleOptions(), beginAtZero: true, ticks: { ...commonScaleOptions().ticks, precision: 0 }, }, y: commonScaleOptions(), }, }, }); } } // --------------------------------------------------------------------------- // API fetchers // --------------------------------------------------------------------------- async function fetchStats() { try { const res = await fetch('/api/dashboard-stats', { credentials: 'same-origin' }); if (!res.ok) return; const json = await res.json(); const stats = json.stats || {}; const disk = json.disk || null; // Downloads today const dlEl = document.getElementById('kpi-downloads'); if (dlEl) dlEl.textContent = fmt(stats.downloads_today); const dlTrend = document.getElementById('kpi-downloads-trend'); if (dlTrend) { const today = stats.downloads_today ?? 0; const yesterday = stats.downloads_yesterday ?? 0; if (yesterday === 0 && today === 0) { dlTrend.textContent = ''; dlTrend.className = 'kpi-trend'; } else if (today >= yesterday) { dlTrend.textContent = `↑ vs ${fmt(yesterday)} yesterday`; dlTrend.className = 'kpi-trend up'; } else { dlTrend.textContent = `↓ vs ${fmt(yesterday)} yesterday`; dlTrend.className = 'kpi-trend down'; } } // Active users const usersEl = document.getElementById('kpi-users'); if (usersEl) usersEl.textContent = fmt(stats.active_users_24h); // Error rate const errEl = document.getElementById('kpi-errors'); if (errEl) { const rate = stats.error_rate != null ? stats.error_rate.toFixed(1) : '--'; errEl.textContent = rate !== '--' ? rate + '%' : '--'; } const errTrend = document.getElementById('kpi-errors-trend'); if (errTrend) { const today = stats.error_rate ?? 0; const yesterday = stats.error_rate_yesterday ?? 0; if (yesterday === 0 && today === 0) { errTrend.textContent = ''; errTrend.className = 'kpi-trend'; } else if (today <= yesterday) { // lower error rate is better errTrend.textContent = `↓ vs ${yesterday.toFixed(1)}% yesterday`; errTrend.className = 'kpi-trend up'; } else { errTrend.textContent = `↑ vs ${yesterday.toFixed(1)}% yesterday`; errTrend.className = 'kpi-trend down'; } } // Disk const diskEl = document.getElementById('kpi-disk'); const diskBar = document.getElementById('disk-bar'); if (disk && diskEl) { const totalBytes = disk.total_bytes || 0; // Use a configured max of 100GB, or actual disk_total from system info // The disk snapshot records downloads folder size; compare against a 100GB ceiling const MAX_BYTES = 100 * 1073741824; // 100 GB ceiling const pct = totalBytes > 0 ? Math.min((totalBytes / MAX_BYTES) * 100, 100) : 0; diskEl.textContent = pct.toFixed(1) + '%'; if (diskBar) { diskBar.style.width = pct + '%'; diskBar.className = 'disk-bar'; if (pct >= 85) diskBar.classList.add('red'); else if (pct >= 60) diskBar.classList.add('amber'); } } } catch (e) { console.warn('fetchStats error', e); } } async function fetchChartData(range = '1D') { try { const res = await fetch(`/api/chart-data?range=${range}`, { credentials: 'same-origin' }); if (!res.ok) return; const data = await res.json(); // Downloads over time chart if (downloadsChart && data.labels && data.values) { downloadsChart.data.labels = data.labels; downloadsChart.data.datasets[0].data = data.values; downloadsChart.update(); } // Platform chart if (platformChart && data.platforms) { platformChart.data.labels = data.platforms.map(p => p.platform); platformChart.data.datasets[0].data = data.platforms.map(p => p.count); platformChart.update(); } // Top users table const tbody = document.getElementById('top-users'); if (tbody && data.top_users) { if (data.top_users.length === 0) { tbody.innerHTML = '