mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[Feat]: Bring back performance V2
Electric Boogaloo
This commit is contained in:
parent
345935f8b9
commit
197d9b923e
12 changed files with 300 additions and 109 deletions
|
|
@ -50,12 +50,6 @@ const config = {
|
||||||
},
|
},
|
||||||
blacklistedSenders: parseValue(process.env.EMAIL_BLACKLISTED_SENDERS) || [],
|
blacklistedSenders: parseValue(process.env.EMAIL_BLACKLISTED_SENDERS) || [],
|
||||||
features: {
|
features: {
|
||||||
imap: {
|
|
||||||
enabled: true, // IMAP is always required
|
|
||||||
refreshIntervalSeconds: Number(process.env.IMAP_REFRESH_INTERVAL_SECONDS),
|
|
||||||
fetchChunkSize: Number(process.env.IMAP_FETCH_CHUNK) || 100,
|
|
||||||
fetchConcurrency: Number(process.env.IMAP_CONCURRENCY) || 6
|
|
||||||
},
|
|
||||||
smtp: parseBool(process.env.SMTP_ENABLED) || false
|
smtp: parseBool(process.env.SMTP_ENABLED) || false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -309,8 +309,8 @@ class StatisticsStore {
|
||||||
// Calculate emails per hour rate (average across all active hours)
|
// Calculate emails per hour rate (average across all active hours)
|
||||||
const activeHours = hourlyActivity.filter(count => count > 0).length
|
const activeHours = hourlyActivity.filter(count => count > 0).length
|
||||||
const emailsPerHour = activeHours > 0 ?
|
const emailsPerHour = activeHours > 0 ?
|
||||||
(allMails.length / activeHours).toFixed(1) :
|
Math.round(allMails.length / activeHours) :
|
||||||
'0.0'
|
0
|
||||||
|
|
||||||
// Calculate day/night percentage
|
// Calculate day/night percentage
|
||||||
const totalDayNight = dayTimeEmails + nightTimeEmails
|
const totalDayNight = dayTimeEmails + nightTimeEmails
|
||||||
|
|
@ -327,7 +327,7 @@ class StatisticsStore {
|
||||||
uniqueSenderDomains: senderDomains.size,
|
uniqueSenderDomains: senderDomains.size,
|
||||||
uniqueRecipientDomains: recipientDomains.size,
|
uniqueRecipientDomains: recipientDomains.size,
|
||||||
peakHourPercentage,
|
peakHourPercentage,
|
||||||
emailsPerHour: parseFloat(emailsPerHour),
|
emailsPerHour: emailsPerHour,
|
||||||
dayPercentage
|
dayPercentage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@
|
||||||
* Handles Chart.js initialization with historical, real-time, and predicted data
|
* Handles Chart.js initialization with historical, real-time, and predicted data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Store chart instance globally for updates
|
||||||
|
let statsChart = null;
|
||||||
|
let chartContext = null;
|
||||||
|
|
||||||
// Initialize stats chart if on stats page
|
// Initialize stats chart if on stats page
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
const chartCanvas = document.getElementById('statsChart');
|
const chartCanvas = document.getElementById('statsChart');
|
||||||
|
|
@ -20,6 +24,12 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
console.log(`Loaded data: ${historicalData.length} historical, ${realtimeData.length} realtime, ${predictionData.length} predictions`);
|
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
|
||||||
if (typeof io !== 'undefined') {
|
if (typeof io !== 'undefined') {
|
||||||
const socket = io();
|
const socket = io();
|
||||||
|
|
@ -63,6 +73,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|
||||||
// Create gradient for fading effect on historical data
|
// Create gradient for fading effect on historical data
|
||||||
const ctx = chartCanvas.getContext('2d');
|
const ctx = chartCanvas.getContext('2d');
|
||||||
|
chartContext = ctx;
|
||||||
const historicalGradient = ctx.createLinearGradient(0, 0, chartCanvas.width * 0.3, 0);
|
const historicalGradient = ctx.createLinearGradient(0, 0, chartCanvas.width * 0.3, 0);
|
||||||
historicalGradient.addColorStop(0, 'rgba(100, 100, 255, 0.05)');
|
historicalGradient.addColorStop(0, 'rgba(100, 100, 255, 0.05)');
|
||||||
historicalGradient.addColorStop(1, 'rgba(100, 100, 255, 0.15)');
|
historicalGradient.addColorStop(1, 'rgba(100, 100, 255, 0.15)');
|
||||||
|
|
@ -70,7 +81,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
// Track visibility state for each dataset
|
// Track visibility state for each dataset
|
||||||
const datasetVisibility = [true, true, true];
|
const datasetVisibility = [true, true, true];
|
||||||
|
|
||||||
const chart = new Chart(ctx, {
|
statsChart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: 'line',
|
||||||
data: {
|
data: {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
|
|
@ -132,7 +143,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
title: function(context) {
|
title: function(context) {
|
||||||
|
if (!context || !context[0] || context[0].dataIndex === undefined) return '';
|
||||||
const dataIndex = context[0].dataIndex;
|
const dataIndex = context[0].dataIndex;
|
||||||
|
if (!allTimePoints[dataIndex]) return '';
|
||||||
const point = allTimePoints[dataIndex];
|
const point = allTimePoints[dataIndex];
|
||||||
const date = new Date(point.timestamp);
|
const date = new Date(point.timestamp);
|
||||||
return date.toLocaleString('en-US', {
|
return date.toLocaleString('en-US', {
|
||||||
|
|
@ -218,8 +231,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
this.classList.toggle('active');
|
this.classList.toggle('active');
|
||||||
|
|
||||||
// Toggle dataset visibility with fade effect
|
// Toggle dataset visibility with fade effect
|
||||||
const meta = chart.getDatasetMeta(index);
|
const meta = statsChart.getDatasetMeta(index);
|
||||||
const dataset = chart.data.datasets[index];
|
const dataset = statsChart.data.datasets[index];
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
// Fade out
|
// Fade out
|
||||||
|
|
@ -231,7 +244,154 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||||
datasetVisibility[index] = true;
|
datasetVisibility[index] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
chart.update('active');
|
statsChart.update('active');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Lazy load full stats data if placeholder detected
|
||||||
|
lazyLoadStats();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuild chart with new data
|
||||||
|
*/
|
||||||
|
function rebuildStatsChart() {
|
||||||
|
if (!statsChart || !chartContext) {
|
||||||
|
console.log('Chart not initialized, skipping rebuild');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realtimeData = window.initialStatsData || [];
|
||||||
|
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' })),
|
||||||
|
...predictionData.map(d => ({...d, type: 'prediction' }))
|
||||||
|
].sort((a, b) => a.timestamp - b.timestamp);
|
||||||
|
|
||||||
|
if (allTimePoints.length === 0) {
|
||||||
|
console.log('No data points to chart');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create labels
|
||||||
|
const labels = allTimePoints.map(d => {
|
||||||
|
const date = new Date(d.timestamp);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepare datasets
|
||||||
|
const historicalPoints = allTimePoints.map(d => d.type === 'historical' ? d.receives : null);
|
||||||
|
const realtimePoints = allTimePoints.map(d => d.type === 'realtime' ? d.receives : null);
|
||||||
|
const predictionPoints = allTimePoints.map(d => d.type === 'prediction' ? d.receives : null);
|
||||||
|
|
||||||
|
// Update chart data
|
||||||
|
statsChart.data.labels = labels;
|
||||||
|
statsChart.data.datasets[0].data = historicalPoints;
|
||||||
|
statsChart.data.datasets[1].data = realtimePoints;
|
||||||
|
statsChart.data.datasets[2].data = predictionPoints;
|
||||||
|
|
||||||
|
// Update the chart
|
||||||
|
statsChart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy load full statistics data and update DOM
|
||||||
|
*/
|
||||||
|
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');
|
||||||
|
|
||||||
|
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 += `<li class="stat-list-item"><span class="stat-list-label">${item.domain}</span><span class="stat-list-value">${item.count}</span></li>`;
|
||||||
|
});
|
||||||
|
topSenderDomains.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topRecipientDomains && data.enhanced.topRecipientDomains && data.enhanced.topRecipientDomains.length > 0) {
|
||||||
|
let html = '';
|
||||||
|
data.enhanced.topRecipientDomains.slice(0, 5).forEach(item => {
|
||||||
|
html += `<li class="stat-list-item"><span class="stat-list-label">${item.domain}</span><span class="stat-list-value">${item.count}</span></li>`;
|
||||||
|
});
|
||||||
|
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();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading stats:', error);
|
||||||
|
// Stats remain as placeholder
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -498,8 +498,61 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
refreshTimer.textContent = refreshInterval;
|
refreshTimer.textContent = refreshInterval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function initAccountModals() {
|
||||||
|
// Add Email Modal
|
||||||
|
const addEmailBtn = document.getElementById('addEmailBtn');
|
||||||
|
const addEmailModal = document.getElementById('addEmailModal');
|
||||||
|
const closeAddEmail = document.getElementById('closeAddEmail');
|
||||||
|
|
||||||
|
if (addEmailBtn && addEmailModal) {
|
||||||
|
addEmailBtn.onclick = function() {
|
||||||
|
addEmailModal.style.display = 'block';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeAddEmail && addEmailModal) {
|
||||||
|
closeAddEmail.onclick = function() {
|
||||||
|
addEmailModal.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete Account Modal
|
||||||
|
const deleteAccountBtn = document.getElementById('deleteAccountBtn');
|
||||||
|
const deleteAccountModal = document.getElementById('deleteAccountModal');
|
||||||
|
const closeDeleteAccount = document.getElementById('closeDeleteAccount');
|
||||||
|
const cancelDelete = document.getElementById('cancelDelete');
|
||||||
|
|
||||||
|
if (deleteAccountBtn && deleteAccountModal) {
|
||||||
|
deleteAccountBtn.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'block';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeDeleteAccount && deleteAccountModal) {
|
||||||
|
closeDeleteAccount.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelDelete && deleteAccountModal) {
|
||||||
|
cancelDelete.onclick = function() {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Window click handler for both modals
|
||||||
|
window.addEventListener('click', function(e) {
|
||||||
|
if (addEmailModal && e.target === addEmailModal) {
|
||||||
|
addEmailModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
if (deleteAccountModal && e.target === deleteAccountModal) {
|
||||||
|
deleteAccountModal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Expose utilities and run them
|
// Expose utilities and run them
|
||||||
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown, initCryptoKeysToggle, initForwardModal, initForwardAllModal };
|
window.utils = { formatEmailDates, formatMailDate, initLockModals, initCopyAddress, initExpiryTimers, initQrModal, initHamburgerMenu, initThemeToggle, initRefreshCountdown, initCryptoKeysToggle, initForwardModal, initForwardAllModal, initAccountModals };
|
||||||
formatEmailDates();
|
formatEmailDates();
|
||||||
formatMailDate();
|
formatMailDate();
|
||||||
initLockModals();
|
initLockModals();
|
||||||
|
|
@ -509,4 +562,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
initThemeToggle();
|
initThemeToggle();
|
||||||
initForwardModal();
|
initForwardModal();
|
||||||
initCryptoKeysToggle();
|
initCryptoKeysToggle();
|
||||||
|
initAccountModals();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2115,6 +2115,7 @@ label {
|
||||||
}
|
}
|
||||||
|
|
||||||
.close {
|
.close {
|
||||||
|
text-align: right;
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ router.get('/account', requireAuth, async(req, res) => {
|
||||||
stats,
|
stats,
|
||||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
|
smtpEnabled: config.email.features.smtp,
|
||||||
successMessage: req.session.accountSuccess,
|
successMessage: req.session.accountSuccess,
|
||||||
errorMessage: req.session.accountError
|
errorMessage: req.session.accountError
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ router.get('/auth', redirectIfAuthenticated, (req, res) => {
|
||||||
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
|
title: `Login or Register | ${(config.http.features.branding || ['48hr.email'])[0]}`,
|
||||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||||
purgeTime: purgeTime,
|
purgeTime: purgeTime,
|
||||||
|
smtpEnabled: config.email.features.smtp,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
successMessage
|
successMessage
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ const express = require('express')
|
||||||
const router = new express.Router()
|
const router = new express.Router()
|
||||||
const debug = require('debug')('48hr-email:stats-routes')
|
const debug = require('debug')('48hr-email:stats-routes')
|
||||||
|
|
||||||
// GET /stats - Statistics page
|
// GET /stats - Statistics page with lazy loading
|
||||||
router.get('/', async(req, res) => {
|
router.get('/', async(req, res) => {
|
||||||
try {
|
try {
|
||||||
const config = req.app.get('config')
|
const config = req.app.get('config')
|
||||||
|
|
@ -16,6 +16,61 @@ router.get('/', async(req, res) => {
|
||||||
return res.redirect(redirectUrl)
|
return res.redirect(redirectUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Helper = require('../../../application/helper')
|
||||||
|
const helper = new Helper()
|
||||||
|
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
||||||
|
const purgeTime = helper.purgeTimeElemetBuilder()
|
||||||
|
|
||||||
|
// Return page with placeholder data immediately - real data loads via JS
|
||||||
|
const placeholderStats = {
|
||||||
|
currentCount: '...',
|
||||||
|
allTimeTotal: '...',
|
||||||
|
last24Hours: {
|
||||||
|
receives: '...',
|
||||||
|
deletes: '...',
|
||||||
|
forwards: '...',
|
||||||
|
timeline: []
|
||||||
|
},
|
||||||
|
enhanced: {
|
||||||
|
topSenderDomains: [],
|
||||||
|
topRecipientDomains: [],
|
||||||
|
uniqueSenderDomains: '...',
|
||||||
|
averageEmailSize: '...',
|
||||||
|
peakHour: '...',
|
||||||
|
prediction24h: '...'
|
||||||
|
},
|
||||||
|
historical: [],
|
||||||
|
prediction: []
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(`Stats page requested - returning with lazy loading`)
|
||||||
|
|
||||||
|
res.render('stats', {
|
||||||
|
title: `Statistics | ${branding[0]}`,
|
||||||
|
branding: branding,
|
||||||
|
purgeTime: purgeTime,
|
||||||
|
stats: placeholderStats,
|
||||||
|
authEnabled: config.user.authEnabled,
|
||||||
|
currentUser: req.session && req.session.username,
|
||||||
|
lazyLoad: true
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
debug(`Error loading stats page: ${error.message}`)
|
||||||
|
console.error('Error while loading stats page', error)
|
||||||
|
res.status(500).send('Error loading statistics')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// GET /stats/api - JSON API for lazy-loaded stats (full calculation)
|
||||||
|
router.get('/api', async(req, res) => {
|
||||||
|
try {
|
||||||
|
const config = req.app.get('config')
|
||||||
|
|
||||||
|
// Check if statistics are enabled
|
||||||
|
if (!config.http.features.statistics) {
|
||||||
|
return res.status(403).json({ error: 'Statistics are disabled' })
|
||||||
|
}
|
||||||
|
|
||||||
const statisticsStore = req.app.get('statisticsStore')
|
const statisticsStore = req.app.get('statisticsStore')
|
||||||
const imapService = req.app.get('imapService')
|
const imapService = req.app.get('imapService')
|
||||||
const mailProcessingService = req.app.get('mailProcessingService')
|
const mailProcessingService = req.app.get('mailProcessingService')
|
||||||
|
|
@ -36,46 +91,13 @@ router.get('/', async(req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = statisticsStore.getEnhancedStats()
|
const stats = statisticsStore.getEnhancedStats()
|
||||||
const purgeTime = helper.purgeTimeElemetBuilder()
|
|
||||||
const branding = config.http.features.branding || ['48hr.email', 'Service', 'https://example.com']
|
|
||||||
|
|
||||||
debug(`Stats page requested: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total, ${stats.historical.length} historical points`)
|
debug(`Stats API returned: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total`)
|
||||||
|
|
||||||
res.render('stats', {
|
|
||||||
title: `Statistics | ${branding[0]}`,
|
|
||||||
branding: branding,
|
|
||||||
purgeTime: purgeTime,
|
|
||||||
stats: stats,
|
|
||||||
authEnabled: config.user.authEnabled,
|
|
||||||
currentUser: req.session && req.session.username
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
debug(`Error loading stats page: ${error.message}`)
|
|
||||||
console.error('Error while loading stats page', error)
|
|
||||||
res.status(500).send('Error loading statistics')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// GET /stats/api - JSON API for real-time updates
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use lightweight stats - no historical analysis on API calls
|
|
||||||
const stats = statisticsStore.getLightweightStats()
|
|
||||||
|
|
||||||
res.json(stats)
|
res.json(stats)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
debug(`Error fetching stats API: ${error.message}`)
|
debug(`Error fetching stats API: ${error.message}`)
|
||||||
|
console.error('Stats API error:', error)
|
||||||
res.status(500).json({ error: 'Failed to fetch statistics' })
|
res.status(500).json({ error: 'Failed to fetch statistics' })
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ class TemplateContext {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.helper = new Helper()
|
this.helper = new Helper()
|
||||||
this.purgeTime = this.helper.purgeTimeElemetBuilder()
|
this.purgeTime = this.helper.purgeTimeElemetBuilder()
|
||||||
|
// Cache domains to avoid reprocessing on every request
|
||||||
|
this.cachedDomains = this.helper.getDomains()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -34,7 +36,7 @@ class TemplateContext {
|
||||||
currentUser: req.session && req.session.username ? req.session.username : null,
|
currentUser: req.session && req.session.username ? req.session.username : null,
|
||||||
|
|
||||||
// Common data
|
// Common data
|
||||||
domains: this.helper.getDomains(),
|
domains: this.cachedDomains,
|
||||||
example: config.email.examples.account
|
example: config.email.examples.account
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,12 @@
|
||||||
<div class="account-card frosted-glass">
|
<div class="account-card frosted-glass">
|
||||||
<h2>Account Overview</h2>
|
<h2>Account Overview</h2>
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
|
{% if smtpEnabled %}
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-value">{{ stats.forwardEmailsCount }}/{{ stats.maxForwardEmails }}</div>
|
<div class="stat-value">{{ stats.forwardEmailsCount }}/{{ stats.maxForwardEmails }}</div>
|
||||||
<div class="stat-label">Forward Emails</div>
|
<div class="stat-label">Forward Emails</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="stat-item">
|
<div class="stat-item">
|
||||||
<div class="stat-value">{{ stats.lockedInboxesCount }}/{{ stats.maxLockedInboxes }}</div>
|
<div class="stat-value">{{ stats.lockedInboxesCount }}/{{ stats.maxLockedInboxes }}</div>
|
||||||
<div class="stat-label">Locked Inboxes</div>
|
<div class="stat-label">Locked Inboxes</div>
|
||||||
|
|
@ -59,6 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if smtpEnabled %}
|
||||||
<!-- Forwarding Emails Section -->
|
<!-- Forwarding Emails Section -->
|
||||||
<div class="account-card frosted-glass">
|
<div class="account-card frosted-glass">
|
||||||
<h2>Forwarding Emails</h2>
|
<h2>Forwarding Emails</h2>
|
||||||
|
|
@ -91,6 +94,7 @@
|
||||||
<p class="limit-reached">Maximum {{ stats.maxForwardEmails }} emails reached</p>
|
<p class="limit-reached">Maximum {{ stats.maxForwardEmails }} emails reached</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Locked Inboxes Section -->
|
<!-- Locked Inboxes Section -->
|
||||||
<div class="account-card frosted-glass">
|
<div class="account-card frosted-glass">
|
||||||
|
|
@ -178,7 +182,7 @@
|
||||||
<div class="danger-content">
|
<div class="danger-content">
|
||||||
<p><strong>Warning:</strong> Deleting your account will:</p>
|
<p><strong>Warning:</strong> Deleting your account will:</p>
|
||||||
<ul class="danger-list">
|
<ul class="danger-list">
|
||||||
<li>Remove all forwarding email addresses</li>
|
{% if smtpEnabled %}<li>Remove all forwarding email addresses</li>{% endif %}
|
||||||
<li>Release all locked inboxes</li>
|
<li>Release all locked inboxes</li>
|
||||||
<li>Permanently delete your account data</li>
|
<li>Permanently delete your account data</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -226,6 +230,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if smtpEnabled %}
|
||||||
<!-- Add Email Modal -->
|
<!-- Add Email Modal -->
|
||||||
<div id="addEmailModal" class="modal">
|
<div id="addEmailModal" class="modal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|
@ -249,56 +254,5 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<script>
|
|
||||||
// Add Email Modal
|
|
||||||
const addEmailBtn = document.getElementById('addEmailBtn');
|
|
||||||
const addEmailModal = document.getElementById('addEmailModal');
|
|
||||||
const closeAddEmail = document.getElementById('closeAddEmail');
|
|
||||||
|
|
||||||
if (addEmailBtn) {
|
|
||||||
addEmailBtn.onclick = function() {
|
|
||||||
addEmailModal.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeAddEmail) {
|
|
||||||
closeAddEmail.onclick = function() {
|
|
||||||
addEmailModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete Account Modal
|
|
||||||
const deleteAccountBtn = document.getElementById('deleteAccountBtn');
|
|
||||||
const deleteAccountModal = document.getElementById('deleteAccountModal');
|
|
||||||
const closeDeleteAccount = document.getElementById('closeDeleteAccount');
|
|
||||||
const cancelDelete = document.getElementById('cancelDelete');
|
|
||||||
|
|
||||||
if (deleteAccountBtn) {
|
|
||||||
deleteAccountBtn.onclick = function() {
|
|
||||||
deleteAccountModal.style.display = 'block';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (closeDeleteAccount) {
|
|
||||||
closeDeleteAccount.onclick = function() {
|
|
||||||
deleteAccountModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelDelete) {
|
|
||||||
cancelDelete.onclick = function() {
|
|
||||||
deleteAccountModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.onclick = function(event) {
|
|
||||||
if (event.target == addEmailModal) {
|
|
||||||
addEmailModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
if (event.target == deleteAccountModal) {
|
|
||||||
deleteAccountModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,11 @@
|
||||||
<div class="auth-features-unified">
|
<div class="auth-features-unified">
|
||||||
<h3>Account Benefits</h3>
|
<h3>Account Benefits</h3>
|
||||||
<div class="features-grid">
|
<div class="features-grid">
|
||||||
|
{% if smtpEnabled %}
|
||||||
<div class="feature-item">Forward emails to verified addresses</div>
|
<div class="feature-item">Forward emails to verified addresses</div>
|
||||||
<div class="feature-item">Lock up to 5 inboxes to your account</div>
|
|
||||||
<div class="feature-item">Manage multiple forwarding destinations</div>
|
<div class="feature-item">Manage multiple forwarding destinations</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="feature-item">Lock up to 5 inboxes to your account</div>
|
||||||
<div class="feature-item">Access your locked inboxes anywhere</div>
|
<div class="feature-item">Access your locked inboxes anywhere</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="guest-note">No account needed for basic temporary inboxes • <a href="/">Browse as guest</a></p>
|
<p class="guest-note">No account needed for basic temporary inboxes • <a href="/">Browse as guest</a></p>
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
Top Sender Domains
|
Top Sender Domains
|
||||||
</h3>
|
</h3>
|
||||||
{% if stats.enhanced.topSenderDomains|length > 0 %}
|
{% if stats.enhanced.topSenderDomains|length > 0 %}
|
||||||
<ul class="stat-list">
|
<ul class="stat-list" data-stats="top-sender-domains">
|
||||||
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
|
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
|
||||||
<li class="stat-list-item">
|
<li class="stat-list-item">
|
||||||
<span class="stat-list-label">{{ item.domain }}</span>
|
<span class="stat-list-label">{{ item.domain }}</span>
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p class="stat-footer">{{ stats.enhanced.uniqueSenderDomains }} unique domains</p>
|
<p class="stat-footer"><span data-stats="unique-sender-domains">{{ stats.enhanced.uniqueSenderDomains }}</span> unique domains</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="stat-empty">No data yet</p>
|
<p class="stat-empty">No data yet</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
Top Recipient Domains
|
Top Recipient Domains
|
||||||
</h3>
|
</h3>
|
||||||
{% if stats.enhanced.topRecipientDomains|length > 0 %}
|
{% if stats.enhanced.topRecipientDomains|length > 0 %}
|
||||||
<ul class="stat-list">
|
<ul class="stat-list" data-stats="top-recipient-domains">
|
||||||
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
|
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
|
||||||
<li class="stat-list-item">
|
<li class="stat-list-item">
|
||||||
<span class="stat-list-label">{{ item.domain }}</span>
|
<span class="stat-list-label">{{ item.domain }}</span>
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<p class="stat-footer">{{ stats.enhanced.uniqueRecipientDomains }} unique domains</p>
|
<p class="stat-footer"><span data-stats="unique-recipient-domains">{{ stats.enhanced.uniqueRecipientDomains }}</span> unique domains</p>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="stat-empty">No data yet</p>
|
<p class="stat-empty">No data yet</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue