Files
warmbly/internal/models/analytics.go
T
Matthew Meszaros 9f23c663c4 feat: give a cold mailbox a rotation lifecycle so a tired one can rest (#237)
* feat: give a cold mailbox a rotation lifecycle so a tired one can rest and come back, instead of running at full volume until a hard band trips: send_lifecycle is warming, active, resting or reserve and decides whether cold sender resolution offers the mailbox at all, which is a different axis from risk_band deciding which worker and IP host it, so a resting mailbox is still a clean-band mailbox that keeps its warmup traffic and its reputation; the hourly rebalancer rests a mailbox at throttled and worse but never at watch, since watch is defined as the band that changes nothing a customer can feel and leaving cold rotation is very much something they feel, and a rested mailbox returns only after three clean days so one good hour cannot bounce it back to full volume; reserve is the owner's hold and is never overridden, the default is active so no existing mailbox changes on deploy, and the state never travels in a workspace archive because it is this instance's decision about sending it watched

* feat: stop a query error re-admitting rested mailboxes, make probation measure healthy time, and rotate the candidate window so no mailbox starves: sendLifecycles returned a nil map on failure and an unresolved state reads as active, so one bad query quietly put every resting and reserved mailbox back into cold rotation, and the gate is now applied only when the states were actually read, with the skip logged rather than silent; ReadyToResume measured total time resting, so a mailbox that sat unhealthy for three days resumed on its first healthy tick having served no clean time, and an unhealthy evaluation now restarts the streak; and ordering candidates by send_lifecycle_since put every never-moved mailbox equal-first, so on an install with more than one page of them the same page was re-examined forever, which a checked-at stamp and its index fix
2026-08-28 12:08:52 -07:00

329 lines
11 KiB
Go

package models
import (
"time"
"github.com/google/uuid"
)
type DateRange struct {
From time.Time `json:"from"`
To time.Time `json:"to"`
}
// Warmup Analytics
type WarmupAnalytics struct {
EmailAccountID uuid.UUID `json:"email_account_id"`
Email string `json:"email"`
DateRange DateRange `json:"date_range"`
Summary WarmupSummary `json:"summary"`
DailyStats []WarmupDailyStats `json:"daily_stats"`
}
type WarmupSummary struct {
TotalSent int `json:"total_sent"`
TotalReplied int `json:"total_replied"`
AverageDaily float64 `json:"average_daily"`
ReplyRate float64 `json:"reply_rate"` // percentage
TargetProgress float64 `json:"target_progress"` // percentage to max
DaysActive int `json:"days_active"`
}
type WarmupDailyStats struct {
Date string `json:"date"` // YYYY-MM-DD
EmailsSent int `json:"emails_sent"`
EmailsReplied int `json:"emails_replied"`
TargetVolume int `json:"target_volume"`
}
// Campaign Analytics
type CampaignAnalytics struct {
CampaignID uuid.UUID `json:"campaign_id"`
Name string `json:"name"`
Status string `json:"status"`
DateRange DateRange `json:"date_range"`
Summary CampaignSummary `json:"summary"`
Sequences []SequenceStats `json:"steps"`
DailyStats []CampaignDailyStats `json:"daily_stats,omitempty"`
}
type CampaignSummary struct {
TotalContacts int `json:"total_contacts"`
EmailsSent int `json:"emails_sent"`
EmailsPending int `json:"emails_pending"`
UniqueOpens int `json:"unique_opens"`
// MachineOpens is the subset of UniqueOpens from automated fetchers
// (Apple MPP prefetch, UA-less clients). Human opens = unique - machine.
MachineOpens int `json:"machine_opens"`
UniqueClicks int `json:"unique_clicks"`
Replies int `json:"replies"`
Bounces int `json:"bounces"`
Unsubscribes int `json:"unsubscribes"`
OpenRate float64 `json:"open_rate"` // percentage
ClickRate float64 `json:"click_rate"` // percentage
ReplyRate float64 `json:"reply_rate"` // percentage
BounceRate float64 `json:"bounce_rate"` // percentage
}
type SequenceStats struct {
SequenceID uuid.UUID `json:"step_id"`
Name string `json:"name"`
Position int `json:"position"`
EmailsSent int `json:"emails_sent"`
Opens int `json:"opens"`
Clicks int `json:"clicks"`
Replies int `json:"replies"`
Bounces int `json:"bounces"`
}
type CampaignDailyStats struct {
Date string `json:"date"`
Sent int `json:"sent"`
Opens int `json:"opens"`
Clicks int `json:"clicks"`
Replies int `json:"replies"`
}
// Email Account Status
type EmailAccountStatus struct {
ID uuid.UUID `json:"id"`
Email string `json:"email"`
Provider string `json:"provider"`
Status string `json:"status"`
LastSyncedAt *time.Time `json:"last_synced_at"`
Health AccountHealth `json:"health"`
Errors []AccountError `json:"errors"`
DailyUsage AccountDailyUsage `json:"daily_usage"`
WarmupStatus *WarmupStatusInfo `json:"warmup_status,omitempty"`
// WarmupHealth is the mailbox's warmup-pool reputation (spam placement,
// complaints, throttle/quarantine state). Folded into Health.Score and
// also exposed in detail here. Nil when the mailbox is not in a pool.
WarmupHealth *WarmupHealthInfo `json:"warmup_health,omitempty"`
// InCampaign reports whether the mailbox currently backs a live campaign.
// When true a low-volume health-check warmup keeps running even if the
// user has warmup paused/off.
InCampaign bool `json:"in_campaign"`
// SendLifecycle is whether the mailbox is in cold rotation, present only
// when it is NOT: an active mailbox needs no explanation.
SendLifecycle *SendLifecycleState `json:"send_lifecycle,omitempty"`
// ColdRamp is the warmup-to-cold graduation ceiling, present only while it
// is below the mailbox's own cap. Without it the cap just reads lower than
// the number the owner configured.
ColdRamp *ColdRampInfo `json:"cold_ramp,omitempty"`
}
// ColdRampInfo explains a cold cap held below the mailbox's configured limit.
type ColdRampInfo struct {
// Ceiling is today's allowance; MailboxCap is what the owner configured.
Ceiling int `json:"ceiling"`
MailboxCap int `json:"mailbox_cap"`
// DaysToFullCap is how many clean days remain before Ceiling reaches
// MailboxCap, 0 when it arrives today.
DaysToFullCap int `json:"days_to_full_cap"`
// Held is set when a recent spam placement is pausing the climb.
Held bool `json:"held"`
}
type WarmupHealthInfo struct {
State string `json:"state"` // healthy/watch/throttled/quarantined/blocked
Score float64 `json:"score"`
Reason string `json:"reason,omitempty"`
SpamScore int `json:"spam_score"`
BlockedUntil *time.Time `json:"blocked_until,omitempty"`
EvaluatedAt *time.Time `json:"evaluated_at,omitempty"`
}
type AccountHealth struct {
Status string `json:"status"` // healthy, warning, error
Score int `json:"score"` // 0-100
Issues []string `json:"issues,omitempty"`
}
type AccountError struct {
ID uuid.UUID `json:"id"`
ErrorCode string `json:"error_code"`
Severity string `json:"severity"`
Title string `json:"title"`
Message string `json:"message"`
ActionRequired *string `json:"action_required,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
type AccountDailyUsage struct {
Date string `json:"date"`
CampaignSent int `json:"campaign_sent"`
CampaignLimit int `json:"campaign_limit"`
WarmupSent int `json:"warmup_sent,omitempty"`
WarmupLimit int `json:"warmup_limit,omitempty"`
}
type WarmupStatusInfo struct {
Enabled bool `json:"enabled"`
Paused bool `json:"paused"`
PausedAt *time.Time `json:"paused_at,omitempty"`
StartedAt time.Time `json:"started_at"`
CurrentVolume int `json:"current_volume"`
TargetVolume int `json:"target_volume"`
MaxVolume int `json:"max_volume"`
ReplyRate int `json:"reply_rate"`
DaysActive int `json:"days_active"`
// RampHold explains a ramp that is not climbing, so a target below the
// plain ramp is never an unexplained drop.
RampHold *WarmupRampHold `json:"ramp_hold,omitempty"`
}
// WarmupRampHold explains a ramp that is not climbing. Present for the whole
// freeze; VolumeCut says whether today's volume is also reduced, which lasts a
// shorter window.
type WarmupRampHold struct {
// Placements and Sends cover the last 48 hours.
Placements int `json:"placements"`
Sends int `json:"sends"`
VolumeCut bool `json:"volume_cut"`
// ResumesAt is when the ramp climbs again if nothing else lands.
ResumesAt time.Time `json:"resumes_at"`
}
// Usage Overview
type UsageOverview struct {
UserID uuid.UUID `json:"user_id"`
Period string `json:"period"` // day, week, month
EmailAccounts AccountsUsage `json:"email_accounts"`
Campaigns CampaignsUsage `json:"campaigns"`
Contacts ContactsUsage `json:"contacts"`
API APIUsage `json:"api"`
}
type AccountsUsage struct {
Total int `json:"total"`
Active int `json:"active"`
InWarmup int `json:"in_warmup"`
WithErrors int `json:"with_errors"`
}
type CampaignsUsage struct {
Total int `json:"total"`
Active int `json:"active"`
Paused int `json:"paused"`
Draft int `json:"draft"`
EmailsSent int `json:"emails_sent"`
}
type ContactsUsage struct {
Total int `json:"total"`
Subscribed int `json:"subscribed"`
AddedToday int `json:"added_today"`
}
type APIUsage struct {
TotalCalls int `json:"total_calls"`
DailyLimit int `json:"daily_limit"`
TopEndpoints []EndpointUsage `json:"top_endpoints"`
}
type EndpointUsage struct {
Endpoint string `json:"endpoint"`
Calls int `json:"calls"`
}
// Dashboard Analytics
// DashboardAnalytics is the main dashboard overview combining multiple stats
type DashboardAnalytics struct {
Period string `json:"period"` // 7d, 30d, 90d
OverallStats DashboardOverallStats `json:"overall_stats"`
RecentActivity []RecentActivityItem `json:"recent_activity"`
TopCampaigns []TopCampaignStats `json:"top_campaigns"`
AccountHealth AccountHealthSummary `json:"account_health"`
DailyTrend []DashboardDailyStats `json:"daily_trend"`
}
// DashboardOverallStats contains aggregate statistics for the dashboard
type DashboardOverallStats struct {
TotalEmailsSent int `json:"total_emails_sent"`
TotalOpens int `json:"total_opens"`
// MachineOpens is the subset of TotalOpens from automated fetchers.
MachineOpens int `json:"machine_opens"`
TotalClicks int `json:"total_clicks"`
TotalReplies int `json:"total_replies"`
TotalBounces int `json:"total_bounces"`
OpenRate float64 `json:"open_rate"`
ClickRate float64 `json:"click_rate"`
ReplyRate float64 `json:"reply_rate"`
BounceRate float64 `json:"bounce_rate"`
ActiveCampaigns int `json:"active_campaigns"`
ActiveAccounts int `json:"active_accounts"`
}
// RecentActivityItem represents a single activity event
type RecentActivityItem struct {
Type string `json:"type"` // opened, clicked, replied, bounced, sent
CampaignID uuid.UUID `json:"campaign_id"`
CampaignName string `json:"campaign_name"`
ContactEmail string `json:"contact_email"`
ContactID uuid.UUID `json:"contact_id,omitempty"`
Timestamp time.Time `json:"timestamp"`
Link string `json:"link,omitempty"` // For click events
}
// TopCampaignStats represents performance stats for a top campaign
type TopCampaignStats struct {
CampaignID uuid.UUID `json:"campaign_id"`
Name string `json:"name"`
Status string `json:"status"`
EmailsSent int `json:"emails_sent"`
OpenRate float64 `json:"open_rate"`
ClickRate float64 `json:"click_rate"`
ReplyRate float64 `json:"reply_rate"`
}
// AccountHealthSummary provides a summary of all email account health
type AccountHealthSummary struct {
TotalAccounts int `json:"total_accounts"`
HealthyAccounts int `json:"healthy_accounts"`
WarningAccounts int `json:"warning_accounts"`
ErrorAccounts int `json:"error_accounts"`
}
// DashboardDailyStats represents daily statistics for trend charts
type DashboardDailyStats struct {
Date string `json:"date"` // YYYY-MM-DD
Sent int `json:"sent"`
Opens int `json:"opens"`
Clicks int `json:"clicks"`
Replies int `json:"replies"`
}
// CampaignHourlyStats represents hourly statistics for a campaign
type CampaignHourlyStats struct {
Hour int `json:"hour"` // 0-23
Sent int `json:"sent"`
Opens int `json:"opens"`
Clicks int `json:"clicks"`
Replies int `json:"replies"`
}
// CampaignComparison allows comparing multiple campaigns
type CampaignComparison struct {
Campaigns []CampaignComparisonItem `json:"campaigns"`
Period DateRange `json:"period"`
}
// CampaignComparisonItem represents a single campaign in a comparison
type CampaignComparisonItem struct {
CampaignID uuid.UUID `json:"campaign_id"`
Name string `json:"name"`
Status string `json:"status"`
EmailsSent int `json:"emails_sent"`
OpenRate float64 `json:"open_rate"`
ClickRate float64 `json:"click_rate"`
ReplyRate float64 `json:"reply_rate"`
BounceRate float64 `json:"bounce_rate"`
}