aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-20 16:54:15 -0800
committerFuwn <[email protected]>2026-01-20 16:54:15 -0800
commitcb8e8a750a275b9fae116cd01210e0ec50f62b55 (patch)
tree5a8d985d706bd57cfaa3ee18c4bf8929f6895c71
parentfeat: Add new API endpoints (health, summary, uptime, incidents) (diff)
downloadkaze-cb8e8a750a275b9fae116cd01210e0ec50f62b55.tar.xz
kaze-cb8e8a750a275b9fae116cd01210e0ec50f62b55.zip
feat: Add SVG status badge endpoint
GET /api/badge/{name}.svg - Shields.io style status badges (always public) Options: - ?label=Custom - Override the label text (default: monitor name) - ?style=flat|plastic - Badge style (default: flat) - ?type=status|uptime - Show status or uptime percentage (default: status) Colors: - Green (#22c55e) for up / >=99% uptime - Yellow (#eab308) for degraded / >=95% uptime - Red (#ef4444) for down / <95% uptime - Gray (#9ca3af) for unknown Example: ![Status](https://status.example.com/api/badge/Website.svg)
-rw-r--r--config.example.yaml3
-rw-r--r--internal/server/server.go131
2 files changed, 134 insertions, 0 deletions
diff --git a/config.example.yaml b/config.example.yaml
index 706e3dc..91d17e9 100644
--- a/config.example.yaml
+++ b/config.example.yaml
@@ -379,6 +379,9 @@ incidents:
# Supports ?period=1h|24h|7d|30d|90d (default: 24h)
# GET /api/incidents - List incidents from config
# Supports ?filter=all|active|resolved|scheduled (default: all)
+# GET /api/badge/{name}.svg - SVG status badge (always public, shields.io style)
+# Supports: ?label=custom&style=flat|plastic&type=status|uptime
+# Example: ![Status](https://status.example.com/api/badge/Website.svg)
#
# Authentication (when access is "authenticated"):
# - Header: X-API-Key: your-secret-key
diff --git a/internal/server/server.go b/internal/server/server.go
index 86ee2a3..b835955 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -77,6 +77,9 @@ func New(cfg *config.Config, store *storage.Storage, sched *monitor.Scheduler, l
// Health check - always public (for load balancers, monitoring)
mux.HandleFunc("GET /api/health", s.handleAPIHealth)
+ // Badge endpoint - always public (for embedding in READMEs, docs)
+ mux.HandleFunc("GET /api/badge/{name}.svg", s.handleAPIBadge)
+
// Full page data endpoint - public if refresh_mode=api, otherwise follows api.access
if cfg.Display.RefreshMode == "api" {
mux.HandleFunc("GET /api/page", s.handleAPIPage)
@@ -648,6 +651,134 @@ func (s *Server) handleAPIHealth(w http.ResponseWriter, r *http.Request) {
s.jsonResponse(w, map[string]string{"status": "ok"})
}
+// handleAPIBadge returns an SVG status badge for a monitor (shields.io style)
+func (s *Server) handleAPIBadge(w http.ResponseWriter, r *http.Request) {
+ name := r.PathValue("name")
+ if name == "" {
+ http.Error(w, "Monitor name required", http.StatusBadRequest)
+ return
+ }
+
+ // Get monitor stats
+ stats, err := s.storage.GetMonitorStats(r.Context(), name)
+ if err != nil {
+ s.logger.Error("failed to get monitor stats for badge", "monitor", name, "error", err)
+ // Return a gray "unknown" badge on error
+ s.serveBadge(w, r, name, "unknown", "#9ca3af")
+ return
+ }
+
+ // Determine status and color
+ var status, color string
+ switch stats.CurrentStatus {
+ case "up":
+ status = "up"
+ color = "#22c55e" // green-500
+ case "degraded":
+ status = "degraded"
+ color = "#eab308" // yellow-500
+ case "down":
+ status = "down"
+ color = "#ef4444" // red-500
+ default:
+ status = "unknown"
+ color = "#9ca3af" // gray-400
+ }
+
+ // Check for custom label
+ label := r.URL.Query().Get("label")
+ if label == "" {
+ label = name
+ }
+
+ // Check for style (flat or plastic, default: flat)
+ style := r.URL.Query().Get("style")
+ if style != "plastic" {
+ style = "flat"
+ }
+
+ // Check if uptime should be shown instead of status
+ if r.URL.Query().Get("type") == "uptime" {
+ status = fmt.Sprintf("%.1f%%", stats.UptimePercent)
+ if stats.UptimePercent >= 99.0 {
+ color = "#22c55e"
+ } else if stats.UptimePercent >= 95.0 {
+ color = "#eab308"
+ } else {
+ color = "#ef4444"
+ }
+ }
+
+ s.serveBadge(w, r, label, status, color)
+}
+
+// serveBadge generates and serves an SVG badge
+func (s *Server) serveBadge(w http.ResponseWriter, r *http.Request, label, status, color string) {
+ style := r.URL.Query().Get("style")
+ if style != "plastic" {
+ style = "flat"
+ }
+
+ // Calculate widths (approximate, 6px per character + padding)
+ labelWidth := len(label)*6 + 10
+ statusWidth := len(status)*6 + 10
+ totalWidth := labelWidth + statusWidth
+
+ var svg string
+ if style == "plastic" {
+ // Plastic style with gradient
+ svg = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
+ <linearGradient id="b" x2="0" y2="100%%">
+ <stop offset="0" stop-color="#fff" stop-opacity=".7"/>
+ <stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
+ <stop offset=".9" stop-color="#000" stop-opacity=".3"/>
+ <stop offset="1" stop-color="#000" stop-opacity=".5"/>
+ </linearGradient>
+ <clipPath id="a">
+ <rect width="%d" height="20" rx="3" fill="#fff"/>
+ </clipPath>
+ <g clip-path="url(#a)">
+ <rect width="%d" height="20" fill="#555"/>
+ <rect x="%d" width="%d" height="20" fill="%s"/>
+ <rect width="%d" height="20" fill="url(#b)"/>
+ </g>
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
+ <text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text>
+ <text x="%d" y="14">%s</text>
+ <text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text>
+ <text x="%d" y="14">%s</text>
+ </g>
+</svg>`,
+ totalWidth, totalWidth, labelWidth, labelWidth, statusWidth, color, totalWidth,
+ labelWidth/2, label, labelWidth/2, label,
+ labelWidth+statusWidth/2, status, labelWidth+statusWidth/2, status)
+ } else {
+ // Flat style (default)
+ svg = fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20">
+ <clipPath id="a">
+ <rect width="%d" height="20" rx="3" fill="#fff"/>
+ </clipPath>
+ <g clip-path="url(#a)">
+ <rect width="%d" height="20" fill="#555"/>
+ <rect x="%d" width="%d" height="20" fill="%s"/>
+ </g>
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
+ <text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text>
+ <text x="%d" y="14">%s</text>
+ <text x="%d" y="15" fill="#010101" fill-opacity=".3">%s</text>
+ <text x="%d" y="14">%s</text>
+ </g>
+</svg>`,
+ totalWidth, totalWidth, labelWidth, labelWidth, statusWidth, color,
+ labelWidth/2, label, labelWidth/2, label,
+ labelWidth+statusWidth/2, status, labelWidth+statusWidth/2, status)
+ }
+
+ w.Header().Set("Content-Type", "image/svg+xml")
+ w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
+ w.Write([]byte(svg))
+}
+
// APISummaryResponse contains a lightweight status overview
type APISummaryResponse struct {
OverallStatus string `json:"overall_status"`