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 @@