diff --git a/.github/assets/stats.png b/.github/assets/stats.png new file mode 100644 index 0000000..ef69cfe Binary files /dev/null and b/.github/assets/stats.png differ diff --git a/README.md b/README.md index 793d331..7827b60 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ All data is being removed 48hrs after they have reached the mail server. ## Screenshots -| Homepage | Account Panel | -|:---:|:---:| -| | | +| Homepage | Account Panel | Stats Page | +|:---:|:---:|:---:| +| | | | | Inbox | Email using HTML and CSS | Attachments and Cryptographic Keys view | |:---:|:---:|:---:| diff --git a/app.js b/app.js index 92dfc22..031a2da 100644 --- a/app.js +++ b/app.js @@ -5,7 +5,7 @@ const config = require('./application/config') const debug = require('debug')('48hr-email:app') const Helper = require('./application/helper') - +const helper = new(Helper) const { app, io, server } = require('./infrastructure/web/web') const ClientNotification = require('./infrastructure/web/client-notification') const ImapService = require('./application/imap-service') @@ -95,10 +95,14 @@ const mailProcessingService = new MailProcessingService( debug('Mail processing service initialized') // Initialize statistics with current count -imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, () => { +imapService.on(ImapService.EVENT_INITIAL_LOAD_DONE, async() => { const count = mailProcessingService.getCount() statisticsStore.initialize(count) - debug(`Statistics initialized with ${count} emails`) + + // Get and set the largest UID for all-time total + const largestUid = await helper.getLargestUid(imapService) + statisticsStore.updateLargestUid(largestUid) + debug(`Statistics initialized with ${count} emails, largest UID: ${largestUid}`) }) // Set up timer sync broadcasting after IMAP is ready diff --git a/application/helper.js b/application/helper.js index dae48cf..a91cc96 100644 --- a/application/helper.js +++ b/application/helper.js @@ -172,7 +172,8 @@ class Helper { } async getLargestUid(imapService) { - return await imapService.getLargestUid(); + const uid = await imapService.getLargestUid(); + return uid || 0; } /** diff --git a/domain/statistics-store.js b/domain/statistics-store.js index d05354c..753fde6 100644 --- a/domain/statistics-store.js +++ b/domain/statistics-store.js @@ -8,15 +8,15 @@ class StatisticsStore { constructor() { // Current totals this.currentCount = 0 - this.historicalTotal = 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() - + debug('Statistics store initialized') } @@ -26,18 +26,27 @@ class StatisticsStore { */ initialize(count) { this.currentCount = count - this.historicalTotal = 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 + debug(`Largest UID updated to ${uid}`) + } + } + /** * Record an email received event */ recordReceive() { this.currentCount++ - this.historicalTotal++ - this._addDataPoint('receive') - debug(`Email received. Current: ${this.currentCount}, Historical: ${this.historicalTotal}`) + this._addDataPoint('receive') + debug(`Email received. Current: ${this.currentCount}`) } /** @@ -79,12 +88,12 @@ class StatisticsStore { */ getStats() { this._cleanup() - + const last24h = this._getLast24Hours() - + return { currentCount: this.currentCount, - historicalTotal: this.historicalTotal, + allTimeTotal: this.largestUid, last24Hours: { receives: last24h.receives, deletes: last24h.deletes, @@ -102,7 +111,7 @@ class StatisticsStore { _addDataPoint(type) { const now = Date.now() const minute = Math.floor(now / 60000) * 60000 // Round to minute - + // Find or create entry for this minute let entry = this.hourlyData.find(e => e.timestamp === minute) if (!entry) { @@ -114,10 +123,10 @@ class StatisticsStore { } this.hourlyData.push(entry) } - + entry[type + 's']++ - - this._cleanup() + + this._cleanup() } /** @@ -126,20 +135,20 @@ class StatisticsStore { */ _cleanup() { const now = Date.now() - + // Only cleanup every 5 minutes to avoid constant filtering if (now - this.lastCleanup < 5 * 60 * 1000) { return } - + const cutoff = now - (24 * 60 * 60 * 1000) const beforeCount = this.hourlyData.length this.hourlyData = this.hourlyData.filter(entry => entry.timestamp >= cutoff) - + if (beforeCount !== this.hourlyData.length) { debug(`Cleaned up ${beforeCount - this.hourlyData.length} old data points`) } - + this.lastCleanup = now } @@ -151,7 +160,7 @@ class StatisticsStore { _getLast24Hours() { const cutoff = Date.now() - (24 * 60 * 60 * 1000) 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), @@ -168,7 +177,7 @@ class StatisticsStore { const now = Date.now() const cutoff = now - (24 * 60 * 60 * 1000) const hourly = {} - + // Aggregate by hour this.hourlyData .filter(e => e.timestamp >= cutoff) @@ -181,7 +190,7 @@ class StatisticsStore { hourly[hour].deletes += entry.deletes hourly[hour].forwards += entry.forwards }) - + // Convert to sorted array return Object.values(hourly).sort((a, b) => a.timestamp - b.timestamp) } diff --git a/infrastructure/web/routes/stats.js b/infrastructure/web/routes/stats.js index 8f0b6eb..c971921 100644 --- a/infrastructure/web/routes/stats.js +++ b/infrastructure/web/routes/stats.js @@ -7,13 +7,20 @@ router.get('/', async(req, res) => { try { const config = req.app.get('config') const statisticsStore = req.app.get('statisticsStore') + const imapService = req.app.get('imapService') const Helper = require('../../../application/helper') const helper = new Helper() + // Update largest UID before getting stats (if IMAP is ready) + if (imapService) { + const largestUid = await helper.getLargestUid(imapService) + statisticsStore.updateLargestUid(largestUid) + } + const stats = statisticsStore.getStats() const purgeTime = helper.purgeTimeElemetBuilder() - debug(`Stats page requested: ${stats.currentCount} current, ${stats.historicalTotal} historical`) + debug(`Stats page requested: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total`) res.render('stats', { title: `Statistics | ${config.http.branding[0]}`, @@ -34,6 +41,16 @@ router.get('/', async(req, res) => { router.get('/api', async(req, res) => { try { const statisticsStore = req.app.get('statisticsStore') + const imapService = req.app.get('imapService') + const Helper = require('../../../application/helper') + const helper = new Helper() + + // Update largest UID before getting stats (if IMAP is ready) + if (imapService) { + const largestUid = await helper.getLargestUid(imapService) + statisticsStore.updateLargestUid(largestUid) + } + const stats = statisticsStore.getStats() res.json(stats) diff --git a/infrastructure/web/views/stats.twig b/infrastructure/web/views/stats.twig index 31d162c..3d78d19 100644 --- a/infrastructure/web/views/stats.twig +++ b/infrastructure/web/views/stats.twig @@ -45,7 +45,7 @@
-
{{ stats.historicalTotal }}
+
{{ stats.allTimeTotal }}
All Time Total