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) || [],
|
||||
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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -309,8 +309,8 @@ class StatisticsStore {
|
|||
// Calculate emails per hour rate (average across all active hours)
|
||||
const activeHours = hourlyActivity.filter(count => count > 0).length
|
||||
const emailsPerHour = activeHours > 0 ?
|
||||
(allMails.length / activeHours).toFixed(1) :
|
||||
'0.0'
|
||||
Math.round(allMails.length / activeHours) :
|
||||
0
|
||||
|
||||
// Calculate day/night percentage
|
||||
const totalDayNight = dayTimeEmails + nightTimeEmails
|
||||
|
|
@ -327,7 +327,7 @@ class StatisticsStore {
|
|||
uniqueSenderDomains: senderDomains.size,
|
||||
uniqueRecipientDomains: recipientDomains.size,
|
||||
peakHourPercentage,
|
||||
emailsPerHour: parseFloat(emailsPerHour),
|
||||
emailsPerHour: emailsPerHour,
|
||||
dayPercentage
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@
|
|||
* 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
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
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`);
|
||||
|
||||
// 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
|
||||
if (typeof io !== 'undefined') {
|
||||
const socket = io();
|
||||
|
|
@ -63,6 +73,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// Create gradient for fading effect on historical data
|
||||
const ctx = chartCanvas.getContext('2d');
|
||||
chartContext = ctx;
|
||||
const historicalGradient = ctx.createLinearGradient(0, 0, chartCanvas.width * 0.3, 0);
|
||||
historicalGradient.addColorStop(0, 'rgba(100, 100, 255, 0.05)');
|
||||
historicalGradient.addColorStop(1, 'rgba(100, 100, 255, 0.15)');
|
||||
|
|
@ -70,7 +81,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Track visibility state for each dataset
|
||||
const datasetVisibility = [true, true, true];
|
||||
|
||||
const chart = new Chart(ctx, {
|
||||
statsChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
|
|
@ -132,7 +143,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
intersect: false,
|
||||
callbacks: {
|
||||
title: function(context) {
|
||||
if (!context || !context[0] || context[0].dataIndex === undefined) return '';
|
||||
const dataIndex = context[0].dataIndex;
|
||||
if (!allTimePoints[dataIndex]) return '';
|
||||
const point = allTimePoints[dataIndex];
|
||||
const date = new Date(point.timestamp);
|
||||
return date.toLocaleString('en-US', {
|
||||
|
|
@ -218,8 +231,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
this.classList.toggle('active');
|
||||
|
||||
// Toggle dataset visibility with fade effect
|
||||
const meta = chart.getDatasetMeta(index);
|
||||
const dataset = chart.data.datasets[index];
|
||||
const meta = statsChart.getDatasetMeta(index);
|
||||
const dataset = statsChart.data.datasets[index];
|
||||
|
||||
if (isActive) {
|
||||
// Fade out
|
||||
|
|
@ -231,7 +244,154 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
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;
|
||||
}
|
||||
|
||||
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
|
||||
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();
|
||||
formatMailDate();
|
||||
initLockModals();
|
||||
|
|
@ -509,4 +562,5 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||
initThemeToggle();
|
||||
initForwardModal();
|
||||
initCryptoKeysToggle();
|
||||
initAccountModals();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -2115,6 +2115,7 @@ label {
|
|||
}
|
||||
|
||||
.close {
|
||||
text-align: right;
|
||||
float: right;
|
||||
font-size: 2.8rem;
|
||||
font-weight: bold;
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ router.get('/account', requireAuth, async(req, res) => {
|
|||
stats,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
purgeTime: purgeTime,
|
||||
smtpEnabled: config.email.features.smtp,
|
||||
successMessage: req.session.accountSuccess,
|
||||
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]}`,
|
||||
branding: config.http.features.branding || ['48hr.email', 'Service', 'https://example.com'],
|
||||
purgeTime: purgeTime,
|
||||
smtpEnabled: config.email.features.smtp,
|
||||
errorMessage,
|
||||
successMessage
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ const express = require('express')
|
|||
const router = new express.Router()
|
||||
const debug = require('debug')('48hr-email:stats-routes')
|
||||
|
||||
// GET /stats - Statistics page
|
||||
// GET /stats - Statistics page with lazy loading
|
||||
router.get('/', async(req, res) => {
|
||||
try {
|
||||
const config = req.app.get('config')
|
||||
|
|
@ -16,6 +16,61 @@ router.get('/', async(req, res) => {
|
|||
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 imapService = req.app.get('imapService')
|
||||
const mailProcessingService = req.app.get('mailProcessingService')
|
||||
|
|
@ -36,46 +91,13 @@ router.get('/', async(req, res) => {
|
|||
}
|
||||
|
||||
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`)
|
||||
|
||||
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()
|
||||
debug(`Stats API returned: ${stats.currentCount} current, ${stats.allTimeTotal} all-time total`)
|
||||
|
||||
res.json(stats)
|
||||
} catch (error) {
|
||||
debug(`Error fetching stats API: ${error.message}`)
|
||||
console.error('Stats API error:', error)
|
||||
res.status(500).json({ error: 'Failed to fetch statistics' })
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ class TemplateContext {
|
|||
constructor() {
|
||||
this.helper = new Helper()
|
||||
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,
|
||||
|
||||
// Common data
|
||||
domains: this.helper.getDomains(),
|
||||
domains: this.cachedDomains,
|
||||
example: config.email.examples.account
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,10 +44,12 @@
|
|||
<div class="account-card frosted-glass">
|
||||
<h2>Account Overview</h2>
|
||||
<div class="stats-grid">
|
||||
{% if smtpEnabled %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.forwardEmailsCount }}/{{ stats.maxForwardEmails }}</div>
|
||||
<div class="stat-label">Forward Emails</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="stat-item">
|
||||
<div class="stat-value">{{ stats.lockedInboxesCount }}/{{ stats.maxLockedInboxes }}</div>
|
||||
<div class="stat-label">Locked Inboxes</div>
|
||||
|
|
@ -59,6 +61,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if smtpEnabled %}
|
||||
<!-- Forwarding Emails Section -->
|
||||
<div class="account-card frosted-glass">
|
||||
<h2>Forwarding Emails</h2>
|
||||
|
|
@ -91,6 +94,7 @@
|
|||
<p class="limit-reached">Maximum {{ stats.maxForwardEmails }} emails reached</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Locked Inboxes Section -->
|
||||
<div class="account-card frosted-glass">
|
||||
|
|
@ -178,7 +182,7 @@
|
|||
<div class="danger-content">
|
||||
<p><strong>Warning:</strong> Deleting your account will:</p>
|
||||
<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>Permanently delete your account data</li>
|
||||
</ul>
|
||||
|
|
@ -226,6 +230,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{% if smtpEnabled %}
|
||||
<!-- Add Email Modal -->
|
||||
<div id="addEmailModal" class="modal">
|
||||
<div class="modal-content">
|
||||
|
|
@ -249,56 +254,5 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -123,9 +123,11 @@
|
|||
<div class="auth-features-unified">
|
||||
<h3>Account Benefits</h3>
|
||||
<div class="features-grid">
|
||||
{% if smtpEnabled %}
|
||||
<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>
|
||||
{% 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>
|
||||
<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
|
||||
</h3>
|
||||
{% 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) %}
|
||||
<li class="stat-list-item">
|
||||
<span class="stat-list-label">{{ item.domain }}</span>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
<p class="stat-empty">No data yet</p>
|
||||
{% endif %}
|
||||
|
|
@ -117,7 +117,7 @@
|
|||
Top Recipient Domains
|
||||
</h3>
|
||||
{% 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) %}
|
||||
<li class="stat-list-item">
|
||||
<span class="stat-list-label">{{ item.domain }}</span>
|
||||
|
|
@ -125,7 +125,7 @@
|
|||
</li>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
<p class="stat-empty">No data yet</p>
|
||||
{% endif %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue