Files
uptop/internal/monitor/history.go
T
lerko f023e38fdc 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
2026-05-15 08:21:17 -04:00

90 lines
1.9 KiB
Go

package monitor
import "time"
const maxHistoryLen = 30
type SiteHistory struct {
Latencies []time.Duration
Statuses []bool
TotalChecks int
UpChecks int
}
func (e *Engine) InitHistory() {
all, err := e.db.LoadAllHistory(maxHistoryLen)
if err != nil {
e.AddLog("Failed to load check history: " + err.Error())
return
}
e.histMu.Lock()
defer e.histMu.Unlock()
for siteID, records := range all {
h := &SiteHistory{}
for _, r := range records {
h.TotalChecks++
if r.IsUp {
h.UpChecks++
}
h.Latencies = append(h.Latencies, time.Duration(r.LatencyNs))
h.Statuses = append(h.Statuses, r.IsUp)
}
e.histories[siteID] = h
}
if len(all) > 0 {
e.AddLog("Loaded check history from database")
}
}
func (e *Engine) recordCheck(siteID int, latency time.Duration, isUp bool) {
e.histMu.Lock()
defer e.histMu.Unlock()
h, ok := e.histories[siteID]
if !ok {
h = &SiteHistory{}
e.histories[siteID] = h
}
h.TotalChecks++
if isUp {
h.UpChecks++
}
h.Latencies = append(h.Latencies, latency)
if len(h.Latencies) > maxHistoryLen {
h.Latencies = h.Latencies[len(h.Latencies)-maxHistoryLen:]
}
h.Statuses = append(h.Statuses, isUp)
if len(h.Statuses) > maxHistoryLen {
h.Statuses = h.Statuses[len(h.Statuses)-maxHistoryLen:]
}
go func() { _ = e.db.SaveCheck(siteID, latency.Nanoseconds(), isUp) }()
}
func (e *Engine) GetHistory(siteID int) (SiteHistory, bool) {
e.histMu.RLock()
defer e.histMu.RUnlock()
h, ok := e.histories[siteID]
if !ok {
return SiteHistory{}, false
}
cp := SiteHistory{
TotalChecks: h.TotalChecks,
UpChecks: h.UpChecks,
Latencies: make([]time.Duration, len(h.Latencies)),
Statuses: make([]bool, len(h.Statuses)),
}
copy(cp.Latencies, h.Latencies)
copy(cp.Statuses, h.Statuses)
return cp, true
}
func (e *Engine) removeHistory(siteID int) {
e.histMu.Lock()
defer e.histMu.Unlock()
delete(e.histories, siteID)
}