/** * Analytics Dashboard * Privacy-respecting, self-hosted analytics */ let currentPeriod = '24h'; let trendChart = null; let refreshInterval = null; // Initialize dashboard document.addEventListener('DOMContentLoaded', () => { initializePeriodSelector(); initializeRefreshButton(); loadDashboard(); startAutoRefresh(); }); /** * Initialize period selector */ function initializePeriodSelector() { const selector = document.getElementById('periodSelector'); const buttons = selector.querySelectorAll('button'); buttons.forEach(button => { button.addEventListener('click', () => { // Update active state buttons.forEach(b => b.classList.remove('active')); button.classList.add('active'); // Update period and reload currentPeriod = button.dataset.period; loadDashboard(); }); }); } /** * Initialize refresh button */ function initializeRefreshButton() { const refreshBtn = document.getElementById('refreshBtn'); const refreshIcon = document.getElementById('refreshIcon'); refreshBtn.addEventListener('click', () => { refreshIcon.style.display = 'inline-block'; refreshIcon.classList.add('loading'); loadDashboard(); }); } /** * Start auto-refresh (every 30 seconds) */ function startAutoRefresh() { refreshInterval = setInterval(() => { loadDashboard(true); // silent refresh }, 30000); } /** * Load complete dashboard */ async function loadDashboard(silent = false) { try { // Load data in parallel await Promise.all([ loadOverview(), loadHourlyTrend(), loadLiveVisitors() ]); // Update last updated timestamp updateLastUpdated(); // Remove loading state from refresh button if (!silent) { const refreshIcon = document.getElementById('refreshIcon'); refreshIcon.classList.remove('loading'); } } catch (error) { console.error('Dashboard load error:', error); if (!silent) { showError('Failed to load analytics data'); } } } /** * Load overview statistics */ async function loadOverview() { const response = await fetch(`/api/analytics/overview?period=${currentPeriod}`, { credentials: 'include' }); if (!response.ok) { throw new Error('Failed to fetch overview'); } const result = await response.json(); const data = result.data; // Update metrics document.getElementById('totalPageViews').textContent = formatNumber(data.totalPageViews); document.getElementById('uniqueVisitors').textContent = formatNumber(data.uniqueVisitors); document.getElementById('bounceRate').textContent = `${data.bounceRate}%`; document.getElementById('avgDuration').textContent = formatDuration(data.avgDuration); // Update top pages updateTopPages(data.topPages); // Update top referrers updateTopReferrers(data.topReferrers); } /** * Load hourly trend data */ async function loadHourlyTrend() { // Determine hours based on period let hours = 24; if (currentPeriod === '1h') hours = 1; else if (currentPeriod === '7d') hours = 168; else if (currentPeriod === '30d') hours = 720; const response = await fetch(`/api/analytics/trend?hours=${hours}`, { credentials: 'include' }); if (!response.ok) { throw new Error('Failed to fetch trend data'); } const result = await response.json(); updateTrendChart(result.data); } /** * Load live visitors */ async function loadLiveVisitors() { const response = await fetch('/api/analytics/live', { credentials: 'include' }); if (!response.ok) { throw new Error('Failed to fetch live visitors'); } const result = await response.json(); const data = result.data; // Update live visitor count document.getElementById('liveVisitors').textContent = data.count; // Update recent activity updateRecentActivity(data.recentPages); } /** * Update top pages table */ function updateTopPages(pages) { const tbody = document.getElementById('topPagesTable'); if (!pages || pages.length === 0) { tbody.innerHTML = '