diff options
Diffstat (limited to 'internal/server/server.go')
| -rw-r--r-- | internal/server/server.go | 122 |
1 files changed, 78 insertions, 44 deletions
diff --git a/internal/server/server.go b/internal/server/server.go index 4217dd2..499ca31 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -162,12 +162,15 @@ type MonitorData struct { 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 } @@ -269,6 +272,11 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { 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 @@ -559,50 +567,7 @@ func templateFuncs() template.FuncMap { } return "bg-neutral-200 dark:bg-neutral-800" }, - "simplifyError": func(err string) string { - if err == "" { - return "" - } - // Common error patterns to simplify - 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"): - // Extract status code if present - if idx := strings.Index(err, "status code"); idx != -1 { - rest := err[idx:] - // Try to find the number - for i, c := range rest { - if c >= '0' && c <= '9' { - end := i - for end < len(rest) && rest[end] >= '0' && rest[end] <= '9' { - end++ - } - return "HTTP " + rest[i:end] - } - } - } - 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" - } - }, + "simplifyError": simplifyErrorMessage, "tickTooltipData": func(tick *storage.TickData, mode, timezone string, hidePing bool) string { if tick == nil { data := map[string]interface{}{"header": "No data"} @@ -912,6 +877,75 @@ func formatLastUpdatedTooltip(t time.Time, timezone string) string { 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 { |