feat: ops layer — incidents, maintenance, status pages, badges, metrics (merge plan M4)

incidents + incident_updates + maintenance_windows + status_pages
tables. Monitor transitions auto-open incidents on down and
auto-resolve on recovery; an active maintenance window covering
the monitor suppresses alerts and auto-incidents, and status
pages report the covered monitor as `maintenance`.

Public status view: GET /api/status-pages/:slug/public aggregates
overall state, monitor uptime, systems, open incidents, and
scheduled maintenance; rendered read-only at /status/:slug.

GET /api/badge/:kind/:id.svg serves stateless shields; GET
/api/metrics exposes Prometheus text over existing tables;
POST /api/import/csv?kind=monitors|domains does header-driven
bulk import; /api/export v2 folds monitors/domains/systems rows
into the backup payload.

Frontend: /incidents with timeline + status flow, /status admin
for pages + windows, /status/:slug public page; nav and palette
wired.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:21:21 +02:00
1 parent 74c987c18a
commit c84a4be876
17 files changed
+2212 -11

No files matched your search

+6 -1
View File
@@ -498,10 +498,15 @@ func (s *Server) writeHeartbeat(m *monitor.Monitor, res *monitor.Result) {
}
// onMonitorTransition fires on real state flips (pending excluded upstream)
// — the notification seam for up/down events.
// — the notification + auto-incident seam for up/down events. An active
// maintenance window covering this monitor suppresses both.
func (s *Server) onMonitorTransition(m *monitor.Monitor, prev, next string) {
s.log.Info("monitor transition",
zap.String("monitor", m.Name), zap.String("from", prev), zap.String("to", next))
if s.inMaintenance(m.ID) {
return
}
s.autoIncident(m.ID, m.Name, next)
target := m.URL
if target == "" {
target = m.Hostname