package server import ( "bytes" "context" "embed" "encoding/json" "fmt" "html/template" "io/fs" "log/slog" "net/http" "sort" "strconv" "strings" "time" "github.com/Fuwn/kaze/internal/config" "github.com/Fuwn/kaze/internal/monitor" "github.com/Fuwn/kaze/internal/storage" "github.com/Fuwn/kaze/internal/theme" ) //go:embed templates/*.html var templatesFS embed.FS //go:embed static/* var staticFS embed.FS // Server handles HTTP requests for the status page type Server struct { config *config.Config storage *storage.Storage scheduler *monitor.Scheduler logger *slog.Logger server *http.Server templates *template.Template } // New creates a new HTTP server func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, logger *slog.Logger) (*Server, error) { // Parse templates tmpl, err := template.New("").Funcs(templateFuncs()).ParseFS(templatesFS, "templates/*.html") if err != nil { return nil, fmt.Errorf("failed to parse templates: %w", err) } s := &Server{ config: cfg, storage: store, scheduler: sched, logger: logger, templates: tmpl, } // Setup routes mux := http.NewServeMux() // Static files staticContent, err := fs.Sub(staticFS, "static") if err != nil { return nil, fmt.Errorf("failed to get static fs: %w", err) } mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticContent)))) // Pages mux.HandleFunc("GET /", s.handleIndex) mux.HandleFunc("GET /api/status", s.handleAPIStatus) mux.HandleFunc("GET /api/monitor/{name}", s.handleAPIMonitor) mux.HandleFunc("GET /api/history/{name}", s.handleAPIHistory) // Create HTTP server s.server = &http.Server{ Addr: fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port), Handler: s.withMiddleware(mux), ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, // Increased for large page renders IdleTimeout: 60 * time.Second, } return s, nil } // Start begins serving HTTP requests func (s *Server) Start() error { s.logger.Info("starting HTTP server", "addr", s.server.Addr) if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { return fmt.Errorf("server error: %w", err) } return nil } // Stop gracefully shuts down the server func (s *Server) Stop(ctx context.Context) error { s.logger.Info("stopping HTTP server") return s.server.Shutdown(ctx) } // withMiddleware adds common middleware to the handler func (s *Server) withMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() // Add security headers w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") next.ServeHTTP(w, r) s.logger.Debug("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start)) }) } // PageData contains data for rendering the status page type PageData struct { Site config.SiteConfig Groups []GroupData Incidents []IncidentData OverallStatus string StatusCounts StatusCounts // Counts for tab title LastUpdated time.Time CurrentTime string // Formatted date/time for display (without timezone) TimezoneTooltip string // JSON data for timezone tooltip LastUpdatedTooltip string // JSON data for last updated tooltip TickMode string // ping, minute, hour, day TickCount int Timezone string // Timezone for display UseBrowserTimezone bool // Use client-side timezone conversion ThemeCSS template.CSS // OpenCode theme CSS (safe CSS) CustomHead template.HTML // Custom HTML for (trusted) Scale float64 // UI scale factor (0.5-2.0) } // StatusCounts holds monitor status counts for display type StatusCounts struct { Up int Down int Degraded int Total int } // GroupData contains data for a monitor group type GroupData struct { Name string Monitors []MonitorData DefaultCollapsed bool ShowGroupUptime bool GroupUptime float64 } // MonitorData contains data for a single monitor type MonitorData struct { Name string Type string Link template.URL // Custom URL for clicking the monitor name (trusted) Status string StatusClass string ResponseTime int64 HidePing bool // Hide response time from display UptimePercent float64 UptimeTooltip string // JSON data for uptime tooltip with last failure info Ticks []*storage.TickData // Aggregated tick data for history bar SSLDaysLeft int SSLExpiryDate time.Time SSLTooltip string // JSON data for SSL expiration tooltip LastCheck time.Time LastError string LastFailure *time.Time // Time of last failure LastFailureError string // Error from last failure DisablePingTooltips bool DisableUptimeTooltip bool } // IncidentData contains data for an incident type IncidentData struct { Title string Status string StatusClass string Message string ScheduledStart *time.Time ScheduledEnd *time.Time CreatedAt *time.Time ResolvedAt *time.Time Updates []IncidentUpdateData IsScheduled bool IsActive bool } // IncidentUpdateData contains data for an incident update type IncidentUpdateData struct { Time time.Time Status string Message string } // handleIndex renders the main status page func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Get all monitor stats stats, err := s.storage.GetAllMonitorStats(ctx) if err != nil { s.logger.Error("failed to get monitor stats", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Load OpenCode theme if configured var themeCSS template.CSS if s.config.Site.ThemeURL != "" { resolvedTheme, err := theme.LoadTheme(s.config.Site.ThemeURL) if err != nil { s.logger.Warn("failed to load theme", "url", s.config.Site.ThemeURL, "error", err) } else if resolvedTheme != nil { // Generate CSS: theme variables + override Kaze's CSS variables cssString := resolvedTheme.GenerateCSS() + resolvedTheme.GenerateVariableOverrides() themeCSS = template.CSS(cssString) } } // Build page data data := PageData{ Site: s.config.Site, TickMode: s.config.Display.TickMode, TickCount: s.config.Display.TickCount, Timezone: s.config.Display.Timezone, UseBrowserTimezone: s.config.Display.Timezone == "Browser", ThemeCSS: themeCSS, CustomHead: template.HTML(s.config.Site.CustomHead), Scale: s.config.Display.Scale, } overallUp := true hasDegraded := false var mostRecentCheck time.Time var statusCounts StatusCounts // Build groups for _, group := range s.config.Groups { gd := GroupData{ Name: group.Name, DefaultCollapsed: group.DefaultCollapsed != nil && *group.DefaultCollapsed, ShowGroupUptime: group.ShowGroupUptime == nil || *group.ShowGroupUptime, } var totalUptime float64 var monitorsWithUptime int for _, monCfg := range group.Monitors { md := MonitorData{ Name: monCfg.Name, Type: monCfg.Type, Link: template.URL(monCfg.Link), HidePing: monCfg.HidePing, DisablePingTooltips: monCfg.DisablePingTooltips, DisableUptimeTooltip: monCfg.DisableUptimeTooltip, } if stat, ok := stats[monCfg.Name]; ok { md.Status = stat.CurrentStatus md.ResponseTime = stat.LastResponseTime md.UptimePercent = stat.UptimePercent md.SSLDaysLeft = stat.SSLDaysLeft md.LastCheck = stat.LastCheck md.LastError = stat.LastError // Set SSL expiry date and tooltip if stat.SSLExpiry != nil { md.SSLExpiryDate = *stat.SSLExpiry md.SSLTooltip = formatSSLTooltip(*stat.SSLExpiry, stat.SSLDaysLeft, s.config.Display.Timezone) } // Set last failure info and uptime tooltip md.LastFailure = stat.LastFailure md.LastFailureError = stat.LastFailureError md.UptimeTooltip = formatUptimeTooltip(stat.UptimePercent, stat.TotalChecks, stat.LastFailure, stat.LastFailureError, s.config.Display.Timezone) // Track most recent check time for footer if stat.LastCheck.After(mostRecentCheck) { mostRecentCheck = stat.LastCheck } // Get aggregated history for display ticks, err := s.storage.GetAggregatedHistory( ctx, monCfg.Name, s.config.Display.TickCount, s.config.Display.TickMode, s.config.Display.PingFixedSlots, ) if err != nil { s.logger.Error("failed to get tick history", "monitor", monCfg.Name, "error", err) } else { md.Ticks = ticks } // Update overall status and counts statusCounts.Total++ switch stat.CurrentStatus { case "down": overallUp = false statusCounts.Down++ case "degraded": hasDegraded = true statusCounts.Degraded++ case "up": statusCounts.Up++ } } else { md.Status = "unknown" } md.StatusClass = statusToClass(md.Status) gd.Monitors = append(gd.Monitors, md) // Accumulate uptime for group average if md.UptimePercent >= 0 { totalUptime += md.UptimePercent monitorsWithUptime++ } } // Calculate group average uptime if monitorsWithUptime > 0 { gd.GroupUptime = totalUptime / float64(monitorsWithUptime) } data.Groups = append(data.Groups, gd) } // Set last updated time from most recent check now := time.Now() if !mostRecentCheck.IsZero() { data.LastUpdated = mostRecentCheck } else { data.LastUpdated = now } // Format current time for display data.CurrentTime = formatCurrentTime(now, s.config.Display.Timezone) data.TimezoneTooltip = formatTimezoneTooltip(now, s.config.Display.Timezone) data.LastUpdatedTooltip = formatLastUpdatedTooltip(data.LastUpdated, s.config.Display.Timezone) // Determine overall status if !overallUp { data.OverallStatus = "Major Outage" } else if hasDegraded { data.OverallStatus = "Partial Outage" } else { data.OverallStatus = "All Systems Operational" } data.StatusCounts = statusCounts // Build incidents for _, inc := range s.config.Incidents { id := IncidentData{ Title: inc.Title, Status: inc.Status, StatusClass: incidentStatusToClass(inc.Status), Message: inc.Message, ScheduledStart: inc.ScheduledStart, ScheduledEnd: inc.ScheduledEnd, CreatedAt: inc.CreatedAt, ResolvedAt: inc.ResolvedAt, IsScheduled: inc.Status == "scheduled", } // Check if incident is active (not resolved and not future scheduled) if inc.Status != "resolved" { if inc.Status == "scheduled" { if inc.ScheduledStart != nil && inc.ScheduledStart.After(time.Now()) { id.IsActive = false } else { id.IsActive = true } } else { id.IsActive = true } } // Add updates for _, upd := range inc.Updates { id.Updates = append(id.Updates, IncidentUpdateData{ Time: upd.Time, Status: upd.Status, Message: upd.Message, }) } data.Incidents = append(data.Incidents, id) } // Sort incidents: active first, then by date sort.Slice(data.Incidents, func(i, j int) bool { if data.Incidents[i].IsActive != data.Incidents[j].IsActive { return data.Incidents[i].IsActive } return false }) // Render template to buffer first to prevent broken pipe errors // This allows us to complete template execution even if client disconnects var buf bytes.Buffer if err := s.templates.ExecuteTemplate(&buf, "index.html", data); err != nil { s.logger.Error("failed to render template", "error", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Write buffered response atomically w.Header().Set("Content-Type", "text/html; charset=utf-8") if _, err := w.Write(buf.Bytes()); err != nil { // Client likely disconnected, just log it s.logger.Debug("failed to write response", "error", err) } } // handleAPIStatus returns JSON status for all monitors func (s *Server) handleAPIStatus(w http.ResponseWriter, r *http.Request) { ctx := r.Context() stats, err := s.storage.GetAllMonitorStats(ctx) if err != nil { s.jsonError(w, "Failed to get stats", http.StatusInternalServerError) return } s.jsonResponse(w, stats) } // handleAPIMonitor returns JSON status for a specific monitor func (s *Server) handleAPIMonitor(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") if name == "" { s.jsonError(w, "Monitor name required", http.StatusBadRequest) return } stats, err := s.storage.GetMonitorStats(r.Context(), name) if err != nil { s.jsonError(w, "Failed to get monitor stats", http.StatusInternalServerError) return } s.jsonResponse(w, stats) } // handleAPIHistory returns aggregated history for a monitor func (s *Server) handleAPIHistory(w http.ResponseWriter, r *http.Request) { name := r.PathValue("name") if name == "" { s.jsonError(w, "Monitor name required", http.StatusBadRequest) return } // Allow optional parameters, default to config values mode := s.config.Display.TickMode if modeParam := r.URL.Query().Get("mode"); modeParam != "" { switch modeParam { case "ping", "minute", "hour", "day": mode = modeParam } } count := s.config.Display.TickCount if countParam := r.URL.Query().Get("count"); countParam != "" { if c, err := strconv.Atoi(countParam); err == nil && c > 0 && c <= 200 { count = c } } ticks, err := s.storage.GetAggregatedHistory(r.Context(), name, count, mode, s.config.Display.PingFixedSlots) if err != nil { s.jsonError(w, "Failed to get history", http.StatusInternalServerError) return } s.jsonResponse(w, map[string]interface{}{ "monitor": name, "mode": mode, "count": count, "ticks": ticks, }) } // jsonResponse writes a JSON response func (s *Server) jsonResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(data); err != nil { s.logger.Error("failed to encode JSON response", "error", err) } } // jsonError writes a JSON error response func (s *Server) jsonError(w http.ResponseWriter, message string, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{"error": message}) } // templateFuncs returns custom template functions func templateFuncs() template.FuncMap { return template.FuncMap{ "formatTime": func(t time.Time) string { if t.IsZero() { return "-" } return t.Format("Jan 2, 15:04 UTC") }, "formatDate": func(t time.Time) string { if t.IsZero() { return "-" } return t.Format("Jan 2, 2006") }, "formatDuration": func(ms int64) string { if ms < 1000 { return fmt.Sprintf("%dms", ms) } return fmt.Sprintf("%.2fs", float64(ms)/1000) }, "formatUptime": func(pct float64) string { if pct < 0 { return "-" } return fmt.Sprintf("%.2f%%", pct) }, "timeAgo": func(t time.Time) string { if t.IsZero() { return "never" } d := time.Since(t) if d < time.Minute { return fmt.Sprintf("%d seconds ago", int(d.Seconds())) } if d < time.Hour { return fmt.Sprintf("%d minutes ago", int(d.Minutes())) } if d < 24*time.Hour { return fmt.Sprintf("%d hours ago", int(d.Hours())) } return fmt.Sprintf("%d days ago", int(d.Hours()/24)) }, "tickColor": func(tick *storage.TickData) string { if tick == nil { return "bg-neutral-200 dark:bg-neutral-800" // No data } if tick.UptimePercent >= 99 { return "bg-emerald-500" } if tick.UptimePercent >= 95 { return "bg-yellow-500" } if tick.UptimePercent > 0 { return "bg-red-500" } // For ping mode with status switch tick.Status { case "up": return "bg-emerald-500" case "degraded": return "bg-yellow-500" case "down": return "bg-red-500" } return "bg-neutral-200 dark:bg-neutral-800" }, "simplifyError": simplifyErrorMessage, "tickTooltipData": func(tick *storage.TickData, mode, timezone string, hidePing bool) string { if tick == nil { data := map[string]interface{}{"header": "No data"} b, _ := json.Marshal(data) return string(b) } // If using browser timezone, include raw timestamp for client-side conversion useBrowserTz := timezone == "Browser" // Convert timestamp to configured timezone (fallback for non-JS users) loc := time.Local if timezone != "" && timezone != "Local" && timezone != "Browser" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } t := tick.Timestamp.In(loc) // Get timezone info tzAbbr := t.Format("MST") _, offset := t.Zone() hours := offset / 3600 minutes := (offset % 3600) / 60 var utcOffset string if minutes != 0 { utcOffset = fmt.Sprintf("UTC%+d:%02d", hours, abs(minutes)) } else { utcOffset = fmt.Sprintf("UTC%+d", hours) } var header, statusClass string data := make(map[string]interface{}) rows := []map[string]string{} // Include raw timestamp for browser timezone conversion if useBrowserTz { data["timestamp"] = tick.Timestamp.Format(time.RFC3339) data["mode"] = mode } switch mode { case "ping": header = t.Format("Jan 2, 15:04:05") statusClass = tickStatusClass(tick.Status) rows = append(rows, map[string]string{"label": "Status", "value": tick.Status, "class": statusClass}, ) if !hidePing { rows = append(rows, map[string]string{"label": "Response", "value": fmt.Sprintf("%dms", tick.ResponseTime), "class": ""}, ) } rows = append(rows, map[string]string{"label": "Timezone", "value": fmt.Sprintf("%s (%s)", tzAbbr, utcOffset), "class": ""}, ) case "minute": header = t.Format("Jan 2, 15:04") statusClass = uptimeStatusClass(tick.UptimePercent) rows = append(rows, map[string]string{"label": "Checks", "value": fmt.Sprintf("%d", tick.TotalChecks), "class": ""}, map[string]string{"label": "Uptime", "value": fmt.Sprintf("%.1f%%", tick.UptimePercent), "class": statusClass}, ) if !hidePing { rows = append(rows, map[string]string{"label": "Avg Response", "value": fmt.Sprintf("%dms", int(tick.AvgResponse)), "class": ""}, ) } rows = append(rows, map[string]string{"label": "Timezone", "value": fmt.Sprintf("%s (%s)", tzAbbr, utcOffset), "class": ""}, ) case "hour": header = t.Format("Jan 2, 15:00") statusClass = uptimeStatusClass(tick.UptimePercent) rows = append(rows, map[string]string{"label": "Checks", "value": fmt.Sprintf("%d", tick.TotalChecks), "class": ""}, map[string]string{"label": "Uptime", "value": fmt.Sprintf("%.1f%%", tick.UptimePercent), "class": statusClass}, ) if !hidePing { rows = append(rows, map[string]string{"label": "Avg Response", "value": fmt.Sprintf("%dms", int(tick.AvgResponse)), "class": ""}, ) } rows = append(rows, map[string]string{"label": "Timezone", "value": fmt.Sprintf("%s (%s)", tzAbbr, utcOffset), "class": ""}, ) case "day": header = t.Format("Jan 2, 2006") statusClass = uptimeStatusClass(tick.UptimePercent) rows = append(rows, map[string]string{"label": "Checks", "value": fmt.Sprintf("%d", tick.TotalChecks), "class": ""}, map[string]string{"label": "Uptime", "value": fmt.Sprintf("%.1f%%", tick.UptimePercent), "class": statusClass}, ) if !hidePing { rows = append(rows, map[string]string{"label": "Avg Response", "value": fmt.Sprintf("%dms", int(tick.AvgResponse)), "class": ""}, ) } rows = append(rows, map[string]string{"label": "Timezone", "value": fmt.Sprintf("%s (%s)", tzAbbr, utcOffset), "class": ""}, ) default: header = t.Format("Jan 2, 15:04") } data["header"] = header data["rows"] = rows b, _ := json.Marshal(data) return string(b) }, "seq": func(n int) []int { result := make([]int, n) for i := range result { result[i] = i } return result }, } } // formatCurrentTime formats the current time for display without timezone func formatCurrentTime(t time.Time, timezone string) string { loc := time.Local if timezone != "" && timezone != "Local" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } t = t.In(loc) return t.Format("Jan 2, 2006 15:04") } // formatTimezoneTooltip creates JSON data for timezone tooltip func formatTimezoneTooltip(t time.Time, timezone string) string { loc := time.Local if timezone != "" && timezone != "Local" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } t = t.In(loc) // Get timezone abbreviation (like PST, EST, etc.) tzAbbr := t.Format("MST") // Get UTC offset in format like "UTC-8" or "UTC+5:30" _, offset := t.Zone() hours := offset / 3600 minutes := (offset % 3600) / 60 var utcOffset string if minutes != 0 { utcOffset = fmt.Sprintf("UTC%+d:%02d", hours, abs(minutes)) } else { utcOffset = fmt.Sprintf("UTC%+d", hours) } // Get GMT offset in same format var gmtOffset string if minutes != 0 { gmtOffset = fmt.Sprintf("GMT%+d:%02d", hours, abs(minutes)) } else { gmtOffset = fmt.Sprintf("GMT%+d", hours) } data := map[string]interface{}{ "header": "Timezone", "rows": []map[string]string{ {"label": "Abbreviation", "value": tzAbbr, "class": ""}, {"label": "UTC Offset", "value": utcOffset, "class": ""}, {"label": "GMT Offset", "value": gmtOffset, "class": ""}, }, } b, _ := json.Marshal(data) return string(b) } // abs returns the absolute value of an integer func abs(n int) int { if n < 0 { return -n } return n } // formatSSLTooltip creates JSON data for SSL expiration tooltip func formatSSLTooltip(expiryDate time.Time, daysLeft int, timezone string) string { loc := time.Local if timezone != "" && timezone != "Local" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } t := expiryDate.In(loc) // Get timezone abbreviation tzAbbr := t.Format("MST") // Get UTC offset _, offset := t.Zone() hours := offset / 3600 minutes := (offset % 3600) / 60 var utcOffset string if minutes != 0 { utcOffset = fmt.Sprintf("UTC%+d:%02d", hours, abs(minutes)) } else { utcOffset = fmt.Sprintf("UTC%+d", hours) } // Get GMT offset var gmtOffset string if minutes != 0 { gmtOffset = fmt.Sprintf("GMT%+d:%02d", hours, abs(minutes)) } else { gmtOffset = fmt.Sprintf("GMT%+d", hours) } // Format the expiry date expiryStr := t.Format("Jan 2, 2006 15:04:05") // Determine status message var statusMsg, statusClass string if daysLeft < 0 { statusMsg = "Expired" statusClass = "error" } else if daysLeft < 7 { statusMsg = fmt.Sprintf("%d days (Critical)", daysLeft) statusClass = "error" } else if daysLeft < 14 { statusMsg = fmt.Sprintf("%d days (Warning)", daysLeft) statusClass = "warning" } else { statusMsg = fmt.Sprintf("%d days", daysLeft) statusClass = "success" } data := map[string]interface{}{ "header": "SSL Certificate", "rows": []map[string]string{ {"label": "Expires", "value": expiryStr, "class": ""}, {"label": "Days Left", "value": statusMsg, "class": statusClass}, {"label": "Timezone", "value": tzAbbr, "class": ""}, {"label": "UTC Offset", "value": utcOffset, "class": ""}, {"label": "GMT Offset", "value": gmtOffset, "class": ""}, }, } b, _ := json.Marshal(data) return string(b) } // formatLastUpdatedTooltip creates JSON data for last updated tooltip func formatLastUpdatedTooltip(t time.Time, timezone string) string { loc := time.Local if timezone != "" && timezone != "Local" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } t = t.In(loc) // Get timezone abbreviation tzAbbr := t.Format("MST") // Get UTC offset _, offset := t.Zone() hours := offset / 3600 minutes := (offset % 3600) / 60 var utcOffset string if minutes != 0 { utcOffset = fmt.Sprintf("UTC%+d:%02d", hours, abs(minutes)) } else { utcOffset = fmt.Sprintf("UTC%+d", hours) } // Get GMT offset var gmtOffset string if minutes != 0 { gmtOffset = fmt.Sprintf("GMT%+d:%02d", hours, abs(minutes)) } else { gmtOffset = fmt.Sprintf("GMT%+d", hours) } // Format the datetime datetime := t.Format("Jan 2, 2006 15:04:05") data := map[string]interface{}{ "header": "Last Check", "rows": []map[string]string{ {"label": "Date & Time", "value": datetime, "class": ""}, {"label": "Timezone", "value": tzAbbr, "class": ""}, {"label": "UTC Offset", "value": utcOffset, "class": ""}, {"label": "GMT Offset", "value": gmtOffset, "class": ""}, }, } b, _ := json.Marshal(data) return string(b) } // formatUptimeTooltip creates JSON data for uptime percentage tooltip func formatUptimeTooltip(uptimePercent float64, totalChecks int64, lastFailure *time.Time, lastFailureError string, timezone string) string { loc := time.Local if timezone != "" && timezone != "Local" && timezone != "Browser" { if l, err := time.LoadLocation(timezone); err == nil { loc = l } } rows := []map[string]string{ {"label": "Uptime", "value": fmt.Sprintf("%.2f%%", uptimePercent), "class": ""}, {"label": "Total Checks", "value": fmt.Sprintf("%d", totalChecks), "class": ""}, } if lastFailure != nil { t := lastFailure.In(loc) failureTime := t.Format("Jan 2, 2006 15:04:05") rows = append(rows, map[string]string{"label": "Last Failure", "value": failureTime, "class": "error"}) if lastFailureError != "" { // Simplify the error for display simplifiedError := simplifyErrorMessage(lastFailureError) rows = append(rows, map[string]string{"label": "Failure Reason", "value": simplifiedError, "class": ""}) } } else { rows = append(rows, map[string]string{"label": "Last Failure", "value": "Never", "class": "success"}) } data := map[string]interface{}{ "header": "Uptime Statistics", "rows": rows, } b, _ := json.Marshal(data) return string(b) } // simplifyErrorMessage simplifies error messages for display func simplifyErrorMessage(err string) string { if err == "" { return "" } switch { case strings.Contains(err, "no such host"): return "DNS lookup failed" case strings.Contains(err, "connection refused"): return "Connection refused" case strings.Contains(err, "connection reset"): return "Connection reset" case strings.Contains(err, "timeout"): return "Timeout" case strings.Contains(err, "certificate"): return "SSL/TLS error" case strings.Contains(err, "EOF"): return "Connection closed" case strings.Contains(err, "status code"): return "Unexpected status" case strings.Contains(err, "expected content"): return "Content mismatch" case strings.Contains(err, "i/o timeout"): return "I/O timeout" case strings.Contains(err, "network is unreachable"): return "Network unreachable" default: return "Error" } } // statusToClass converts a status to a CSS class func statusToClass(status string) string { switch status { case "up": return "status-up" case "down": return "status-down" case "degraded": return "status-degraded" default: return "status-unknown" } } // tickStatusClass returns CSS class for tooltip status text func tickStatusClass(status string) string { switch status { case "up": return "success" case "degraded": return "warning" case "down": return "error" default: return "" } } // uptimeStatusClass returns CSS class based on uptime percentage func uptimeStatusClass(pct float64) string { if pct >= 99 { return "success" } if pct >= 95 { return "warning" } return "error" } // incidentStatusToClass converts an incident status to a CSS class func incidentStatusToClass(status string) string { switch status { case "scheduled": return "incident-scheduled" case "investigating": return "incident-investigating" case "identified": return "incident-identified" case "monitoring": return "incident-monitoring" case "resolved": return "incident-resolved" default: return "incident-unknown" } }