Add Web Push, mobile login and manual escalate for the mobile companion

Sandbar sends Web Push (VAPID + aes128gcm, standard library only) to devices
registered for a topic, serves the companion PWA at /app/, allows one CORS
origin, and exposes a read-only Hammerhead edge view and POST .../escalate.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
VeillaxandClaude Sonnet 5.5 committed 2026-10-27 16:57:07 -06:00
1 parent b4fd31a182
commit 042a60152c
12 files changed
+1056 -18

No files matched your search

+22 -5
View File
@@ -162,7 +162,7 @@ func (s *Server) RaiseAlert(key, source, summary, detail, severity, policyID str
return nil, false, err
}
s.st.addEvent("alert", a.ID, "created", source, summary, now)
s.broadcast(Note{Status: "firing", Kind: "alert", Title: summary, Message: s.alertText(a), Severity: severity, Link: s.link("/#alerts"), Time: now.Unix()}, "alerts")
s.broadcast(Note{Status: "firing", Kind: "alert", Title: summary, Message: s.alertText(a), Severity: severity, Link: s.link("/#alerts"), Time: now.Unix(), Alert: a.ID}, "alerts")
return a, true, nil
}
@@ -195,7 +195,7 @@ func (s *Server) ResolveAlert(id, actor string) error {
return err
}
s.st.addEvent("alert", id, "resolved", actor, "resolved", now)
s.broadcast(Note{Status: "resolved", Kind: "alert", Title: a.Summary, Message: "Resolved: " + a.Summary, Severity: a.Severity, Link: s.link("/#alerts"), Time: now.Unix()}, "alerts")
s.broadcast(Note{Status: "resolved", Kind: "alert", Title: a.Summary, Message: "Resolved: " + a.Summary, Severity: a.Severity, Link: s.link("/#alerts"), Time: now.Unix(), Alert: a.ID}, "alerts")
return nil
}
@@ -212,7 +212,7 @@ func (s *Server) AckAlert(id, actor string) error {
return err
}
s.st.addEvent("alert", id, "acked", actor, "acknowledged", now)
s.broadcast(Note{Status: "acked", Kind: "alert", Title: a.Summary, Message: "Acknowledged by " + actor + ": " + a.Summary, Time: now.Unix()}, "alerts")
s.broadcast(Note{Status: "acked", Kind: "alert", Title: a.Summary, Message: "Acknowledged by " + actor + ": " + a.Summary, Time: now.Unix(), Alert: a.ID}, "alerts")
return nil
}
@@ -233,6 +233,23 @@ func (s *Server) SnoozeAlert(id, actor string, d time.Duration) error {
return nil
}
// EscalateAlert moves a firing alert to its next step now instead of waiting for the delay.
func (s *Server) EscalateAlert(id, actor string) error {
a, err := s.getAlert(id)
if err != nil {
return err
}
if a.Status != "firing" {
return errors.New("only a firing alert can be escalated")
}
if a.PolicyID == "" {
return errors.New("this alert has no escalation policy")
}
s.st.addEvent("alert", id, "note", actor, "escalated by hand", s.now())
s.runStep(id, s.now())
return nil
}
// ---------- escalation ----------
// escalate runs every alert step that has come due.
@@ -289,7 +306,7 @@ func (s *Server) runStep(id string, now time.Time) {
for _, uid := range people {
if u, err := s.userByID(uid); err == nil {
s.notifyUser(u, Note{Status: "firing", Kind: "alert", Title: a.Summary, Message: s.alertText(a), Severity: a.Severity,
Link: s.link("/#alerts"), Time: now.Unix()})
Link: s.link("/#alerts"), Time: now.Unix(), Alert: a.ID})
names = append(names, u.Name)
}
}
@@ -314,7 +331,7 @@ func (s *Server) runStep(id string, now time.Time) {
// notifyUser reaches one person: a message on their personal topic (u-<id>, which
// their phone subscribes to) and an email if the server can send mail.
func (s *Server) notifyUser(u *User, n Note) {
s.Publish(userTopic(u.ID), n.subject(), n.text(), n.priority(), n.tags())
s.publish(userTopic(u.ID), n.subject(), n.text(), n.priority(), n.tags(), n.Alert, n.Status)
if u.Email != "" && (s.opts.SMTP.Host != "" || s.opts.Mailer != nil) {
go func() {
if err := s.Mail(u.Email, n.subject(), n.text()); err != nil {