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:
@@ -2,7 +2,6 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/monitor"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -237,11 +236,11 @@ func (m *Model) submitAlertForm() {
|
||||
|
||||
if m.editID > 0 {
|
||||
if err := m.store.UpdateAlert(m.editID, d.Name, d.AlertType, settings); err != nil {
|
||||
monitor.AddLog("Update alert failed: " + err.Error())
|
||||
m.engine.AddLog("Update alert failed: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := m.store.AddAlert(d.Name, d.AlertType, settings); err != nil {
|
||||
monitor.AddLog("Add alert failed: " + err.Error())
|
||||
m.engine.AddLog("Add alert failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
m.state = stateDashboard
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,7 +2,6 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/monitor"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -104,11 +103,11 @@ func (m *Model) submitUserForm() {
|
||||
d := m.userFormData
|
||||
if m.editID > 0 {
|
||||
if err := m.store.UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
|
||||
monitor.AddLog("Update user failed: " + err.Error())
|
||||
m.engine.AddLog("Update user failed: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := m.store.AddUser(d.Username, d.PublicKey, d.Role); err != nil {
|
||||
monitor.AddLog("Add user failed: " + err.Error())
|
||||
m.engine.AddLog("Add user failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
m.state = stateUsers
|
||||
|
||||
+10
-13
@@ -69,6 +69,7 @@ type Model struct {
|
||||
|
||||
collapsed map[int]bool
|
||||
store store.Store
|
||||
engine *monitor.Engine
|
||||
|
||||
// harmonica animation state
|
||||
pulseSpring harmonica.Spring
|
||||
@@ -81,7 +82,7 @@ type Model struct {
|
||||
users []models.User
|
||||
}
|
||||
|
||||
func InitialModel(isAdmin bool, s store.Store) Model {
|
||||
func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
||||
vpLogs := viewport.New(100, 20)
|
||||
vpLogs.SetContent("Waiting for logs...")
|
||||
z := zone.New()
|
||||
@@ -92,6 +93,7 @@ func InitialModel(isAdmin bool, s store.Store) Model {
|
||||
maxTableRows: 5,
|
||||
isAdmin: isAdmin,
|
||||
store: s,
|
||||
engine: eng,
|
||||
zones: z,
|
||||
pulseSpring: spring,
|
||||
collapsed: make(map[int]bool),
|
||||
@@ -112,18 +114,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch m.deleteTab {
|
||||
case 0:
|
||||
if err := m.store.DeleteSite(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete site failed: " + err.Error())
|
||||
m.engine.AddLog("Delete site failed: " + err.Error())
|
||||
}
|
||||
monitor.RemoveSite(m.deleteID)
|
||||
m.engine.RemoveSite(m.deleteID)
|
||||
m.adjustCursor(len(m.sites) - 1)
|
||||
case 1:
|
||||
if err := m.store.DeleteAlert(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete alert failed: " + err.Error())
|
||||
m.engine.AddLog("Delete alert failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.alerts) - 1)
|
||||
case 3:
|
||||
if err := m.store.DeleteUser(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete user failed: " + err.Error())
|
||||
m.engine.AddLog("Delete user failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.users) - 1)
|
||||
}
|
||||
@@ -317,7 +319,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
case "p":
|
||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
||||
site := m.sites[m.cursor]
|
||||
monitor.ToggleSitePause(site.ID)
|
||||
m.engine.ToggleSitePause(site.ID)
|
||||
site.Paused = !site.Paused
|
||||
_ = m.store.UpdateSitePaused(site.ID, site.Paused)
|
||||
m.refreshData()
|
||||
@@ -433,12 +435,7 @@ func (m *Model) adjustCursor(newLen int) {
|
||||
}
|
||||
|
||||
func (m *Model) refreshData() {
|
||||
monitor.Mutex.RLock()
|
||||
var allSites []models.Site
|
||||
for _, s := range monitor.LiveState {
|
||||
allSites = append(allSites, s)
|
||||
}
|
||||
monitor.Mutex.RUnlock()
|
||||
allSites := m.engine.GetAllSites()
|
||||
|
||||
var groups, ungrouped []models.Site
|
||||
children := make(map[int][]models.Site)
|
||||
@@ -476,7 +473,7 @@ func (m *Model) refreshData() {
|
||||
m.users = users
|
||||
}
|
||||
}
|
||||
m.logViewport.SetContent(strings.Join(monitor.GetLogs(), "\n"))
|
||||
m.logViewport.SetContent(strings.Join(m.engine.GetLogs(), "\n"))
|
||||
|
||||
listLen := len(m.sites)
|
||||
if m.currentTab == 1 {
|
||||
|
||||
Reference in New Issue
Block a user