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

+10 -1
View File
@@ -61,6 +61,13 @@ type Server struct {
anaSecret string
anaTry map[string][]time.Time
statusMu sync.Mutex // the status page settings cache and the signing secret
statusCfg *StatusSettings
statusKey []byte
subMu sync.Mutex // per-address limit on public subscribe requests
subTry map[string][]time.Time
notifyWG sync.WaitGroup // status page notifications in flight
stop chan struct{}
wg sync.WaitGroup
}
@@ -75,7 +82,7 @@ func New(opts Options, st *Store) *Server {
if opts.HitDays <= 0 {
opts.HitDays = 365
}
return &Server{opts: opts, st: st, hub: newHub(), running: map[string]bool{}, loginTry: map[string][]time.Time{}, anaTry: map[string][]time.Time{}, stop: make(chan struct{})}
return &Server{opts: opts, st: st, hub: newHub(), running: map[string]bool{}, loginTry: map[string][]time.Time{}, anaTry: map[string][]time.Time{}, subTry: map[string][]time.Time{}, stop: make(chan struct{})}
}
func (s *Server) now() time.Time {
@@ -89,12 +96,14 @@ func (s *Server) now() time.Time {
func (s *Server) Start() {
s.every(time.Second, func() { s.runDueMonitors(s.now()) })
s.every(5*time.Second, func() { s.escalate(s.now()) })
s.every(30*time.Second, func() { s.runMaintenance(s.now()) })
s.every(time.Hour, func() { s.st.prune(s.now(), s.opts.MessageDays, s.opts.CheckDays, s.opts.HitDays) })
}
func (s *Server) Close() {
close(s.stop)
s.wg.Wait()
s.notifyWG.Wait()
}
func (s *Server) every(d time.Duration, f func()) {