mirror of
https://github.com/Crazyco-xyz/48hr.email.git
synced 2026-01-09 11:19:36 +01:00
[AI][Feat]: StatisticsStore V3
This commit is contained in:
parent
847092e866
commit
d454f91912
4 changed files with 385 additions and 1 deletions
|
|
@ -26,6 +26,11 @@ class StatisticsStore {
|
||||||
this.lastAnalysisTime = 0
|
this.lastAnalysisTime = 0
|
||||||
this.analysisCacheDuration = 5 * 60 * 1000 // Cache for 5 minutes
|
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
|
// Load persisted data if database is available
|
||||||
if (this.db) {
|
if (this.db) {
|
||||||
this._loadFromDatabase()
|
this._loadFromDatabase()
|
||||||
|
|
@ -198,6 +203,138 @@ class StatisticsStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate enhanced statistics from current emails
|
||||||
|
* Privacy-friendly: uses domain analysis, time patterns, and aggregates
|
||||||
|
* @param {Array} allMails - Array of all mail summaries
|
||||||
|
*/
|
||||||
|
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`)
|
||||||
|
|
||||||
|
// Track sender domains (privacy-friendly: domain only, not full address)
|
||||||
|
const senderDomains = new Map()
|
||||||
|
const recipientDomains = new Map()
|
||||||
|
const hourlyActivity = Array(24).fill(0)
|
||||||
|
let totalSubjectLength = 0
|
||||||
|
let subjectCount = 0
|
||||||
|
let withAttachments = 0
|
||||||
|
let dayTimeEmails = 0 // 6am-6pm
|
||||||
|
let nightTimeEmails = 0 // 6pm-6am
|
||||||
|
|
||||||
|
allMails.forEach(mail => {
|
||||||
|
try {
|
||||||
|
// Sender domain analysis
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipient domain analysis
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hourly activity pattern
|
||||||
|
if (mail.date) {
|
||||||
|
const date = new Date(mail.date)
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
const hour = date.getHours()
|
||||||
|
hourlyActivity[hour]++
|
||||||
|
|
||||||
|
// Day vs night distribution (6am-6pm = day, 6pm-6am = night)
|
||||||
|
if (hour >= 6 && hour < 18) {
|
||||||
|
dayTimeEmails++
|
||||||
|
} else {
|
||||||
|
nightTimeEmails++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subject length analysis (privacy-friendly: only length, not content)
|
||||||
|
if (mail.subject) {
|
||||||
|
totalSubjectLength += mail.subject.length
|
||||||
|
subjectCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if email likely has attachments (would need full fetch to confirm)
|
||||||
|
// For now, we'll track this separately when we fetch full emails
|
||||||
|
} catch (e) {
|
||||||
|
// Skip invalid entries
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get top sender domains (limit to top 10)
|
||||||
|
const topSenderDomains = Array.from(senderDomains.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([domain, count]) => ({ domain, count }))
|
||||||
|
|
||||||
|
// Get top recipient domains
|
||||||
|
const topRecipientDomains = Array.from(recipientDomains.entries())
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 10)
|
||||||
|
.map(([domain, count]) => ({ domain, count }))
|
||||||
|
|
||||||
|
// Find busiest hours (top 5)
|
||||||
|
const busiestHours = hourlyActivity
|
||||||
|
.map((count, hour) => ({ hour, count }))
|
||||||
|
.filter(h => h.count > 0)
|
||||||
|
.sort((a, b) => b.count - a.count)
|
||||||
|
.slice(0, 5)
|
||||||
|
|
||||||
|
// Calculate peak hour concentration (% of emails in busiest hour)
|
||||||
|
const peakHourCount = busiestHours.length > 0 ? busiestHours[0].count : 0
|
||||||
|
const peakHourPercentage = allMails.length > 0 ?
|
||||||
|
Math.round((peakHourCount / allMails.length) * 100) :
|
||||||
|
0
|
||||||
|
|
||||||
|
// 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'
|
||||||
|
|
||||||
|
// Calculate day/night percentage
|
||||||
|
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: parseFloat(emailsPerHour),
|
||||||
|
dayPercentage
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastEnhancedStatsTime = now
|
||||||
|
debug(`Enhanced stats calculated: ${this.enhancedStats.uniqueSenderDomains} unique sender domains, ${this.enhancedStats.busiestHours.length} busy hours`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Analyze all existing emails to build historical statistics
|
* Analyze all existing emails to build historical statistics
|
||||||
* @param {Array} allMails - Array of all mail summaries with date property
|
* @param {Array} allMails - Array of all mail summaries with date property
|
||||||
|
|
@ -276,7 +413,8 @@ class StatisticsStore {
|
||||||
timeline: timeline
|
timeline: timeline
|
||||||
},
|
},
|
||||||
historical: historicalTimeline,
|
historical: historicalTimeline,
|
||||||
prediction: prediction
|
prediction: prediction,
|
||||||
|
enhanced: this.enhancedStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2552,6 +2552,7 @@ body.light-mode .theme-icon-light {
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
height: 550px;
|
height: 550px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-top: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-container h2 {
|
.chart-container h2 {
|
||||||
|
|
@ -2642,6 +2643,127 @@ body.light-mode .theme-icon-light {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Enhanced Statistics Cards */
|
||||||
|
|
||||||
|
.enhanced-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-detailed {
|
||||||
|
background: var(--overlay-white-05);
|
||||||
|
border: 1px solid var(--overlay-purple-30);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 2rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card-detailed:hover {
|
||||||
|
background: var(--overlay-white-08);
|
||||||
|
border-color: var(--overlay-purple-40);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header-small {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0 0 1.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-light);
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
border-bottom: 2px solid var(--overlay-purple-20);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-bottom: 1px solid var(--overlay-white-05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list-label {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
color: var(--color-text-primary);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-list-value {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
min-width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-footer {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid var(--overlay-white-08);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
padding: 2rem 0;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--overlay-white-03);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stat-item:hover {
|
||||||
|
background: var(--overlay-purple-08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stat-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--color-accent-purple-light);
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--color-text-dim);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.chart-legend-custom {
|
.chart-legend-custom {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -2697,6 +2819,32 @@ body.light-mode .theme-icon-light {
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
/* Enhanced stats mobile */
|
||||||
|
.enhanced-stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
.stat-card-detailed {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.section-header-small {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
.stat-list-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
max-width: 65%;
|
||||||
|
}
|
||||||
|
.stat-list-value {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.quick-stats {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.quick-stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
.action-links {
|
.action-links {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ router.get('/', async(req, res) => {
|
||||||
if (mailProcessingService) {
|
if (mailProcessingService) {
|
||||||
const allMails = mailProcessingService.getAllMailSummaries()
|
const allMails = mailProcessingService.getAllMailSummaries()
|
||||||
statisticsStore.analyzeHistoricalData(allMails)
|
statisticsStore.analyzeHistoricalData(allMails)
|
||||||
|
statisticsStore.calculateEnhancedStatistics(allMails)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = statisticsStore.getEnhancedStats()
|
const stats = statisticsStore.getEnhancedStats()
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,103 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if stats.enhanced %}
|
||||||
|
<!-- Enhanced Statistics Grid -->
|
||||||
|
<div class="enhanced-stats-grid">
|
||||||
|
<!-- Top Sender Domains -->
|
||||||
|
<div class="stat-card-detailed">
|
||||||
|
<h3 class="section-header-small">
|
||||||
|
Top Sender Domains
|
||||||
|
</h3>
|
||||||
|
{% if stats.enhanced.topSenderDomains|length > 0 %}
|
||||||
|
<ul class="stat-list">
|
||||||
|
{% for item in stats.enhanced.topSenderDomains|slice(0, 5) %}
|
||||||
|
<li class="stat-list-item">
|
||||||
|
<span class="stat-list-label">{{ item.domain }}</span>
|
||||||
|
<span class="stat-list-value">{{ item.count }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="stat-footer">{{ stats.enhanced.uniqueSenderDomains }} unique domains</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="stat-empty">No data yet</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Top Recipient Domains -->
|
||||||
|
<div class="stat-card-detailed">
|
||||||
|
<h3 class="section-header-small">
|
||||||
|
Top Recipient Domains
|
||||||
|
</h3>
|
||||||
|
{% if stats.enhanced.topRecipientDomains|length > 0 %}
|
||||||
|
<ul class="stat-list">
|
||||||
|
{% for item in stats.enhanced.topRecipientDomains|slice(0, 5) %}
|
||||||
|
<li class="stat-list-item">
|
||||||
|
<span class="stat-list-label">{{ item.domain }}</span>
|
||||||
|
<span class="stat-list-value">{{ item.count }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<p class="stat-footer">{{ stats.enhanced.uniqueRecipientDomains }} unique domains</p>
|
||||||
|
{% else %}
|
||||||
|
<p class="stat-empty">No data yet</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Busiest Hours -->
|
||||||
|
<div class="stat-card-detailed">
|
||||||
|
<h3 class="section-header-small">
|
||||||
|
Busiest Hours
|
||||||
|
</h3>
|
||||||
|
{% if stats.enhanced.busiestHours|length > 0 %}
|
||||||
|
<ul class="stat-list">
|
||||||
|
{% for item in stats.enhanced.busiestHours %}
|
||||||
|
<li class="stat-list-item">
|
||||||
|
<span class="stat-list-label">{{ item.hour }}:00 - {{ item.hour + 1 }}:00</span>
|
||||||
|
<span class="stat-list-value">{{ item.count }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="stat-empty">No data yet</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="stat-card-detailed">
|
||||||
|
<h3 class="section-header-small">
|
||||||
|
Quick Insights
|
||||||
|
</h3>
|
||||||
|
<div class="quick-stats">
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.averageSubjectLength }}</div>
|
||||||
|
<div class="quick-stat-label">Avg Subject Length</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.uniqueSenderDomains }}</div>
|
||||||
|
<div class="quick-stat-label">Unique Senders</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.uniqueRecipientDomains }}</div>
|
||||||
|
<div class="quick-stat-label">Unique Recipients</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.peakHourPercentage }}%</div>
|
||||||
|
<div class="quick-stat-label">Peak Hour Traffic</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.emailsPerHour }}</div>
|
||||||
|
<div class="quick-stat-label">Emails per Hour</div>
|
||||||
|
</div>
|
||||||
|
<div class="quick-stat-item">
|
||||||
|
<div class="quick-stat-value">{{ stats.enhanced.dayPercentage }}%</div>
|
||||||
|
<div class="quick-stat-label">Daytime (6am-6pm)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="chart-container">
|
<div class="chart-container">
|
||||||
<h2>Email Activity Timeline</h2>
|
<h2>Email Activity Timeline</h2>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue