package handlers import ( "embed" "fmt" "html/template" "math" "net/http" "strings" "time" "sneak.berlin/go/dnswatcher/internal/notify" "sneak.berlin/go/dnswatcher/internal/state" ) //go:embed templates/dashboard.html var dashboardFS embed.FS // Time unit constants for relative time calculations. const ( secondsPerMinute = 60 minutesPerHour = 60 hoursPerDay = 24 ) // newDashboardTemplate parses the embedded dashboard HTML // template with helper functions. func newDashboardTemplate() *template.Template { funcs := template.FuncMap{ "relTime": relTime, "joinStrings": joinStrings, "formatRecords": formatRecords, "expiryDays": expiryDays, } return template.Must( template.New("dashboard.html"). Funcs(funcs). ParseFS(dashboardFS, "templates/dashboard.html"), ) } // dashboardData is the data passed to the dashboard template. type dashboardData struct { Snapshot state.Snapshot Alerts []notify.AlertEntry StateAge string GeneratedAt string } // HandleDashboard returns the dashboard page handler. func (h *Handlers) HandleDashboard() http.HandlerFunc { tmpl := newDashboardTemplate() return func( writer http.ResponseWriter, _ *http.Request, ) { snap := h.state.GetSnapshot() alerts := h.notifyHistory.Recent() data := dashboardData{ Snapshot: snap, Alerts: alerts, StateAge: relTime(snap.LastUpdated), GeneratedAt: time.Now().UTC().Format("2006-01-02 15:04:05"), } writer.Header().Set( "Content-Type", "text/html; charset=utf-8", ) err := tmpl.Execute(writer, data) if err != nil { h.log.Error( "dashboard template error", "error", err, ) } } } // relTime returns a human-readable relative time string such // as "2 minutes ago" or "never" for zero times. func relTime(t time.Time) string { if t.IsZero() { return "never" } d := time.Since(t) if d < 0 { return "just now" } seconds := int(math.Round(d.Seconds())) if seconds < secondsPerMinute { return fmt.Sprintf("%ds ago", seconds) } minutes := seconds / secondsPerMinute if minutes < minutesPerHour { return fmt.Sprintf("%dm ago", minutes) } hours := minutes / minutesPerHour if hours < hoursPerDay { return fmt.Sprintf( "%dh %dm ago", hours, minutes%minutesPerHour, ) } days := hours / hoursPerDay return fmt.Sprintf( "%dd %dh ago", days, hours%hoursPerDay, ) } // joinStrings joins a string slice with a separator. func joinStrings(items []string, sep string) string { return strings.Join(items, sep) } // formatRecords formats a map of record type → values into a // compact display string. func formatRecords(records map[string][]string) string { if len(records) == 0 { return "-" } var parts []string for rtype, values := range records { for _, v := range values { parts = append(parts, rtype+": "+v) } } return strings.Join(parts, ", ") } // expiryDays returns the number of days until the given time, // rounded down. Returns 0 if already expired. func expiryDays(t time.Time) int { d := time.Until(t).Hours() / hoursPerDay if d < 0 { return 0 } return int(d) }