Turn the status page into a full status page generator

Components can be put in groups and given public names. Public incidents carry
posts written for customers (the internal timeline stays internal), the
components they affect, and a page for each incident plus paged history.
Scheduled maintenance has its own window, shows affected components as under
maintenance, and is announced when scheduled, started, ended or canceled.

Branding (title, logo, accent checked for contrast, contact link, footer) and a
custom domain that serves only the status page. Visitors follow by RSS, by
confirmed email, or by webhook that must answer a test request. Admin UI for
all of it, tests, README and design notes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 17:05:38 -06:00
1 parent b4fd31a182
commit cf4d5b3ce2
12 files changed
+2403 -147

No files matched your search

+17 -70
View File
@@ -21,8 +21,7 @@ func (s *Server) Handler() http.Handler {
// pages
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) { servePage(w, indexHTML) })
mux.HandleFunc("GET /status", func(w http.ResponseWriter, r *http.Request) { servePage(w, statusHTML) })
mux.HandleFunc("GET /status.json", s.statusJSON)
s.statusRoutes(mux)
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") })
mux.HandleFunc("GET /metrics", s.metrics)
mux.HandleFunc("GET /script.js", s.serveTracker)
@@ -217,7 +216,7 @@ func (s *Server) Handler() http.Handler {
fail2(w, s.st.del("incident", r.PathValue("id")))
}))
return secureHeaders(mux)
return s.statusHost(secureHeaders(mux))
}
var ok = map[string]bool{"ok": true}
@@ -233,7 +232,11 @@ func secureHeaders(next http.Handler) http.Handler {
h.Set("X-Content-Type-Options", "nosniff")
h.Set("Referrer-Policy", "no-referrer")
h.Set("X-Frame-Options", "DENY")
h.Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src 'self' data:")
img := "'self' data:"
if strings.HasPrefix(r.URL.Path, "/status") {
img += " https:" // the status page can show a logo hosted elsewhere
}
h.Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'; img-src "+img)
next.ServeHTTP(w, r)
})
}
@@ -750,6 +753,9 @@ func (s *Server) declareHTTP(w http.ResponseWriter, r *http.Request) {
Summary string `json:"summary"`
Lead string `json:"lead"`
AlertIDs []string `json:"alertIds"`
// Public puts the incident on the status page, naming the affected components, as it is declared.
Public bool `json:"public"`
Components []string `json:"components"`
}
if !decode(r, &in) {
bad(w, "invalid request")
@@ -764,6 +770,13 @@ func (s *Server) declareHTTP(w http.ResponseWriter, r *http.Request) {
bad(w, err.Error())
return
}
if in.Public {
pub, comps := true, in.Components
if inc, err = s.UpdateIncident(inc.ID, me.Name, IncidentPatch{Public: &pub, Components: &comps}); err != nil {
fail(w, err)
return
}
}
writeJSON(w, 200, inc)
}
@@ -808,72 +821,6 @@ func (s *Server) heartbeat(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok")
}
// statusJSON feeds the public status page: only monitors and incidents marked public.
func (s *Server) statusJSON(w http.ResponseWriter, r *http.Request) {
type mon struct {
Name string `json:"name"`
Status string `json:"status"`
Uptime float64 `json:"uptime"`
Since int64 `json:"since"`
Days [][2]int `json:"days"` // [checks, failed] per UTC day, oldest first, ending today
}
type inc struct {
Title string `json:"title"`
Status string `json:"status"`
Severity string `json:"severity"`
Created int64 `json:"created"`
Resolved int64 `json:"resolved"`
Updates []Event `json:"updates"`
}
out := struct {
Monitors []mon `json:"monitors"`
Incidents []inc `json:"incidents"`
Overall string `json:"overall"`
Today string `json:"today"` // the UTC date of the last bar
}{[]mon{}, []inc{}, "operational", s.now().UTC().Format("2006-01-02")}
mons, _ := listOf[Monitor](s.st, "monitor")
for _, m := range mons {
if !m.Public || !m.Enabled {
continue
}
st := s.monitorState(m.ID)
days := s.dailyUptime(m.ID, uptimeDays, s.now())
var checks, fails int
for _, d := range days {
checks, fails = checks+d[0], fails+d[1]
}
uptime := -1.0 // no checks yet
if checks > 0 {
uptime = float64(checks-fails) * 100 / float64(checks)
}
out.Monitors = append(out.Monitors, mon{m.Name, st.Status, uptime, st.Since, days})
if st.Status == "down" {
out.Overall = "outage"
}
}
incs, _ := listOf[Incident](s.st, "incident")
sortIncidents(incs)
cut := s.now().AddDate(0, 0, -14).Unix()
for _, in := range incs {
if !in.Public || (in.Status == "resolved" && in.Resolved < cut) {
continue
}
var ups []Event
for _, e := range s.st.events("incident", in.ID) {
if e.Kind == "update" || e.Kind == "declared" {
e.Actor = "" // staff names stay off the public page
ups = append(ups, e)
}
}
out.Incidents = append(out.Incidents, inc{in.Title, in.Status, in.Severity, in.Created, in.Resolved, ups})
if in.Status != "resolved" && out.Overall == "operational" {
out.Overall = "degraded"
}
}
w.Header().Set("Cache-Control", "max-age=15")
writeJSON(w, 200, out)
}
func sortIncidents(list []Incident) {
for i := 1; i < len(list); i++ { // newest first; lists are short
for j := i; j > 0 && list[j].Created > list[j-1].Created; j-- {