refactor(monitor): encapsulate engine state, add graceful shutdown and tests

Replace all monitor package-level mutable state with Engine struct.
All state (liveState, logStore, histories, tokenIndex, HTTP clients)
is now encapsulated in Engine, created via NewEngine(store).

Key changes:
- Engine struct holds all monitor state with proper mutex protection
- Engine.Start(ctx) and monitorRoutine respect context cancellation
  for graceful shutdown — no more leaked goroutines
- cluster.runFollowerLoop also respects context for clean exit
- Token index (map[string]int) for O(1) push heartbeat lookup,
  replacing O(n) linear scan through LiveState
- UpdateSiteConfig preserves 8 runtime fields instead of copying
  17 config fields individually
- triggerAlert goroutines get 30s timeout context
- All consumers (TUI, server, cluster, main) receive *Engine via
  constructor/parameter — no package-level state access
- main.go creates context.WithCancel, passes to engine and cluster

First test suite: 12 tests across store and alert packages
- Store: CRUD for sites/alerts/users, push token generation,
  import/export round-trip, check history persistence
- Alert: Discord/Slack/Webhook payload format, HTTP 4xx error
  propagation, Ntfy headers, unknown provider returns nil
This commit is contained in:
2026-05-15 08:21:17 -04:00
parent 0e6dc774cb
commit f023e38fdc
11 changed files with 705 additions and 315 deletions
+4 -5
View File
@@ -3,7 +3,6 @@ package tui
import (
"fmt"
"go-upkeep/internal/models"
"go-upkeep/internal/monitor"
"net/url"
"strconv"
"strings"
@@ -243,7 +242,7 @@ func (m Model) viewSitesTab() string {
name = limitStr(name, 13)
}
hist, _ := monitor.GetHistory(site.ID)
hist, _ := m.engine.GetHistory(site.ID)
var spark string
if site.Type == "push" {
spark = heartbeatSparkline(hist.Statuses, sparkWidth)
@@ -508,12 +507,12 @@ func (m *Model) submitSiteForm() {
if m.editID > 0 {
if err := m.store.UpdateSite(site); err != nil {
monitor.AddLog("Update site failed: " + err.Error())
m.engine.AddLog("Update site failed: " + err.Error())
}
monitor.UpdateSiteConfig(site)
m.engine.UpdateSiteConfig(site)
} else {
if err := m.store.AddSite(site); err != nil {
monitor.AddLog("Add site failed: " + err.Error())
m.engine.AddLog("Add site failed: " + err.Error())
}
}
m.state = stateDashboard