diff options
| author | Fuwn <[email protected]> | 2026-01-20 16:49:50 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-20 16:49:50 -0800 |
| commit | 31cc0a33a378dbc8cb0e3c70b40d6570f3bed2e4 (patch) | |
| tree | 6de6f3354af8095be83740b3b24ca1490892a999 /internal/server | |
| parent | perf: Optimize API refresh to single /api/page request (diff) | |
| download | kaze-31cc0a33a378dbc8cb0e3c70b40d6570f3bed2e4.tar.xz kaze-31cc0a33a378dbc8cb0e3c70b40d6570f3bed2e4.zip | |
feat: Add new API endpoints (health, summary, uptime, incidents)
New endpoints:
- GET /api/health - Simple health check, always public (for load balancers)
- GET /api/summary - Lightweight status overview (counts + overall status)
- GET /api/uptime/{name}?period=1h|24h|7d|30d|90d - Historical uptime stats
- GET /api/incidents?filter=all|active|resolved|scheduled - List incidents
All new endpoints (except /api/health) follow api.access rules.
Diffstat (limited to 'internal/server')
| -rw-r--r-- | internal/server/server.go | 183 |
1 files changed, 183 insertions, 0 deletions
diff --git a/internal/server/server.go b/internal/server/server.go index d7dacaa..86ee2a3 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -70,6 +70,12 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l mux.HandleFunc("GET /api/status", s.withAPIAuth(s.handleAPIStatus)) mux.HandleFunc("GET /api/monitor/{name}", s.withAPIAuth(s.handleAPIMonitor)) mux.HandleFunc("GET /api/history/{name}", s.withAPIAuth(s.handleAPIHistory)) + mux.HandleFunc("GET /api/summary", s.withAPIAuth(s.handleAPISummary)) + mux.HandleFunc("GET /api/uptime/{name}", s.withAPIAuth(s.handleAPIUptime)) + mux.HandleFunc("GET /api/incidents", s.withAPIAuth(s.handleAPIIncidents)) + + // Health check - always public (for load balancers, monitoring) + mux.HandleFunc("GET /api/health", s.handleAPIHealth) // Full page data endpoint - public if refresh_mode=api, otherwise follows api.access if cfg.Display.RefreshMode == "api" { @@ -637,6 +643,183 @@ func (s *Server) handleAPIPage(w http.ResponseWriter, r *http.Request) { s.jsonResponse(w, response) } +// handleAPIHealth returns a simple health check response (always public) +func (s *Server) handleAPIHealth(w http.ResponseWriter, r *http.Request) { + s.jsonResponse(w, map[string]string{"status": "ok"}) +} + +// APISummaryResponse contains a lightweight status overview +type APISummaryResponse struct { + OverallStatus string `json:"overall_status"` + Counts StatusCounts `json:"counts"` + LastUpdated time.Time `json:"last_updated"` +} + +// handleAPISummary returns a lightweight status summary (no history data) +func (s *Server) handleAPISummary(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 + } + + response := APISummaryResponse{ + LastUpdated: time.Now(), + } + + overallUp := true + hasDegraded := false + + for _, group := range s.config.Groups { + for _, monCfg := range group.Monitors { + stat, ok := stats[monCfg.Name] + if !ok { + continue + } + + response.Counts.Total++ + switch stat.CurrentStatus { + case "down": + overallUp = false + response.Counts.Down++ + case "degraded": + hasDegraded = true + response.Counts.Degraded++ + case "up": + response.Counts.Up++ + } + } + } + + if !overallUp { + response.OverallStatus = "Major Outage" + } else if hasDegraded { + response.OverallStatus = "Partial Outage" + } else { + response.OverallStatus = "All Systems Operational" + } + + s.jsonResponse(w, response) +} + +// APIUptimeResponse contains historical uptime statistics +type APIUptimeResponse struct { + Monitor string `json:"monitor"` + Period string `json:"period"` + UptimePercent float64 `json:"uptime_percent"` + TotalChecks int64 `json:"total_checks"` + SuccessChecks int64 `json:"success_checks"` + FailedChecks int64 `json:"failed_checks"` +} + +// handleAPIUptime returns historical uptime for a specific period +func (s *Server) handleAPIUptime(w http.ResponseWriter, r *http.Request) { + name := r.PathValue("name") + if name == "" { + s.jsonError(w, "Monitor name required", http.StatusBadRequest) + return + } + + // Parse period (default: 24h, options: 1h, 24h, 7d, 30d, 90d) + period := r.URL.Query().Get("period") + if period == "" { + period = "24h" + } + + var duration time.Duration + switch period { + case "1h": + duration = time.Hour + case "24h": + duration = 24 * time.Hour + case "7d": + duration = 7 * 24 * time.Hour + case "30d": + duration = 30 * 24 * time.Hour + case "90d": + duration = 90 * 24 * time.Hour + default: + s.jsonError(w, "Invalid period (use: 1h, 24h, 7d, 30d, 90d)", http.StatusBadRequest) + return + } + + stats, err := s.storage.GetUptimeStats(r.Context(), name, duration) + if err != nil { + s.jsonError(w, "Failed to get uptime stats", http.StatusInternalServerError) + return + } + + s.jsonResponse(w, APIUptimeResponse{ + Monitor: name, + Period: period, + UptimePercent: stats.UptimePercent, + TotalChecks: stats.TotalChecks, + SuccessChecks: stats.SuccessChecks, + FailedChecks: stats.FailedChecks, + }) +} + +// APIIncidentResponse contains incident data for the API +type APIIncidentResponse struct { + Title string `json:"title"` + Status string `json:"status"` + Message string `json:"message"` + IsActive bool `json:"is_active"` + CreatedAt *time.Time `json:"created_at,omitempty"` + ResolvedAt *time.Time `json:"resolved_at,omitempty"` + ScheduledStart *time.Time `json:"scheduled_start,omitempty"` + ScheduledEnd *time.Time `json:"scheduled_end,omitempty"` + Affected []string `json:"affected_monitors,omitempty"` +} + +// handleAPIIncidents returns active and recent incidents +func (s *Server) handleAPIIncidents(w http.ResponseWriter, r *http.Request) { + // Filter: all, active, resolved, scheduled (default: all) + filter := r.URL.Query().Get("filter") + + var incidents []APIIncidentResponse + + for _, inc := range s.config.Incidents { + isActive := inc.Status != "resolved" + isScheduled := inc.Status == "scheduled" + + // Apply filter + switch filter { + case "active": + if !isActive || isScheduled { + continue + } + case "resolved": + if isActive { + continue + } + case "scheduled": + if !isScheduled { + continue + } + } + + incidents = append(incidents, APIIncidentResponse{ + Title: inc.Title, + Status: inc.Status, + Message: inc.Message, + IsActive: isActive, + CreatedAt: inc.CreatedAt, + ResolvedAt: inc.ResolvedAt, + ScheduledStart: inc.ScheduledStart, + ScheduledEnd: inc.ScheduledEnd, + Affected: inc.AffectedMonitors, + }) + } + + s.jsonResponse(w, map[string]interface{}{ + "incidents": incidents, + "count": len(incidents), + }) +} + // jsonResponse writes a JSON response func (s *Server) jsonResponse(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") |