Files
archivmail/internal/storage/storage_stats.go
T
sysops 58bcfb1586 feat: Dashboard-Metriken nach Mailpiler-Vorbild (Uptime, Aktivität, Prognose)
- storage_stats.go (neu): MailActivityStats (60min/24h/7d/30d), StorageEstimateStats
- dashboard_handlers.go: Uptime (/proc/uptime), activity + estimate in System-Stats-Response
- DashboardTab: Uptime in API-Kachel, neue Kacheln "Mail-Eingang" + "Speicherprognose"
- Warnung (Badge "Knapp!") wenn Partition in <90 Tagen voll

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 10:44:02 +02:00

89 lines
2.5 KiB
Go

package storage
import (
"context"
"time"
)
// MailActivity holds mail counts over different time windows.
type MailActivity struct {
Last60Min int64 `json:"last_60_min"`
Last24h int64 `json:"last_24h"`
Last7d int64 `json:"last_7d"`
Last30d int64 `json:"last_30d"`
}
// StorageEstimate holds a rough prediction of when disk space will run out.
type StorageEstimate struct {
AvgMailsPerDay float64 `json:"avg_mails_per_day"`
AvgMailBytes int64 `json:"avg_mail_bytes"`
DaysUntilFull int64 `json:"days_until_full"` // -1 = unknown/infinite
ArchiveAgeDays int64 `json:"archive_age_days"`
}
// MailActivityStats returns how many mails arrived in the last 60min / 24h / 7d / 30d.
func (s *Store) MailActivityStats(ctx context.Context) (*MailActivity, error) {
if s.db == nil {
return &MailActivity{}, nil
}
now := time.Now()
windows := []time.Duration{
60 * time.Minute,
24 * time.Hour,
7 * 24 * time.Hour,
30 * 24 * time.Hour,
}
counts := make([]int64, len(windows))
for i, w := range windows {
cutoff := now.Add(-w)
row := s.db.QueryRow(ctx,
`SELECT COUNT(*) FROM emails WHERE received_at >= $1`, cutoff)
_ = row.Scan(&counts[i])
}
return &MailActivity{
Last60Min: counts[0],
Last24h: counts[1],
Last7d: counts[2],
Last30d: counts[3],
}, nil
}
// StorageEstimateStats calculates avg mails/day and avg mail size from the DB.
// freeDiskBytes: free bytes on the archive partition (passed in from caller).
func (s *Store) StorageEstimateStats(ctx context.Context, freeDiskBytes uint64) (*StorageEstimate, error) {
if s.db == nil {
return &StorageEstimate{DaysUntilFull: -1}, nil
}
var totalMails, totalBytes int64
var firstReceived *time.Time
if err := s.db.QueryRow(ctx,
`SELECT COUNT(*), COALESCE(SUM(size_bytes), 0), MIN(received_at) FROM emails`,
).Scan(&totalMails, &totalBytes, &firstReceived); err != nil || totalMails == 0 {
return &StorageEstimate{DaysUntilFull: -1}, nil
}
ageDays := int64(1)
if firstReceived != nil {
d := time.Since(*firstReceived).Hours() / 24
if d > 1 {
ageDays = int64(d)
}
}
avgMailsPerDay := float64(totalMails) / float64(ageDays)
avgMailBytes := totalBytes / totalMails
daysUntilFull := int64(-1)
if avgMailsPerDay > 0 && avgMailBytes > 0 && freeDiskBytes > 0 {
daysUntilFull = int64(float64(freeDiskBytes) / (avgMailsPerDay * float64(avgMailBytes)))
}
return &StorageEstimate{
AvgMailsPerDay: avgMailsPerDay,
AvgMailBytes: avgMailBytes,
DaysUntilFull: daysUntilFull,
ArchiveAgeDays: ageDays,
}, nil
}