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:
1 parent
b4fd31a182
commit
cf4d5b3ce2
12 files changed
+2403
-147
No files matched your search
+17
-70
@@ -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-- {
|
||||
|
||||
Reference in new issue
Block a user