package monitor import ( "context" "database/sql" "errors" "fmt" "regexp" "strings" "time" ) type ProbeGroup struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` Status string `json:"status"` Monitors []Monitor `json:"monitors,omitempty"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type ProbeGroupInput struct { Name string `json:"name"` Description string `json:"description"` MonitorIDs []int64 `json:"monitor_ids"` } type StatusPage struct { ID int64 `json:"id"` Name string `json:"name"` Slug string `json:"slug"` Description string `json:"description"` Status string `json:"status"` Enabled bool `json:"enabled"` ServiceIDs []int64 `json:"service_ids"` Services []ProbeGroup `json:"services,omitempty"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type StatusPageInput struct { Name string `json:"name"` Slug string `json:"slug"` Description string `json:"description"` Enabled *bool `json:"enabled"` ServiceIDs []int64 `json:"service_ids"` } var slugRx = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`) func aggregateStatus(ms []Monitor) string { if len(ms) == 0 { return "unknown" } hasMaint, hasPending, hasPaused := false, false, false for _, m := range ms { switch m.Status { case "down": return "down" case "maintenance": hasMaint = true case "pending": hasPending = true case "paused": hasPaused = true } } if hasMaint { return "maintenance" } if hasPending { return "pending" } if hasPaused { return "paused" } return "up" } func (s *Service) ListGroups(ctx context.Context) ([]ProbeGroup, error) { rows, err := s.db.QueryContext(ctx, `SELECT id,name,description,created_at,updated_at FROM monitor_services ORDER BY name`) if err != nil { return nil, err } out := []ProbeGroup{} for rows.Next() { var g ProbeGroup if err := rows.Scan(&g.ID, &g.Name, &g.Description, &g.CreatedAt, &g.UpdatedAt); err != nil { _ = rows.Close() return nil, err } out = append(out, g) } if err := rows.Err(); err != nil { _ = rows.Close() return nil, err } if err := rows.Close(); err != nil { return nil, err } // Load monitors once and group in memory. This avoids an N+1 query pattern // on the dashboard and keeps refresh cost predictable with many services. all, err := s.List(ctx) if err != nil { return nil, err } byService := make(map[int64][]Monitor, len(out)) for _, m := range all { if m.ServiceID != nil { byService[*m.ServiceID] = append(byService[*m.ServiceID], m) } } for i := range out { out[i].Monitors = byService[out[i].ID] if out[i].Monitors == nil { out[i].Monitors = []Monitor{} } out[i].Status = aggregateStatus(out[i].Monitors) } return out, nil } func (s *Service) GetGroup(ctx context.Context, id int64) (ProbeGroup, error) { var g ProbeGroup if err := s.db.QueryRowContext(ctx, `SELECT id,name,description,created_at,updated_at FROM monitor_services WHERE id=?`, id).Scan(&g.ID, &g.Name, &g.Description, &g.CreatedAt, &g.UpdatedAt); err != nil { return g, err } ms, err := s.listGroupMonitors(ctx, id) if err != nil { return g, err } g.Monitors = ms g.Status = aggregateStatus(ms) return g, nil } func (s *Service) listGroupMonitors(ctx context.Context, id int64) ([]Monitor, error) { rows, err := s.db.QueryContext(ctx, selectMonitor+` WHERE m.service_id=? ORDER BY m.name`, time.Now().Add(-24*time.Hour).Unix(), id) if err != nil { return nil, err } defer rows.Close() out := []Monitor{} for rows.Next() { m, e := scanMonitor(rows) if e != nil { return nil, e } out = append(out, m) } return out, rows.Err() } func normalizeGroupInput(in *ProbeGroupInput) error { in.Name = strings.TrimSpace(in.Name) in.Description = strings.TrimSpace(in.Description) if in.Name == "" || len(in.Name) > 120 || strings.ContainsAny(in.Name, "\r\n") { return errors.New("valid service name required") } if len(in.Description) > 2000 { return errors.New("service description too long") } if in.MonitorIDs != nil { seen := map[int64]bool{} ids := make([]int64, 0, len(in.MonitorIDs)) for _, id := range in.MonitorIDs { if id < 1 { return errors.New("monitor_ids must contain positive IDs") } if !seen[id] { seen[id] = true ids = append(ids, id) } } in.MonitorIDs = ids } return nil } func assignGroupMonitors(ctx context.Context, tx *sql.Tx, groupID int64, monitorIDs []int64, clearExisting bool) error { if clearExisting { if _, err := tx.ExecContext(ctx, `UPDATE monitors SET service_id=NULL,updated_at=? WHERE service_id=?`, time.Now().Unix(), groupID); err != nil { return err } } for _, monitorID := range monitorIDs { r, err := tx.ExecContext(ctx, `UPDATE monitors SET service_id=?,updated_at=? WHERE id=?`, groupID, time.Now().Unix(), monitorID) if err != nil { return err } n, _ := r.RowsAffected() if n == 0 { return fmt.Errorf("monitor %d not found", monitorID) } } return nil } func (s *Service) CreateGroup(ctx context.Context, in ProbeGroupInput) (ProbeGroup, error) { if err := normalizeGroupInput(&in); err != nil { return ProbeGroup{}, err } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return ProbeGroup{}, err } defer tx.Rollback() now := time.Now().Unix() r, err := tx.ExecContext(ctx, `INSERT INTO monitor_services(name,description,created_at,updated_at) VALUES(?,?,?,?)`, in.Name, in.Description, now, now) if err != nil { return ProbeGroup{}, err } id, _ := r.LastInsertId() if in.MonitorIDs != nil { if err := assignGroupMonitors(ctx, tx, id, in.MonitorIDs, false); err != nil { return ProbeGroup{}, err } } if err := tx.Commit(); err != nil { return ProbeGroup{}, err } return s.GetGroup(ctx, id) } func (s *Service) UpdateGroup(ctx context.Context, id int64, in ProbeGroupInput) (ProbeGroup, error) { if err := normalizeGroupInput(&in); err != nil { return ProbeGroup{}, err } tx, err := s.db.BeginTx(ctx, nil) if err != nil { return ProbeGroup{}, err } defer tx.Rollback() r, err := tx.ExecContext(ctx, `UPDATE monitor_services SET name=?,description=?,updated_at=? WHERE id=?`, in.Name, in.Description, time.Now().Unix(), id) if err != nil { return ProbeGroup{}, err } n, _ := r.RowsAffected() if n == 0 { return ProbeGroup{}, sql.ErrNoRows } if in.MonitorIDs != nil { if err := assignGroupMonitors(ctx, tx, id, in.MonitorIDs, true); err != nil { return ProbeGroup{}, err } } if err := tx.Commit(); err != nil { return ProbeGroup{}, err } return s.GetGroup(ctx, id) } func (s *Service) DeleteGroup(ctx context.Context, id int64) error { _, e := s.db.ExecContext(ctx, `DELETE FROM monitor_services WHERE id=?`, id) return e } func normalizeStatusPage(in *StatusPageInput) error { in.Name = strings.TrimSpace(in.Name) in.Slug = strings.ToLower(strings.TrimSpace(in.Slug)) in.Description = strings.TrimSpace(in.Description) if in.Name == "" || len(in.Name) > 120 || strings.ContainsAny(in.Name, "\r\n") || !slugRx.MatchString(in.Slug) { return errors.New("name and valid slug required (lowercase letters, numbers, hyphens)") } if len(in.Description) > 4000 { return errors.New("status page description too long") } seen := map[int64]bool{} var ids []int64 for _, id := range in.ServiceIDs { if id > 0 && !seen[id] { seen[id] = true ids = append(ids, id) } } in.ServiceIDs = ids return nil } func (s *Service) ListStatusPages(ctx context.Context) ([]StatusPage, error) { rows, e := s.db.QueryContext(ctx, `SELECT id,name,slug,description,enabled,created_at,updated_at FROM status_pages ORDER BY name`) if e != nil { return nil, e } out := []StatusPage{} for rows.Next() { var p StatusPage if e = rows.Scan(&p.ID, &p.Name, &p.Slug, &p.Description, &p.Enabled, &p.CreatedAt, &p.UpdatedAt); e != nil { _ = rows.Close() return nil, e } p.ServiceIDs = []int64{} out = append(out, p) } if e = rows.Err(); e != nil { _ = rows.Close() return nil, e } if e = rows.Close(); e != nil { return nil, e } // Resolve page memberships only after releasing the one SQLite connection. for i := range out { out[i].ServiceIDs, e = s.statusPageServiceIDs(ctx, out[i].ID) if e != nil { return nil, e } } return out, nil } func (s *Service) GetStatusPage(ctx context.Context, id int64) (StatusPage, error) { var p StatusPage e := s.db.QueryRowContext(ctx, `SELECT id,name,slug,description,enabled,created_at,updated_at FROM status_pages WHERE id=?`, id).Scan(&p.ID, &p.Name, &p.Slug, &p.Description, &p.Enabled, &p.CreatedAt, &p.UpdatedAt) if e != nil { return p, e } p.ServiceIDs, e = s.statusPageServiceIDs(ctx, id) return p, e } func (s *Service) statusPageServiceIDs(ctx context.Context, id int64) ([]int64, error) { rows, e := s.db.QueryContext(ctx, `SELECT service_id FROM status_page_services WHERE page_id=? ORDER BY sort_order,service_id`, id) if e != nil { return nil, e } defer rows.Close() out := []int64{} for rows.Next() { var x int64 if e = rows.Scan(&x); e != nil { return nil, e } out = append(out, x) } return out, rows.Err() } func (s *Service) savePageServices(ctx context.Context, tx *sql.Tx, id int64, ids []int64) error { if _, e := tx.ExecContext(ctx, `DELETE FROM status_page_services WHERE page_id=?`, id); e != nil { return e } for i, sid := range ids { if _, e := tx.ExecContext(ctx, `INSERT INTO status_page_services(page_id,service_id,sort_order) VALUES(?,?,?)`, id, sid, i); e != nil { return e } } return nil } func (s *Service) CreateStatusPage(ctx context.Context, in StatusPageInput) (StatusPage, error) { if e := normalizeStatusPage(&in); e != nil { return StatusPage{}, e } en := true if in.Enabled != nil { en = *in.Enabled } tx, e := s.db.BeginTx(ctx, nil) if e != nil { return StatusPage{}, e } defer tx.Rollback() now := time.Now().Unix() r, e := tx.ExecContext(ctx, `INSERT INTO status_pages(name,slug,description,enabled,created_at,updated_at) VALUES(?,?,?,?,?,?)`, in.Name, in.Slug, in.Description, en, now, now) if e != nil { return StatusPage{}, e } id, _ := r.LastInsertId() if e = s.savePageServices(ctx, tx, id, in.ServiceIDs); e != nil { return StatusPage{}, e } if e = tx.Commit(); e != nil { return StatusPage{}, e } return s.GetStatusPage(ctx, id) } func (s *Service) UpdateStatusPage(ctx context.Context, id int64, in StatusPageInput) (StatusPage, error) { if e := normalizeStatusPage(&in); e != nil { return StatusPage{}, e } old, e := s.GetStatusPage(ctx, id) if e != nil { return StatusPage{}, e } en := old.Enabled if in.Enabled != nil { en = *in.Enabled } tx, e := s.db.BeginTx(ctx, nil) if e != nil { return StatusPage{}, e } defer tx.Rollback() if _, e = tx.ExecContext(ctx, `UPDATE status_pages SET name=?,slug=?,description=?,enabled=?,updated_at=? WHERE id=?`, in.Name, in.Slug, in.Description, en, time.Now().Unix(), id); e != nil { return StatusPage{}, e } if e = s.savePageServices(ctx, tx, id, in.ServiceIDs); e != nil { return StatusPage{}, e } if e = tx.Commit(); e != nil { return StatusPage{}, e } return s.GetStatusPage(ctx, id) } func (s *Service) DeleteStatusPage(ctx context.Context, id int64) error { _, e := s.db.ExecContext(ctx, `DELETE FROM status_pages WHERE id=?`, id) return e } func (s *Service) PublicStatusPage(ctx context.Context, slug string) (StatusPage, error) { p := StatusPage{Services: []ProbeGroup{}, ServiceIDs: []int64{}} e := s.db.QueryRowContext(ctx, `SELECT id,name,slug,description,enabled,created_at,updated_at FROM status_pages WHERE slug=? AND enabled=1`, slug).Scan(&p.ID, &p.Name, &p.Slug, &p.Description, &p.Enabled, &p.CreatedAt, &p.UpdatedAt) if e != nil { return p, e } rows, e := s.db.QueryContext(ctx, `SELECT ms.id,ms.name,ms.description,ms.created_at,ms.updated_at FROM monitor_services ms JOIN status_page_services ps ON ps.service_id=ms.id WHERE ps.page_id=? ORDER BY ps.sort_order,ms.name`, p.ID) if e != nil { return p, e } for rows.Next() { var g ProbeGroup if e = rows.Scan(&g.ID, &g.Name, &g.Description, &g.CreatedAt, &g.UpdatedAt); e != nil { _ = rows.Close() return p, e } g.Monitors = []Monitor{} p.Services = append(p.Services, g) p.ServiceIDs = append(p.ServiceIDs, g.ID) } if e = rows.Err(); e != nil { _ = rows.Close() return p, e } if e = rows.Close(); e != nil { return p, e } all, e := s.List(ctx) if e != nil { return p, e } byService := map[int64][]Monitor{} for _, m := range all { if m.ServiceID != nil { byService[*m.ServiceID] = append(byService[*m.ServiceID], m) } } for gi := range p.Services { p.Services[gi].Monitors = byService[p.Services[gi].ID] if p.Services[gi].Monitors == nil { p.Services[gi].Monitors = []Monitor{} } p.Services[gi].Status = aggregateStatus(p.Services[gi].Monitors) } p.Status = aggregateServiceStatuses(p.Services) return p, nil } func aggregateServiceStatuses(groups []ProbeGroup) string { if len(groups) == 0 { return "unknown" } hasMaint, hasPending, hasPaused := false, false, false for _, g := range groups { switch g.Status { case "down": return "down" case "maintenance": hasMaint = true case "pending", "unknown": hasPending = true case "paused": hasPaused = true } } if hasMaint { return "maintenance" } if hasPending { return "pending" } if hasPaused { return "paused" } return "up" }