diff --git a/domain/statistics-store.js b/domain/statistics-store.js index 2493676..dbb0d64 100644 --- a/domain/statistics-store.js +++ b/domain/statistics-store.js @@ -1,152 +1,402 @@ -const debug = require('debug')('48hr-email:stats-store') -const config = require('../application/config') +const debug = require('debug')('48hr-email:stats-store'); +const config = require('../application/config'); /** * Statistics Store - Tracks email metrics and historical data - * Stores 24-hour rolling statistics for receives, deletes, and forwards + * Stores rolling statistics for receives, deletes, and forwards over the configured purge window * Persists data to database for survival across restarts */ class StatisticsStore { constructor(db = null) { - this.db = db - - // Current totals - this.currentCount = 0 - this.largestUid = 0 - - // 24-hour rolling data (one entry per minute = 1440 entries) - this.hourlyData = [] - this.maxDataPoints = 24 * 60 // 24 hours * 60 minutes - - // Track last cleanup to avoid too frequent operations - this.lastCleanup = Date.now() - - // Historical data caching to prevent repeated analysis - this.historicalData = null - this.lastAnalysisTime = 0 - this.analysisCacheDuration = 5 * 60 * 1000 // Cache for 5 minutes - - // Enhanced statistics (calculated from current emails) - this.enhancedStats = null - this.lastEnhancedStatsTime = 0 - this.enhancedStatsCacheDuration = 5 * 60 * 1000 // Cache for 5 minutes - - // Load persisted data if database is available + this.db = db; + this.currentCount = 0; + this.largestUid = 0; + this.hourlyData = []; + this.maxDataPoints = 1440; // Default: 1440 minutes (24 hours), but actual retention is purge window + this.lastCleanup = Date.now(); + this.historicalData = null; + this.lastAnalysisTime = 0; + this.analysisCacheDuration = 5 * 60 * 1000; // Cache for 5 minutes + this.enhancedStats = null; + this.lastEnhancedStatsTime = 0; + this.enhancedStatsCacheDuration = 5 * 60 * 1000; // Cache for 5 minutes if (this.db) { - this._loadFromDatabase() + this._loadFromDatabase(); } - - debug('Statistics store initialized') + debug('Statistics store initialized'); } - /** - * Get cutoff time based on email purge configuration - * @returns {number} Timestamp in milliseconds - * @private - */ _getPurgeCutoffMs() { - const time = config.email.purgeTime.time - const unit = config.email.purgeTime.unit - - let cutoffMs = 0 + const time = config.email.purgeTime.time; + const unit = config.email.purgeTime.unit; + let cutoffMs = 0; switch (unit) { case 'minutes': - cutoffMs = time * 60 * 1000 - break + cutoffMs = time * 60 * 1000; + break; case 'hours': - cutoffMs = time * 60 * 60 * 1000 - break + cutoffMs = time * 60 * 60 * 1000; + break; case 'days': - cutoffMs = time * 24 * 60 * 60 * 1000 - break + cutoffMs = time * 24 * 60 * 60 * 1000; + break; default: - cutoffMs = 48 * 60 * 60 * 1000 // Fallback to 48 hours + cutoffMs = 48 * 60 * 60 * 1000; // Fallback to 48 hours } - - return cutoffMs + return cutoffMs; } - /** - * Load statistics from database - * @private - */ _loadFromDatabase() { try { - const stmt = this.db.prepare('SELECT largest_uid, hourly_data, last_updated FROM statistics WHERE id = 1') - const row = stmt.get() - + const stmt = this.db.prepare('SELECT largest_uid, hourly_data, last_updated FROM statistics WHERE id = 1'); + const row = stmt.get(); if (row) { - this.largestUid = row.largest_uid || 0 - - // Parse hourly data + this.largestUid = row.largest_uid || 0; if (row.hourly_data) { try { - const parsed = JSON.parse(row.hourly_data) - // Filter out stale data based on config purge time - const cutoff = Date.now() - this._getPurgeCutoffMs() - this.hourlyData = parsed.filter(entry => entry.timestamp >= cutoff) - debug(`Loaded ${this.hourlyData.length} hourly data points from database (cutoff: ${new Date(cutoff).toISOString()})`) + const parsed = JSON.parse(row.hourly_data); + const cutoff = Date.now() - this._getPurgeCutoffMs(); + this.hourlyData = parsed.filter(entry => entry.timestamp >= cutoff); + debug(`Loaded ${this.hourlyData.length} hourly data points from database (cutoff: ${new Date(cutoff).toISOString()})`); } catch (e) { - debug('Failed to parse hourly data:', e.message) - this.hourlyData = [] + debug('Failed to parse hourly data:', e.message); + this.hourlyData = []; } } - - debug(`Loaded from database: largestUid=${this.largestUid}, hourlyData=${this.hourlyData.length} entries`) + debug(`Loaded from database: largestUid=${this.largestUid}, hourlyData=${this.hourlyData.length} entries`); } } catch (error) { - debug('Failed to load statistics from database:', error.message) + debug('Failed to load statistics from database:', error.message); } } - /** - * Save statistics to database - * @private - */ _saveToDatabase() { - if (!this.db) return - + if (!this.db) return; try { const stmt = this.db.prepare(` UPDATE statistics SET largest_uid = ?, hourly_data = ?, last_updated = ? WHERE id = 1 - `) - stmt.run(this.largestUid, JSON.stringify(this.hourlyData), Date.now()) - debug('Statistics saved to database') + `); + stmt.run(this.largestUid, JSON.stringify(this.hourlyData), Date.now()); + debug('Statistics saved to database'); } catch (error) { - debug('Failed to save statistics to database:', error.message) + debug('Failed to save statistics to database:', error.message); } } - /** - * Initialize with current email count - * @param {number} count - Current email count - */ initialize(count) { - this.currentCount = count - debug(`Initialized with ${count} emails`) + this.currentCount = count; + debug(`Initialized with ${count} emails`); } - /** - * Update largest UID (all-time total emails processed) - * @param {number} uid - Largest UID from mailbox (0 if no emails) - */ updateLargestUid(uid) { if (uid >= 0 && uid > this.largestUid) { - this.largestUid = uid - this._saveToDatabase() - debug(`Largest UID updated to ${uid}`) + this.largestUid = uid; + this._saveToDatabase(); + debug(`Largest UID updated to ${uid}`); } } - /** - * Record an email received event - */ recordReceive() { - this.currentCount++ - this._addDataPoint('receive') - debug(`Email received. Current: ${this.currentCount}`) + this.currentCount++; + this._addDataPoint('receive'); + debug(`Email received. Current: ${this.currentCount}`); + } + + recordDelete() { + this.currentCount = Math.max(0, this.currentCount - 1); + this._addDataPoint('delete'); + debug(`Email deleted. Current: ${this.currentCount}`); + } + + recordForward() { + this._addDataPoint('forward'); + debug(`Email forwarded`); + } + + updateCurrentCount(count) { + const diff = count - this.currentCount; + if (diff < 0) { + for (let i = 0; i < Math.abs(diff); i++) { + this._addDataPoint('delete'); + } + } + this.currentCount = count; + debug(`Current count updated to ${count}`); + } + + getStats() { + this._cleanup(); + const purgeWindowStats = this._getPurgeWindowStats(); + return { + currentCount: this.currentCount, + allTimeTotal: this.largestUid, + purgeWindow: { + receives: purgeWindowStats.receives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, + timeline: this._getTimeline() + } + }; + } + + calculateEnhancedStatistics(allMails) { + if (!allMails || allMails.length === 0) { + this.enhancedStats = null; + return; + } + const now = Date.now(); + if (this.enhancedStats && (now - this.lastEnhancedStatsTime) < this.enhancedStatsCacheDuration) { + debug(`Using cached enhanced stats (age: ${Math.round((now - this.lastEnhancedStatsTime) / 1000)}s)`); + return; + } + debug(`Calculating enhanced statistics from ${allMails.length} emails`); + const senderDomains = new Map(); + const recipientDomains = new Map(); + const hourlyActivity = Array(24).fill(0); + let totalSubjectLength = 0; + let subjectCount = 0; + let dayTimeEmails = 0; + let nightTimeEmails = 0; + allMails.forEach(mail => { + try { + if (mail.from && mail.from[0] && mail.from[0].address) { + const parts = mail.from[0].address.split('@'); + const domain = parts[1] ? parts[1].toLowerCase() : null; + if (domain) senderDomains.set(domain, (senderDomains.get(domain) || 0) + 1); + } + if (mail.to && mail.to[0]) { + const parts = mail.to[0].split('@'); + const domain = parts[1] ? parts[1].toLowerCase() : null; + if (domain) recipientDomains.set(domain, (recipientDomains.get(domain) || 0) + 1); + } + if (mail.date) { + const date = new Date(mail.date); + if (!isNaN(date.getTime())) { + const hour = date.getHours(); + hourlyActivity[hour]++; + if (hour >= 6 && hour < 18) dayTimeEmails++; + else nightTimeEmails++; + } + } + if (mail.subject) { + totalSubjectLength += mail.subject.length; + subjectCount++; + } + } catch (e) {} + }); + const topSenderDomains = Array.from(senderDomains.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count })); + const topRecipientDomains = Array.from(recipientDomains.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([domain, count]) => ({ domain, count })); + const busiestHours = hourlyActivity.map((count, hour) => ({ hour, count })).filter(h => h.count > 0).sort((a, b) => b.count - a.count).slice(0, 5); + const peakHourCount = busiestHours.length > 0 ? busiestHours[0].count : 0; + const peakHourPercentage = allMails.length > 0 ? Math.round((peakHourCount / allMails.length) * 100) : 0; + const activeHours = hourlyActivity.filter(count => count > 0).length; + const emailsPerHour = activeHours > 0 ? Math.round(allMails.length / activeHours) : 0; + const totalDayNight = dayTimeEmails + nightTimeEmails; + const dayPercentage = totalDayNight > 0 ? Math.round((dayTimeEmails / totalDayNight) * 100) : 50; + this.enhancedStats = { + topSenderDomains, + topRecipientDomains, + busiestHours, + averageSubjectLength: subjectCount > 0 ? Math.round(totalSubjectLength / subjectCount) : 0, + totalEmails: allMails.length, + uniqueSenderDomains: senderDomains.size, + uniqueRecipientDomains: recipientDomains.size, + peakHourPercentage, + emailsPerHour, + dayPercentage + }; + this.lastEnhancedStatsTime = now; + debug(`Enhanced stats calculated: ${this.enhancedStats.uniqueSenderDomains} unique sender domains, ${this.enhancedStats.busiestHours.length} busy hours`); + } + + analyzeHistoricalData(allMails) { + if (!allMails || allMails.length === 0) { + debug('No historical data to analyze'); + return; + } + const now = Date.now(); + if (this.historicalData && (now - this.lastAnalysisTime) < this.analysisCacheDuration) { + debug(`Using cached historical data (${this.historicalData.length} points, age: ${Math.round((now - this.lastAnalysisTime) / 1000)}s)`); + return; + } + debug(`Analyzing ${allMails.length} emails for historical statistics`); + const startTime = Date.now(); + const histogram = new Map(); + allMails.forEach(mail => { + try { + const date = new Date(mail.date); + if (isNaN(date.getTime())) return; + const minute = Math.floor(date.getTime() / 60000) * 60000; + if (!histogram.has(minute)) histogram.set(minute, 0); + histogram.set(minute, histogram.get(minute) + 1); + } catch (e) {} + }); + this.historicalData = Array.from(histogram.entries()).map(([timestamp, count]) => ({ timestamp, receives: count })).sort((a, b) => a.timestamp - b.timestamp); + this.lastAnalysisTime = now; + const elapsed = Date.now() - startTime; + debug(`Built historical data: ${this.historicalData.length} time buckets in ${elapsed}ms`); + } + + getEnhancedStats() { + this._cleanup(); + const purgeWindowStats = this._getPurgeWindowStats(); + const timeline = this._getTimeline(); + const historicalTimeline = this._getHistoricalTimeline(); + const prediction = this._generatePrediction(); + const cutoff = Date.now() - this._getPurgeCutoffMs(); + const historicalReceives = historicalTimeline.filter(point => point.timestamp >= cutoff).reduce((sum, point) => sum + point.receives, 0); + return { + currentCount: this.currentCount, + allTimeTotal: this.largestUid, + purgeWindow: { + receives: purgeWindowStats.receives + historicalReceives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, + timeline: timeline + }, + historical: historicalTimeline, + prediction: prediction, + enhanced: this.enhancedStats + }; + } + + getLightweightStats() { + this._cleanup(); + const purgeWindowStats = this._getPurgeWindowStats(); + const timeline = this._getTimeline(); + return { + currentCount: this.currentCount, + allTimeTotal: this.largestUid, + purgeWindow: { + receives: purgeWindowStats.receives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, + timeline: timeline + } + }; + } + + _getPurgeWindowStats() { + const cutoff = Date.now() - this._getPurgeCutoffMs(); + const recent = this.hourlyData.filter(e => e.timestamp >= cutoff); + return { + receives: recent.reduce((sum, e) => sum + e.receives, 0), + deletes: recent.reduce((sum, e) => sum + e.deletes, 0), + forwards: recent.reduce((sum, e) => sum + e.forwards, 0) + }; + } + + _getTimeline() { + const now = Date.now(); + const cutoff = now - this._getPurgeCutoffMs(); + const buckets = {}; + this.hourlyData.filter(e => e.timestamp >= cutoff).forEach(entry => { + const interval = Math.floor(entry.timestamp / 900000) * 900000; // 15 minutes + if (!buckets[interval]) { + buckets[interval] = { timestamp: interval, receives: 0, deletes: 0, forwards: 0 }; + } + buckets[interval].receives += entry.receives; + buckets[interval].deletes += entry.deletes; + buckets[interval].forwards += entry.forwards; + }); + return Object.values(buckets).sort((a, b) => a.timestamp - b.timestamp); + } + + _getHistoricalTimeline() { + if (!this.historicalData || this.historicalData.length === 0) { + return []; + } + const cutoff = Date.now() - this._getPurgeCutoffMs(); + const relevantHistory = this.historicalData.filter(point => point.timestamp >= cutoff); + const intervalBuckets = new Map(); + relevantHistory.forEach(point => { + const interval = Math.floor(point.timestamp / 900000) * 900000; // 15 minutes + if (!intervalBuckets.has(interval)) { + intervalBuckets.set(interval, 0); + } + intervalBuckets.set(interval, intervalBuckets.get(interval) + point.receives); + }); + const intervalData = Array.from(intervalBuckets.entries()).map(([timestamp, receives]) => ({ timestamp, receives })).sort((a, b) => a.timestamp - b.timestamp); + debug(`Historical timeline: ${intervalData.length} 15-min interval points within ${config.email.purgeTime.time} ${config.email.purgeTime.unit} window`); + return intervalData; + } + + _generatePrediction() { + if (!this.historicalData || this.historicalData.length < 100) { + return []; + } + const now = Date.now(); + const predictions = []; + const hourlyPatterns = new Map(); + this.historicalData.forEach(point => { + const date = new Date(point.timestamp); + const hour = date.getHours(); + if (!hourlyPatterns.has(hour)) { + hourlyPatterns.set(hour, []); + } + hourlyPatterns.get(hour).push(point.receives); + }); + const hourlyAverages = new Map(); + hourlyPatterns.forEach((values, hour) => { + const avg = values.reduce((sum, v) => sum + v, 0) / values.length; + hourlyAverages.set(hour, avg); + }); + debug(`Built hourly patterns for ${hourlyAverages.size} hours from ${this.historicalData.length} data points`); + const purgeMs = this._getPurgeCutoffMs(); + const purgeDurationHours = Math.ceil(purgeMs / (60 * 60 * 1000)); + const predictionHours = Math.min(12, Math.ceil(purgeDurationHours * 0.2)); + const predictionIntervals = predictionHours * 4; + for (let i = 1; i <= predictionIntervals; i++) { + const timestamp = now + (i * 15 * 60 * 1000); + const futureDate = new Date(timestamp); + const futureHour = futureDate.getHours(); + let baseCount = hourlyAverages.get(futureHour); + if (baseCount === undefined) { + const allValues = Array.from(hourlyAverages.values()); + baseCount = allValues.reduce((sum, v) => sum + v, 0) / allValues.length; + } + const scaledCount = baseCount * 15; + const randomFactor = 0.8 + (Math.random() * 0.4); + const predictedCount = Math.round(scaledCount * randomFactor); + predictions.push({ + timestamp, + receives: Math.max(0, predictedCount) + }); + } + debug(`Generated ${predictions.length} prediction points based on hourly patterns`); + return predictions; + } + + _addDataPoint(type) { + const now = Date.now(); + const minute = Math.floor(now / 60000) * 60000; + let entry = this.hourlyData.find(e => e.timestamp === minute); + if (!entry) { + entry = { timestamp: minute, receives: 0, deletes: 0, forwards: 0 }; + this.hourlyData.push(entry); + } + entry[type + 's']++; + this._cleanup(); + if (Math.random() < 0.1) { + this._saveToDatabase(); + } + } + + _cleanup() { + const now = Date.now(); + if (now - this.lastCleanup < 5 * 60 * 1000) { + return; + } + const cutoff = now - this._getPurgeCutoffMs(); + const beforeCount = this.hourlyData.length; + this.hourlyData = this.hourlyData.filter(entry => entry.timestamp >= cutoff); + if (beforeCount !== this.hourlyData.length) { + this._saveToDatabase(); + debug(`Cleaned up ${beforeCount - this.hourlyData.length} old data points (keeping data for ${config.email.purgeTime.time} ${config.email.purgeTime.unit})`); + } + this.lastCleanup = now; } /** @@ -155,7 +405,9 @@ class StatisticsStore { recordDelete() { this.currentCount = Math.max(0, this.currentCount - 1) this._addDataPoint('delete') - debug(`Email deleted. Current: ${this.currentCount}`) + debug(` + Email deleted.Current: $ { this.currentCount } + `) } /** @@ -163,7 +415,8 @@ class StatisticsStore { */ recordForward() { this._addDataPoint('forward') - debug(`Email forwarded`) + debug(` + Email forwarded `) } /** @@ -179,7 +432,8 @@ class StatisticsStore { } } this.currentCount = count - debug(`Current count updated to ${count}`) + debug(` + `) } /** @@ -189,15 +443,15 @@ class StatisticsStore { getStats() { this._cleanup() - const last24h = this._getLast24Hours() + const purgeWindowStats = this._getPurgeWindowStats() return { currentCount: this.currentCount, allTimeTotal: this.largestUid, - last24Hours: { - receives: last24h.receives, - deletes: last24h.deletes, - forwards: last24h.forwards, + purgeWindow: { + receives: purgeWindowStats.receives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, timeline: this._getTimeline() } } @@ -216,11 +470,16 @@ class StatisticsStore { const now = Date.now() if (this.enhancedStats && (now - this.lastEnhancedStatsTime) < this.enhancedStatsCacheDuration) { - debug(`Using cached enhanced stats (age: ${Math.round((now - this.lastEnhancedStatsTime) / 1000)}s)`) + debug(` + Using cached enhanced stats(age: $ { Math.round((now - this.lastEnhancedStatsTime) / 1000) } + s) + `) return } - debug(`Calculating enhanced statistics from ${allMails.length} emails`) + debug(` + Calculating enhanced statistics from $ { allMails.length } + emails `) // Track sender domains (privacy-friendly: domain only, not full address) const senderDomains = new Map() @@ -332,7 +591,10 @@ class StatisticsStore { } this.lastEnhancedStatsTime = now - debug(`Enhanced stats calculated: ${this.enhancedStats.uniqueSenderDomains} unique sender domains, ${this.enhancedStats.busiestHours.length} busy hours`) + debug(` + Enhanced stats calculated: $ { this.enhancedStats.uniqueSenderDomains } + unique sender domains, $ { this.enhancedStats.busiestHours.length } + busy hours `) } /** @@ -348,11 +610,18 @@ class StatisticsStore { // Check cache - if analysis was done recently, skip it const now = Date.now() if (this.historicalData && (now - this.lastAnalysisTime) < this.analysisCacheDuration) { - debug(`Using cached historical data (${this.historicalData.length} points, age: ${Math.round((now - this.lastAnalysisTime) / 1000)}s)`) + debug(` + Using cached historical data($ { this.historicalData.length } + points, age: $ { Math.round((now - this.lastAnalysisTime) / 1000) } + s) + `) return } - debug(`Analyzing ${allMails.length} emails for historical statistics`) + debug(` + Analyzing $ { allMails.length } + emails + for historical statistics `) const startTime = Date.now() // Group emails by minute @@ -382,7 +651,10 @@ class StatisticsStore { this.lastAnalysisTime = now const elapsed = Date.now() - startTime - debug(`Built historical data: ${this.historicalData.length} time buckets in ${elapsed}ms`) + debug(` + Built historical data: $ { this.historicalData.length } + time buckets in $ { elapsed } + ms `) } /** @@ -392,7 +664,7 @@ class StatisticsStore { getEnhancedStats() { this._cleanup() - const last24h = this._getLast24Hours() + const purgeWindowStats = this._getPurgeWindowStats() const timeline = this._getTimeline() const historicalTimeline = this._getHistoricalTimeline() const prediction = this._generatePrediction() @@ -406,10 +678,10 @@ class StatisticsStore { return { currentCount: this.currentCount, allTimeTotal: this.largestUid, - last24Hours: { - receives: last24h.receives + historicalReceives, - deletes: last24h.deletes, - forwards: last24h.forwards, + purgeWindow: { + receives: purgeWindowStats.receives + historicalReceives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, timeline: timeline }, historical: historicalTimeline, @@ -425,16 +697,16 @@ class StatisticsStore { getLightweightStats() { this._cleanup() - const last24h = this._getLast24Hours() + const purgeWindowStats = this._getPurgeWindowStats() const timeline = this._getTimeline() return { currentCount: this.currentCount, allTimeTotal: this.largestUid, - last24Hours: { - receives: last24h.receives, - deletes: last24h.deletes, - forwards: last24h.forwards, + purgeWindow: { + receives: purgeWindowStats.receives, + deletes: purgeWindowStats.deletes, + forwards: purgeWindowStats.forwards, timeline: timeline } } @@ -470,7 +742,11 @@ class StatisticsStore { .map(([timestamp, receives]) => ({ timestamp, receives })) .sort((a, b) => a.timestamp - b.timestamp) - debug(`Historical timeline: ${intervalData.length} 15-min interval points within ${config.email.purgeTime.time} ${config.email.purgeTime.unit} window`) + debug(` + Historical timeline: $ { intervalData.length } + 15 - min interval points within $ { config.email.purgeTime.time } + $ { config.email.purgeTime.unit } + window `) return intervalData } @@ -510,7 +786,11 @@ class StatisticsStore { hourlyAverages.set(hour, avg) }) - debug(`Built hourly patterns for ${hourlyAverages.size} hours from ${this.historicalData.length} data points`) + debug(` + Built hourly patterns + for $ { hourlyAverages.size } + hours from $ { this.historicalData.length } + data points `) // Generate predictions for a reasonable future window // Limit to 20% of purge duration or 12 hours max to maintain chart balance @@ -546,7 +826,9 @@ class StatisticsStore { }) } - debug(`Generated ${predictions.length} prediction points based on hourly patterns`) + debug(` + Generated $ { predictions.length } + prediction points based on hourly patterns `) return predictions } @@ -599,7 +881,12 @@ class StatisticsStore { if (beforeCount !== this.hourlyData.length) { this._saveToDatabase() // Save after cleanup - debug(`Cleaned up ${beforeCount - this.hourlyData.length} old data points (keeping data for ${config.email.purgeTime.time} ${config.email.purgeTime.unit})`) + debug(` + Cleaned up $ { beforeCount - this.hourlyData.length } + old data points(keeping data + for $ { config.email.purgeTime.time } + $ { config.email.purgeTime.unit }) + `) } this.lastCleanup = now @@ -610,16 +897,7 @@ class StatisticsStore { * @returns {Object} Aggregated counts * @private */ - _getLast24Hours() { - const cutoff = Date.now() - this._getPurgeCutoffMs() - const recent = this.hourlyData.filter(e => e.timestamp >= cutoff) - return { - receives: recent.reduce((sum, e) => sum + e.receives, 0), - deletes: recent.reduce((sum, e) => sum + e.deletes, 0), - forwards: recent.reduce((sum, e) => sum + e.forwards, 0) - } - } /** * Get timeline data for graphing (hourly aggregates) diff --git a/infrastructure/web/api/routes/stats.api.md b/infrastructure/web/api/routes/stats.api.md index d01d51c..341b1e5 100644 --- a/infrastructure/web/api/routes/stats.api.md +++ b/infrastructure/web/api/routes/stats.api.md @@ -10,12 +10,12 @@ Endpoints for retrieving statistics and historical data. ### GET `/api/v1/stats/` Get lightweight statistics (no historical analysis). - **Response:** - - `currentCount`, `allTimeTotal`, `last24Hours` (object with `receives`, `deletes`, `forwards`, `timeline`) + - `currentCount`, `allTimeTotal`, `purgeWindow` (object with `receives`, `deletes`, `forwards`, `timeline`) ### GET `/api/v1/stats/enhanced` Get full statistics with historical data and predictions. - **Response:** - - `currentCount`, `allTimeTotal`, `last24Hours`, `historical`, `prediction`, `enhanced` + - `currentCount`, `allTimeTotal`, `purgeWindow`, `historical`, `prediction`, `enhanced` --- @@ -41,7 +41,7 @@ Get full statistics with historical data and predictions. "data": { "currentCount": 123, "allTimeTotal": 4567, - "last24Hours": { + "purgeWindow": { "receives": 10, "deletes": 2, "forwards": 1, diff --git a/infrastructure/web/public/javascripts/stats.js b/infrastructure/web/public/javascripts/stats.js index 8332df9..cc9490d 100644 --- a/infrastructure/web/public/javascripts/stats.js +++ b/infrastructure/web/public/javascripts/stats.js @@ -341,11 +341,16 @@ function reloadStatsData() { */ 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'; + const elCurrent = document.getElementById('currentCount'); + if (elCurrent) elCurrent.textContent = data.currentCount || '0'; + const elTotal = document.getElementById('historicalTotal'); + if (elTotal) elTotal.textContent = data.allTimeTotal || '0'; + const elReceives = document.getElementById('receivesPurgeWindow'); + if (elReceives) elReceives.textContent = (data.purgeWindow && data.purgeWindow.receives) || '0'; + const elDeletes = document.getElementById('deletesPurgeWindow'); + if (elDeletes) elDeletes.textContent = (data.purgeWindow && data.purgeWindow.deletes) || '0'; + const elForwards = document.getElementById('forwardsPurgeWindow'); + if (elForwards) elForwards.textContent = (data.purgeWindow && data.purgeWindow.forwards) || '0'; // Update enhanced stats if available if (data.enhanced) { @@ -420,10 +425,10 @@ function updateStatsDOM(data) { } // Update window data for charts - window.initialStatsData = (data.last24Hours && data.last24Hours.timeline) || []; + window.initialStatsData = (data.purgeWindow && data.purgeWindow.timeline) || []; window.historicalData = data.historical || []; window.predictionData = data.prediction || []; // Rebuild chart with new data rebuildStatsChart(); -} +} \ No newline at end of file diff --git a/infrastructure/web/routes/stats.js b/infrastructure/web/routes/stats.js index bcbbde3..0056302 100644 --- a/infrastructure/web/routes/stats.js +++ b/infrastructure/web/routes/stats.js @@ -52,7 +52,7 @@ router.get('/', async(req, res) => { const placeholderStats = { currentCount: '...', allTimeTotal: '...', - last24Hours: { + purgeWindow: { receives: '...', deletes: '...', forwards: '...', @@ -129,4 +129,4 @@ router.get('/api', async(req, res) => { } }) -module.exports = router +module.exports = router \ No newline at end of file diff --git a/infrastructure/web/views/stats.twig b/infrastructure/web/views/stats.twig index 87d01fd..798737f 100644 --- a/infrastructure/web/views/stats.twig +++ b/infrastructure/web/views/stats.twig @@ -71,19 +71,19 @@