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

+19 -11
View File
@@ -21,7 +21,8 @@ type Incident struct {
Comms string `json:"comms"` // user id
Summary string `json:"summary"`
Postmortem string `json:"postmortem"`
Public bool `json:"public"` // shown on the status page
Public bool `json:"public"` // shown on the status page
Components []string `json:"components"` // monitor ids the status page says are affected
Created int64 `json:"created"`
Resolved int64 `json:"resolved"`
AlertIDs []string `json:"alertIds"`
@@ -66,7 +67,7 @@ func (s *Server) DeclareIncident(title, severity, summary, lead, actor string, a
return nil, errors.New("severity must be sev1, sev2 or sev3")
}
now := s.now()
in := &Incident{ID: newID(), Title: title, Severity: severity, Status: "investigating", Lead: lead, Summary: summary, Created: now.Unix(), AlertIDs: []string{}}
in := &Incident{ID: newID(), Title: title, Severity: severity, Status: "investigating", Lead: lead, Summary: summary, Created: now.Unix(), AlertIDs: []string{}, Components: []string{}}
for _, id := range alertIDs {
if a, err := s.getAlert(id); err == nil {
in.AlertIDs = append(in.AlertIDs, a.ID)
@@ -96,15 +97,16 @@ func firstNonEmpty(v ...string) string {
// IncidentPatch holds the fields an update may change; nil means unchanged.
type IncidentPatch struct {
Title *string `json:"title"`
Severity *string `json:"severity"`
Status *string `json:"status"`
Lead *string `json:"lead"`
Comms *string `json:"comms"`
Summary *string `json:"summary"`
Postmortem *string `json:"postmortem"`
Public *bool `json:"public"`
Note string `json:"note"` // text for the update announcement and timeline
Title *string `json:"title"`
Severity *string `json:"severity"`
Status *string `json:"status"`
Lead *string `json:"lead"`
Comms *string `json:"comms"`
Summary *string `json:"summary"`
Postmortem *string `json:"postmortem"`
Public *bool `json:"public"`
Components *[]string `json:"components"`
Note string `json:"note"` // text for the update announcement and timeline
}
func (s *Server) UpdateIncident(id, actor string, p IncidentPatch) (*Incident, error) {
@@ -113,6 +115,7 @@ func (s *Server) UpdateIncident(id, actor string, p IncidentPatch) (*Incident, e
return nil, err
}
now := s.now()
prevStatus := in.Status
var changes []string
if p.Severity != nil && *p.Severity != in.Severity {
if !validSeverity(*p.Severity) {
@@ -151,12 +154,17 @@ func (s *Server) UpdateIncident(id, actor string, p IncidentPatch) (*Incident, e
if p.Postmortem != nil {
in.Postmortem = *p.Postmortem
}
wasPublic, statusChanged := in.Public, in.Status != prevStatus
if p.Public != nil {
in.Public = *p.Public
}
if p.Components != nil {
in.Components = cleanIDs(*p.Components)
}
if err := s.st.put("incident", in.ID, in); err != nil {
return nil, err
}
s.postToStatusPage(in, wasPublic, statusChanged, p.Note, now)
if len(changes) > 0 || p.Note != "" {
text := strings.Join(changes, "; ")
if p.Note != "" {