package dashboard import ( "dove/config" "fmt" "math" "math/rand" "strings" domainRepo "dove/repositories/domain" mailRepo "dove/repositories/mail" dnsSystem "dove/utils/dns" ) type ServiceStatus struct { Name string `json:"name"` Port string `json:"port"` Protocol string `json:"protocol"` Active bool `json:"active"` } type SparklineData struct { LinePath string `json:"line_path"` FillPath string `json:"fill_path"` } type EmailCard struct { Sparkline24h SparklineData `json:"sparkline_24h"` Sparkline7d SparklineData `json:"sparkline_7d"` Sparkline30d SparklineData `json:"sparkline_30d"` TotalMailboxes int64 `json:"total_mailboxes"` TotalDelivered int `json:"total_delivered"` TotalBounced int `json:"total_bounced"` BounceRate string `json:"bounce_rate"` TotalMessages int64 `json:"total_messages"` } type DomainStatusEntry struct { Name string `json:"name"` StatusOk int `json:"status_ok"` StatusErr int `json:"status_err"` StatusWarn int `json:"status_warn"` Total int `json:"total"` PercentOk string `json:"percent_ok"` PercentWarn string `json:"percent_warn"` PercentErr string `json:"percent_err"` } type DomainsCard struct { Domains []DomainStatusEntry `json:"domains"` Selected string `json:"selected"` } type ProxyServiceEntry struct { Name string `json:"name"` Target string `json:"target"` Up bool `json:"up"` } type ReverseProxyCard struct { Services []ProxyServiceEntry `json:"services"` } type StorageBucketEntry struct { Name string `json:"name"` UsageBytes int64 `json:"usage_bytes"` UsageLabel string `json:"usage_label"` Percentage float64 `json:"percentage"` } type ObjectStorageCard struct { Buckets []StorageBucketEntry `json:"buckets"` TotalUsage string `json:"total_usage"` } type CronFailureEntry struct { JobName string `json:"job_name"` Domain string `json:"domain"` FailedAt string `json:"failed_at"` ErrorText string `json:"error_text"` } type CronJobsCard struct { Failures []CronFailureEntry `json:"failures"` TotalJobs int `json:"total_jobs"` TotalRuns int `json:"total_runs"` FailCount int `json:"fail_count"` } type DoveStatusEntry struct { Route string `json:"route"` Method string `json:"method"` StatusCode int `json:"status_code"` Count int `json:"count"` } type DoveCard struct { StatusGroups map[string]int `json:"status_groups"` RecentRoutes []DoveStatusEntry `json:"recent_routes"` TotalRoutes int `json:"total_routes"` } type DnsCard struct { Configured bool `json:"configured"` Address string `json:"address"` Platform string `json:"platform"` } type OverviewResponse struct { BindAddress string `json:"bind_address"` DnsAddress string `json:"dns_address"` Services []ServiceStatus `json:"services"` Email EmailCard `json:"email"` Domains DomainsCard `json:"domains"` ReverseProxy ReverseProxyCard `json:"reverse_proxy"` Storage ObjectStorageCard `json:"storage"` CronJobs CronJobsCard `json:"cron_jobs"` Dove DoveCard `json:"dove"` Dns DnsCard `json:"dns"` } func Overview() OverviewResponse { return OverviewResponse{ BindAddress: config.BindAddress, DnsAddress: fmt.Sprintf("%s:%d", config.BindAddress, config.DnsPort), Services: buildServiceStatuses(), Email: buildEmailCard(), Domains: buildDomainsCard(), ReverseProxy: buildReverseProxyCard(), Storage: buildObjectStorageCard(), CronJobs: buildCronJobsCard(), Dove: buildDoveCard(), Dns: buildDnsCard(), } } func buildServiceStatuses() []ServiceStatus { return []ServiceStatus{ {Name: "HTTP", Port: fmt.Sprintf("%d", config.HttpPort), Protocol: "Plain", Active: true}, {Name: "HTTPS", Port: "443", Protocol: "TLS", Active: false}, {Name: "DNS", Port: fmt.Sprintf("%d", config.DnsPort), Protocol: "UDP/TCP", Active: true}, {Name: "SMTP", Port: fmt.Sprintf("%d", config.SmtpPort), Protocol: "Plain/STARTTLS", Active: true}, {Name: "SMTPS", Port: fmt.Sprintf("%d", config.SmtpsPort), Protocol: "TLS", Active: true}, {Name: "SMTP (MSA)", Port: fmt.Sprintf("%d", config.SubmissionPort), Protocol: "Plain/STARTTLS", Active: true}, {Name: "IMAP", Port: fmt.Sprintf("%d", config.ImapPort), Protocol: "Plain/STARTTLS", Active: false}, {Name: "IMAPS", Port: fmt.Sprintf("%d", config.ImapsPort), Protocol: "TLS", Active: false}, {Name: "POP3", Port: fmt.Sprintf("%d", config.Pop3Port), Protocol: "Plain/STARTTLS", Active: false}, {Name: "POP3S", Port: fmt.Sprintf("%d", config.Pop3sPort), Protocol: "TLS", Active: false}, {Name: "S3", Port: fmt.Sprintf("%d", config.S3Port), Protocol: "Plain", Active: false}, } } const SparklineResolution = 100 func buildEmailCard() EmailCard { mailboxCount := mailRepo.CountMailboxes() emailCount := mailRepo.CountEmails() delivered := 150 + rand.Intn(500) bounced := rand.Intn(20) total := delivered + bounced bounceRate := 0.0 if total > 0 { bounceRate = float64(bounced) / float64(total) * 100 } return EmailCard{ Sparkline24h: generateSparkline(24, 3, 40), Sparkline7d: generateSparkline(28, 10, 80), Sparkline30d: generateSparkline(30, 30, 200), TotalMailboxes: mailboxCount, TotalDelivered: delivered, TotalBounced: bounced, BounceRate: fmt.Sprintf("%.1f", math.Round(bounceRate*10)/10), TotalMessages: emailCount, } } func buildDomainsCard() DomainsCard { allDomains := domainRepo.AllDomains() entries := make([]DomainStatusEntry, 0, len(allDomains)) for _, domain := range allDomains { tld := "" if domain.TLD.Name != "" { tld = "." + domain.TLD.Name } fullName := domain.Name + tld statusOk := 200 + rand.Intn(800) statusWarn := rand.Intn(50) statusErr := rand.Intn(30) total := statusOk + statusWarn + statusErr percentOk := 0.0 percentWarn := 0.0 percentErr := 0.0 if total > 0 { percentOk = math.Round(float64(statusOk)/float64(total)*1000) / 10 percentWarn = math.Round(float64(statusWarn)/float64(total)*1000) / 10 percentErr = math.Round(float64(statusErr)/float64(total)*1000) / 10 } entries = append(entries, DomainStatusEntry{ Name: fullName, StatusOk: statusOk, StatusErr: statusErr, StatusWarn: statusWarn, Total: total, PercentOk: fmt.Sprintf("%.1f", percentOk), PercentWarn: fmt.Sprintf("%.1f", percentWarn), PercentErr: fmt.Sprintf("%.1f", percentErr), }) } selected := "" if len(entries) > 0 { selected = entries[0].Name } return DomainsCard{ Domains: entries, Selected: selected, } } func buildReverseProxyCard() ReverseProxyCard { return ReverseProxyCard{ Services: []ProxyServiceEntry{ {Name: "frontend", Target: "127.0.0.1:3001", Up: true}, {Name: "api", Target: "127.0.0.1:8080", Up: true}, {Name: "worker", Target: "127.0.0.1:9090", Up: false}, {Name: "docs", Target: "127.0.0.1:4000", Up: true}, {Name: "websocket", Target: "127.0.0.1:8443", Up: false}, }, } } func buildObjectStorageCard() ObjectStorageCard { buckets := []StorageBucketEntry{ generateBucket("media-uploads", 1, 4), generateBucket("backups", 500, 2000), generateBucket("thumbnails", 50, 500), generateBucket("exports", 10, 200), } var totalBytes int64 var maxBytes int64 for _, bucket := range buckets { totalBytes += bucket.UsageBytes if bucket.UsageBytes > maxBytes { maxBytes = bucket.UsageBytes } } for index := range buckets { buckets[index].Percentage = float64(buckets[index].UsageBytes) / float64(maxBytes) * 100 } return ObjectStorageCard{ Buckets: buckets, TotalUsage: formatBytes(totalBytes), } } func buildCronJobsCard() CronJobsCard { totalJobs := 5 + rand.Intn(8) totalRuns := 20 + rand.Intn(80) failures := []CronFailureEntry{ {JobName: "cleanup-temp", Domain: "api.dove", FailedAt: "2m ago", ErrorText: "Connection refused on :8080"}, {JobName: "sync-storage", Domain: "cdn.dove", FailedAt: "18m ago", ErrorText: "Bucket not found: staging"}, {JobName: "health-ping", Domain: "monitor.dove", FailedAt: "1h ago", ErrorText: "Timeout after 30s"}, } return CronJobsCard{ Failures: failures, TotalJobs: totalJobs, TotalRuns: totalRuns, FailCount: len(failures), } } func buildDoveCard() DoveCard { statusGroups := map[string]int{ "2xx": 800 + rand.Intn(2000), "3xx": 50 + rand.Intn(200), "4xx": 10 + rand.Intn(50), "5xx": rand.Intn(10), } recentRoutes := []DoveStatusEntry{ {Route: "/dashboard", Method: "GET", StatusCode: 200, Count: 120 + rand.Intn(200)}, {Route: "/domains", Method: "GET", StatusCode: 200, Count: 80 + rand.Intn(150)}, {Route: "/mail/webmail", Method: "GET", StatusCode: 200, Count: 60 + rand.Intn(100)}, {Route: "/domains/records", Method: "POST", StatusCode: 201, Count: 15 + rand.Intn(30)}, {Route: "/auth/login", Method: "POST", StatusCode: 302, Count: 10 + rand.Intn(20)}, {Route: "/mail/compose", Method: "POST", StatusCode: 500, Count: rand.Intn(5)}, {Route: "/domains/manage", Method: "DELETE", StatusCode: 404, Count: rand.Intn(8)}, } return DoveCard{ StatusGroups: statusGroups, RecentRoutes: recentRoutes, TotalRoutes: len(recentRoutes), } } func generateBucket(name string, minMegabytes int, maxMegabytes int) StorageBucketEntry { megabytes := int64(minMegabytes) + int64(rand.Intn(maxMegabytes-minMegabytes)) bytes := megabytes * 1024 * 1024 return StorageBucketEntry{ Name: name, UsageBytes: bytes, UsageLabel: formatBytes(bytes), } } func generateSparkline(anchorCount int, minValue float64, maxValue float64) SparklineData { viewboxWidth := 200.0 viewboxHeight := 40.0 padding := 2.0 anchorValues := make([]float64, anchorCount) currentValue := minValue + (maxValue-minValue)*0.3 + rand.Float64()*(maxValue-minValue)*0.4 for index := range anchorValues { delta := (rand.Float64() - 0.45) * (maxValue - minValue) * 0.25 currentValue += delta currentValue = math.Max(minValue, math.Min(maxValue, currentValue)) anchorValues[index] = currentValue } smoothValues := catmullRomResample(anchorValues, SparklineResolution) minPoint := smoothValues[0] maxPoint := smoothValues[0] for _, point := range smoothValues { if point < minPoint { minPoint = point } if point > maxPoint { maxPoint = point } } pointRange := maxPoint - minPoint if pointRange == 0 { pointRange = 1 } lineCoordinates := make([]string, SparklineResolution) for index, point := range smoothValues { horizontalPosition := padding + (float64(index)/float64(SparklineResolution-1))*(viewboxWidth-2*padding) verticalPosition := padding + (1-(point-minPoint)/pointRange)*(viewboxHeight-2*padding) lineCoordinates[index] = fmt.Sprintf("%.1f,%.1f", horizontalPosition, verticalPosition) } linePath := strings.Join(lineCoordinates, " ") fillPath := fmt.Sprintf("M%.1f,%.1f L%s L%.1f,%.1f Z", padding, viewboxHeight, linePath, viewboxWidth-padding, viewboxHeight, ) return SparklineData{ LinePath: linePath, FillPath: fillPath, } } func catmullRomResample(inputValues []float64, outputCount int) []float64 { inputCount := len(inputValues) if inputCount < 2 { return inputValues } resampledValues := make([]float64, outputCount) for outputIndex := 0; outputIndex < outputCount; outputIndex++ { normalizedPosition := float64(outputIndex) / float64(outputCount-1) * float64(inputCount-1) segmentIndex := int(normalizedPosition) segmentFraction := normalizedPosition - float64(segmentIndex) point0 := inputValues[max(0, segmentIndex-1)] point1 := inputValues[min(inputCount-1, segmentIndex)] point2 := inputValues[min(inputCount-1, segmentIndex+1)] point3 := inputValues[min(inputCount-1, segmentIndex+2)] fractionSquared := segmentFraction * segmentFraction fractionCubed := fractionSquared * segmentFraction resampledValues[outputIndex] = 0.5 * ((2 * point1) + (-point0+point2)*segmentFraction + (2*point0-5*point1+4*point2-point3)*fractionSquared + (-point0+3*point1-3*point2+point3)*fractionCubed) } return resampledValues } func buildDnsCard() DnsCard { systemDnsStatus := dnsSystem.CheckSystemDns() return DnsCard{ Configured: systemDnsStatus.Configured, Address: systemDnsStatus.Address, Platform: systemDnsStatus.Platform, } } func formatBytes(bytes int64) string { if bytes < 1024 { return fmt.Sprintf("%d B", bytes) } if bytes < 1024*1024 { return fmt.Sprintf("%.1f KB", float64(bytes)/1024) } if bytes < 1024*1024*1024 { return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) } return fmt.Sprintf("%.1f GB", float64(bytes)/(1024*1024*1024)) }