diff --git a/infrastructure/web/public/javascripts/stats.js b/infrastructure/web/public/javascripts/stats.js index 9cc12c5..6e1a371 100644 --- a/infrastructure/web/public/javascripts/stats.js +++ b/infrastructure/web/public/javascripts/stats.js @@ -6,6 +6,8 @@ // Store chart instance globally for updates let statsChart = null; let chartContext = null; +let lastReloadTime = 0; +const RELOAD_COOLDOWN_MS = 2000; // 2 second cooldown between reloads // Initialize stats chart if on stats page document.addEventListener('DOMContentLoaded', function() { @@ -14,7 +16,6 @@ document.addEventListener('DOMContentLoaded', function() { // Get data from global variables (set by template) if (typeof window.initialStatsData === 'undefined') { - console.error('Initial stats data not found'); return; } @@ -22,25 +23,16 @@ document.addEventListener('DOMContentLoaded', function() { const historicalData = window.historicalData || []; const predictionData = window.predictionData || []; - console.log(`Loaded data: ${historicalData.length} historical, ${realtimeData.length} realtime, ${predictionData.length} predictions`); - - // If no data yet (lazy loading), add a placeholder message - const hasData = historicalData.length > 0 || realtimeData.length > 0 || predictionData.length > 0; - if (!hasData) { - console.log('No chart data yet - will populate after lazy load'); - } - - // Set up Socket.IO connection for real-time updates + // Set up Socket.IO connection for real-time updates with rate limiting if (typeof io !== 'undefined') { const socket = io(); socket.on('stats-update', () => { - console.log('Stats update received (page will not auto-reload)'); - // Don't auto-reload - user can manually refresh if needed - }); - - socket.on('reconnect', () => { - console.log('Reconnected to server'); + const now = Date.now(); + if (now - lastReloadTime >= RELOAD_COOLDOWN_MS) { + lastReloadTime = now; + reloadStatsData(); + } }); } @@ -257,7 +249,6 @@ document.addEventListener('DOMContentLoaded', function() { */ function rebuildStatsChart() { if (!statsChart || !chartContext) { - console.log('Chart not initialized, skipping rebuild'); return; } @@ -265,8 +256,6 @@ function rebuildStatsChart() { const historicalData = window.historicalData || []; const predictionData = window.predictionData || []; - console.log(`Rebuilding chart with: ${historicalData.length} historical, ${realtimeData.length} realtime, ${predictionData.length} predictions`); - const allTimePoints = [ ...historicalData.map(d => ({...d, type: 'historical' })), ...realtimeData.map(d => ({...d, type: 'realtime' })), @@ -274,7 +263,6 @@ function rebuildStatsChart() { ].sort((a, b) => a.timestamp - b.timestamp); if (allTimePoints.length === 0) { - console.log('No data points to chart'); return; } @@ -311,87 +299,120 @@ function lazyLoadStats() { // Check if this is a lazy-loaded page (has placeholder data) const currentCountEl = document.getElementById('currentCount'); if (!currentCountEl) { - console.log('Stats lazy load: currentCount element not found'); return; } const currentText = currentCountEl.textContent.trim(); - console.log('Stats lazy load: current count text is:', currentText); if (currentText !== '...') { - console.log('Stats lazy load: already loaded with real data, skipping'); return; // Already loaded with real data } - console.log('Stats lazy load: fetching data from /stats/api'); + reloadStatsData(); +} +/** + * Reload statistics data from API and update DOM + */ +function reloadStatsData() { fetch('/stats/api') .then(response => response.json()) .then(data => { - console.log('Stats lazy load: received data', data); - // Update main stat cards - document.getElementById('currentCount').textContent = data.currentCount || '0'; - document.getElementById('historicalTotal').textContent = data.allTimeTotal || '0'; - document.getElementById('receives24h').textContent = (data.last24Hours && data.last24Hours.receives) || '0'; - document.getElementById('deletes24h').textContent = (data.last24Hours && data.last24Hours.deletes) || '0'; - document.getElementById('forwards24h').textContent = (data.last24Hours && data.last24Hours.forwards) || '0'; - - // Update enhanced stats if available - if (data.enhanced) { - const topSenderDomains = document.querySelector('[data-stats="top-sender-domains"]'); - const topRecipientDomains = document.querySelector('[data-stats="top-recipient-domains"]'); - - if (topSenderDomains && data.enhanced.topSenderDomains && data.enhanced.topSenderDomains.length > 0) { - let html = ''; - data.enhanced.topSenderDomains.slice(0, 5).forEach(item => { - html += `
  • ${item.domain}${item.count}
  • `; - }); - topSenderDomains.innerHTML = html; - } - - if (topRecipientDomains && data.enhanced.topRecipientDomains && data.enhanced.topRecipientDomains.length > 0) { - let html = ''; - data.enhanced.topRecipientDomains.slice(0, 5).forEach(item => { - html += `
  • ${item.domain}${item.count}
  • `; - }); - topRecipientDomains.innerHTML = html; - } - - // Update unique domains count - const uniqueSenderDomains = document.querySelector('[data-stats="unique-sender-domains"]'); - if (uniqueSenderDomains && data.enhanced.uniqueSenderDomains) { - uniqueSenderDomains.textContent = data.enhanced.uniqueSenderDomains; - } - - // Update average email size - const avgSize = document.querySelector('[data-stats="average-email-size"]'); - if (avgSize && data.enhanced.averageEmailSize) { - avgSize.textContent = data.enhanced.averageEmailSize; - } - - // Update peak hour - const peakHour = document.querySelector('[data-stats="peak-hour"]'); - if (peakHour && data.enhanced.peakHour) { - peakHour.textContent = data.enhanced.peakHour; - } - - // Update 24h prediction - const prediction24h = document.querySelector('[data-stats="prediction-24h"]'); - if (prediction24h && data.enhanced.prediction24h) { - prediction24h.textContent = data.enhanced.prediction24h; - } - } - - // Update window data for charts - window.initialStatsData = (data.last24Hours && data.last24Hours.timeline) || []; - window.historicalData = data.historical || []; - window.predictionData = data.prediction || []; - - // Rebuild chart with new data - rebuildStatsChart(); + updateStatsDOM(data); }) .catch(error => { - console.error('Error loading stats:', error); - // Stats remain as placeholder + console.error('Error reloading stats:', error); }); } + +/** + * Update DOM with stats data + */ +function updateStatsDOM(data) { + // Update main stat cards + document.getElementById('currentCount').textContent = data.currentCount || '0'; + document.getElementById('historicalTotal').textContent = data.allTimeTotal || '0'; + document.getElementById('receives24h').textContent = (data.last24Hours && data.last24Hours.receives) || '0'; + document.getElementById('deletes24h').textContent = (data.last24Hours && data.last24Hours.deletes) || '0'; + document.getElementById('forwards24h').textContent = (data.last24Hours && data.last24Hours.forwards) || '0'; + + // Update enhanced stats if available + if (data.enhanced) { + const topSenderDomains = document.querySelector('[data-stats="top-sender-domains"]'); + const topRecipientDomains = document.querySelector('[data-stats="top-recipient-domains"]'); + const busiestHours = document.querySelector('[data-stats="busiest-hours"]'); + if (topSenderDomains && data.enhanced.topSenderDomains && data.enhanced.topSenderDomains.length > 0) { + let html = ''; + data.enhanced.topSenderDomains.slice(0, 5).forEach(item => { + html += `
  • ${item.domain}${item.count}
  • `; + }); + topSenderDomains.innerHTML = html; + } + + if (topRecipientDomains && data.enhanced.topRecipientDomains && data.enhanced.topRecipientDomains.length > 0) { + let html = ''; + data.enhanced.topRecipientDomains.slice(0, 5).forEach(item => { + html += `
  • ${item.domain}${item.count}
  • `; + }); + topRecipientDomains.innerHTML = html; + } + + if (busiestHours && data.enhanced.busiestHours && data.enhanced.busiestHours.length > 0) { + let html = ''; + data.enhanced.busiestHours.forEach(item => { + html += `
  • ${item.hour}:00 - ${item.hour + 1}:00${item.count}
  • `; + }); + busiestHours.innerHTML = html; + } + + // Update unique domains count + const uniqueSenderDomains = document.querySelector('[data-stats="unique-sender-domains"]'); + if (uniqueSenderDomains && data.enhanced.uniqueSenderDomains !== undefined) { + uniqueSenderDomains.textContent = data.enhanced.uniqueSenderDomains; + } + + const uniqueRecipientDomains = document.querySelector('[data-stats="unique-recipient-domains"]'); + if (uniqueRecipientDomains && data.enhanced.uniqueRecipientDomains !== undefined) { + uniqueRecipientDomains.textContent = data.enhanced.uniqueRecipientDomains; + } + + // Update Quick Insights values + const avgSubjectLength = document.querySelector('[data-stats="average-subject-length"]'); + if (avgSubjectLength && data.enhanced.averageSubjectLength !== undefined) { + avgSubjectLength.textContent = data.enhanced.averageSubjectLength; + } + + const uniqueSenderDomainsValue = document.querySelector('[data-stats="unique-sender-domains-value"]'); + if (uniqueSenderDomainsValue && data.enhanced.uniqueSenderDomains !== undefined) { + uniqueSenderDomainsValue.textContent = data.enhanced.uniqueSenderDomains; + } + + const uniqueRecipientDomainsValue = document.querySelector('[data-stats="unique-recipient-domains-value"]'); + if (uniqueRecipientDomainsValue && data.enhanced.uniqueRecipientDomains !== undefined) { + uniqueRecipientDomainsValue.textContent = data.enhanced.uniqueRecipientDomains; + } + + const peakHourPercentage = document.querySelector('[data-stats="peak-hour-percentage"]'); + if (peakHourPercentage && data.enhanced.peakHourPercentage !== undefined) { + peakHourPercentage.textContent = data.enhanced.peakHourPercentage + '%'; + } + + const emailsPerHour = document.querySelector('[data-stats="emails-per-hour"]'); + if (emailsPerHour && data.enhanced.emailsPerHour !== undefined) { + emailsPerHour.textContent = data.enhanced.emailsPerHour; + } + + const dayPercentage = document.querySelector('[data-stats="day-percentage"]'); + if (dayPercentage && data.enhanced.dayPercentage !== undefined) { + dayPercentage.textContent = data.enhanced.dayPercentage + '%'; + } + } + + // Update window data for charts + window.initialStatsData = (data.last24Hours && data.last24Hours.timeline) || []; + window.historicalData = data.historical || []; + window.predictionData = data.prediction || []; + + // Rebuild chart with new data + rebuildStatsChart(); +} diff --git a/infrastructure/web/routes/stats.js b/infrastructure/web/routes/stats.js index 5636a9f..621e60a 100644 --- a/infrastructure/web/routes/stats.js +++ b/infrastructure/web/routes/stats.js @@ -34,10 +34,13 @@ router.get('/', async(req, res) => { enhanced: { topSenderDomains: [], topRecipientDomains: [], + busiestHours: [], uniqueSenderDomains: '...', - averageEmailSize: '...', - peakHour: '...', - prediction24h: '...' + uniqueRecipientDomains: '...', + averageSubjectLength: '...', + peakHourPercentage: '...', + emailsPerHour: '...', + dayPercentage: '...' }, historical: [], prediction: [] diff --git a/infrastructure/web/views/stats.twig b/infrastructure/web/views/stats.twig index f1405ed..c426c2a 100644 --- a/infrastructure/web/views/stats.twig +++ b/infrastructure/web/views/stats.twig @@ -96,19 +96,19 @@

    Top Sender Domains

    + + @@ -116,19 +116,19 @@

    Top Recipient Domains

    + + @@ -136,18 +136,18 @@

    Busiest Hours

    + @@ -157,27 +157,27 @@
    -
    {{ stats.enhanced.averageSubjectLength }}
    +
    {{ stats.enhanced.averageSubjectLength }}
    Avg Subject Length
    -
    {{ stats.enhanced.uniqueSenderDomains }}
    +
    {{ stats.enhanced.uniqueSenderDomains }}
    Unique Senders
    -
    {{ stats.enhanced.uniqueRecipientDomains }}
    +
    {{ stats.enhanced.uniqueRecipientDomains }}
    Unique Recipients
    -
    {{ stats.enhanced.peakHourPercentage }}%
    +
    {{ stats.enhanced.peakHourPercentage }}%
    Peak Hour Traffic
    -
    {{ stats.enhanced.emailsPerHour }}
    +
    {{ stats.enhanced.emailsPerHour }}
    Emails per Hour
    -
    {{ stats.enhanced.dayPercentage }}%
    +
    {{ stats.enhanced.dayPercentage }}%
    Daytime (6am-6pm)