refactor: propagate context.Context through call chains
CI / test (pull_request) Successful in 1m56s
CI / lint (pull_request) Successful in 1m17s
CI / vulncheck (pull_request) Successful in 56s

Thread context.Context from callers instead of creating
context.Background() at every call site. Engine stores its
lifecycle ctx for use by triggerAlert goroutines. TUI Model
carries ctx for store operations in Cmd closures. CLI commands
create a root ctx and thread it through openStore, seed
functions, and init methods.

Only two intentional context.Background() remain in non-root
positions: engine constructor default (overridden by Start)
and drainWrites (parent already cancelled at shutdown).
This commit was merged in pull request #155.
This commit is contained in:
2026-06-27 19:43:44 -04:00
parent 8f16a09da1
commit a5fd3aec90
14 changed files with 108 additions and 89 deletions
+10 -7
View File
@@ -13,9 +13,9 @@ import (
tea "github.com/charmbracelet/bubbletea"
)
func loadCollapsed(s store.Store) map[int]bool {
func loadCollapsed(ctx context.Context, s store.Store) map[int]bool {
m := make(map[int]bool)
raw, err := s.GetPreference(context.Background(), "collapsed_groups")
raw, err := s.GetPreference(ctx, "collapsed_groups")
if err != nil || raw == "" {
return m
}
@@ -163,9 +163,9 @@ func (m *Model) loadTabDataCmd() tea.Cmd {
m.tabSeq++
seq := m.tabSeq
st := m.store
ctx := m.ctx
isAdmin := m.isAdmin
return func() tea.Msg {
ctx := context.Background()
alerts, err := st.GetAllAlerts(ctx)
if err != nil {
return tabDataMsg{seq: seq, err: err}
@@ -192,6 +192,7 @@ func (m *Model) loadTabDataCmd() tea.Cmd {
// goroutine. View renders the cached result rather than querying the DB.
func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
eng := m.engine
ctx := m.ctx
var currentStatus models.Status
for _, s := range m.sites {
if s.ID == siteID {
@@ -200,9 +201,9 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
}
}
return func() tea.Msg {
changes := eng.GetStateChanges(siteID, 5)
changes := eng.GetStateChanges(ctx, siteID, 5)
now := time.Now()
allChanges := eng.GetStateChangesSince(siteID, now.Add(-stateHistoryLookback))
allChanges := eng.GetStateChangesSince(ctx, siteID, now.Add(-stateHistoryLookback))
daily := monitor.ComputeDailyBreakdown(allChanges, currentStatus, stateHistoryDays, now)
return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily}
}
@@ -212,8 +213,9 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
// the UI goroutine.
func (m *Model) loadHistoryCmd(siteID int) tea.Cmd {
eng := m.engine
ctx := m.ctx
return func() tea.Msg {
return historyDataMsg{siteID: siteID, changes: eng.GetStateChanges(siteID, stateHistoryLimit)}
return historyDataMsg{siteID: siteID, changes: eng.GetStateChanges(ctx, siteID, stateHistoryLimit)}
}
}
@@ -222,12 +224,13 @@ func (m *Model) loadHistoryCmd(siteID int) tea.Cmd {
// can be recognized and dropped.
func (m *Model) loadSLACmd(siteID, periodIdx int) tea.Cmd {
eng := m.engine
ctx := m.ctx
since := time.Now().Add(-slaPeriods[periodIdx].duration)
return func() tea.Msg {
return slaDataMsg{
siteID: siteID,
periodIdx: periodIdx,
changes: eng.GetStateChangesSince(siteID, since),
changes: eng.GetStateChangesSince(ctx, siteID, since),
}
}
}
+3 -3
View File
@@ -1,7 +1,6 @@
package tui
import (
"context"
"fmt"
neturl "net/url"
"sort"
@@ -537,15 +536,16 @@ func (m *Model) submitAlertForm() tea.Cmd {
}
st := m.store
ctx := m.ctx
id := m.editID
name, aType := d.Name, d.AlertType
m.state = stateDashboard
if id > 0 {
return writeCmd("Update alert", func() error {
return st.UpdateAlert(context.Background(), id, name, aType, settings)
return st.UpdateAlert(ctx, id, name, aType, settings)
})
}
return writeCmd("Add alert", func() error {
return st.AddAlert(context.Background(), name, aType, settings)
return st.AddAlert(ctx, name, aType, settings)
})
}
+1 -2
View File
@@ -1,7 +1,6 @@
package tui
import (
"context"
"fmt"
"strconv"
"time"
@@ -254,9 +253,9 @@ func (m *Model) submitMaintForm() tea.Cmd {
}
st := m.store
ctx := m.ctx
m.state = stateDashboard
return writeCmd("Add maintenance window", func() error {
ctx := context.Background()
overlaps, _ := st.GetOverlappingMaintenanceWindows(ctx, mw.MonitorID, mw.StartTime, mw.EndTime)
if len(overlaps) > 0 {
_ = st.SaveLog(ctx, fmt.Sprintf("Overlap: new window %q overlaps with existing %q", mw.Title, overlaps[0].Title))
+3 -3
View File
@@ -1,7 +1,6 @@
package tui
import (
"context"
"fmt"
"net/url"
"strconv"
@@ -604,10 +603,11 @@ func (m *Model) submitSiteForm() tea.Cmd {
}
st := m.store
ctx := m.ctx
m.state = stateDashboard
if m.editID > 0 {
m.engine.UpdateSiteConfig(cfg)
return writeCmd("Update site", func() error { return st.UpdateSite(context.Background(), cfg) })
return writeCmd("Update site", func() error { return st.UpdateSite(ctx, cfg) })
}
return writeCmd("Add site", func() error { return st.AddSite(context.Background(), cfg) })
return writeCmd("Add site", func() error { return st.AddSite(ctx, cfg) })
}
+3 -3
View File
@@ -1,7 +1,6 @@
package tui
import (
"context"
"fmt"
tea "github.com/charmbracelet/bubbletea"
@@ -114,15 +113,16 @@ func (m *Model) initUserHuhForm() tea.Cmd {
func (m *Model) submitUserForm() tea.Cmd {
d := m.userFormData
st := m.store
ctx := m.ctx
id := m.editID
username, key, role := d.Username, d.PublicKey, d.Role
m.state = stateDashboard
if id > 0 {
return writeCmd("Update user", func() error {
return st.UpdateUser(context.Background(), id, username, key, role)
return st.UpdateUser(ctx, id, username, key, role)
})
}
return writeCmd("Add user", func() error {
return st.AddUser(context.Background(), username, key, role)
return st.AddUser(ctx, username, key, role)
})
}
+6 -4
View File
@@ -175,6 +175,7 @@ type Model struct {
deleteName string
deleteTab int
ctx context.Context
collapsed map[int]bool
store store.Store
engine *monitor.Engine
@@ -213,14 +214,14 @@ type Model struct {
version string
}
func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine, version string) Model {
func InitialModel(ctx context.Context, isAdmin bool, s store.Store, eng *monitor.Engine, version string) Model {
vpLogs := viewport.New(100, 20)
vpLogs.SetContent("Waiting for logs...")
z := zone.New()
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4)
collapsed := loadCollapsed(s)
collapsed := loadCollapsed(ctx, s)
themeName, _ := s.GetPreference(context.Background(), "theme")
themeName, _ := s.GetPreference(ctx, "theme")
theme := themeByName(themeName)
themeIdx := 0
for i, t := range themes {
@@ -230,9 +231,10 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine, version stri
}
}
detailPref, _ := s.GetPreference(context.Background(), "detail_open")
detailPref, _ := s.GetPreference(ctx, "detail_open")
return Model{
ctx: ctx,
state: stateDashboard,
logViewport: vpLogs,
maxTableRows: 5,
+19 -12
View File
@@ -1,7 +1,6 @@
package tui
import (
"context"
"fmt"
"time"
@@ -76,23 +75,24 @@ func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) {
// writeDoneMsg reload converges the UI back to the DB state (and the
// engine poll loop re-adds a site that is still in the DB).
st := m.store
ctx := m.ctx
id := m.deleteID
var cmd tea.Cmd
switch m.deleteTab {
case tabMonitors:
cmd = writeCmd("Delete site", func() error { return st.DeleteSite(context.Background(), id) })
cmd = writeCmd("Delete site", func() error { return st.DeleteSite(ctx, id) })
m.engine.RemoveSite(id)
m.adjustCursor(len(m.sites) - 1)
case tabMaint:
cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(context.Background(), id) })
cmd = writeCmd("Delete maintenance window", func() error { return st.DeleteMaintenanceWindow(ctx, id) })
m.adjustCursor(len(m.maintenanceWindows) - 1)
case tabSettings:
switch m.settingsSection {
case sectionAlerts:
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(context.Background(), id) })
cmd = writeCmd("Delete alert", func() error { return st.DeleteAlert(ctx, id) })
m.adjustCursor(len(m.alerts) - 1)
case sectionUsers:
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(context.Background(), id) })
cmd = writeCmd("Delete user", func() error { return st.DeleteUser(ctx, id) })
m.adjustCursor(len(m.users) - 1)
}
}
@@ -232,8 +232,9 @@ func (m *Model) handleTabData(msg tabDataMsg) (tea.Model, tea.Cmd) {
// surfaces through the engine log (picked up by the next refreshLive).
func (m *Model) testAlertCmd(id int, name string) tea.Cmd {
eng := m.engine
ctx := m.ctx
return func() tea.Msg {
if err := eng.TestAlert(id); err != nil {
if err := eng.TestAlert(ctx, id); err != nil {
eng.AddLog(fmt.Sprintf("Test alert failed (%s): %v", name, err))
}
return nil
@@ -635,9 +636,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.collapsed[gid] = !m.collapsed[gid]
payload := collapsedJSON(m.collapsed)
st := m.store
ctx := m.ctx
m.refreshLive()
return m, writeCmd("Save collapsed groups", func() error {
return st.SetPreference(context.Background(), "collapsed_groups", payload)
return st.SetPreference(ctx, "collapsed_groups", payload)
})
}
case "p":
@@ -645,9 +647,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
id := m.sites[m.cursor].ID
paused := m.engine.ToggleSitePause(id)
st := m.store
ctx := m.ctx
m.refreshLive()
return m, writeCmd("Update pause state", func() error {
return st.UpdateSitePaused(context.Background(), id, paused)
return st.UpdateSitePaused(ctx, id, paused)
})
}
case "i":
@@ -655,6 +658,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.detailOpen = !m.detailOpen
m.recalcLayout()
st := m.store
ctx := m.ctx
open := m.detailOpen
var cmd tea.Cmd
if m.detailOpen {
@@ -665,7 +669,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if open {
v = "true"
}
return st.SetPreference(context.Background(), "detail_open", v)
return st.SetPreference(ctx, "detail_open", v)
})
if cmd != nil {
return m, tea.Batch(cmd, saveCmd)
@@ -682,8 +686,9 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.detailOpen = false
m.recalcLayout()
st := m.store
ctx := m.ctx
return m, writeCmd("Save detail preference", func() error {
return st.SetPreference(context.Background(), "detail_open", "false")
return st.SetPreference(ctx, "detail_open", "false")
})
}
}
@@ -712,10 +717,11 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
if isActive {
st := m.store
ctx := m.ctx
id := mw.ID
m.refreshLive()
return m, writeCmd("End maintenance", func() error {
return st.EndMaintenanceWindow(context.Background(), id)
return st.EndMaintenanceWindow(ctx, id)
})
}
}
@@ -724,9 +730,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.theme = themes[m.themeIndex]
m.st = newStyles(m.theme)
st := m.store
ctx := m.ctx
name := m.theme.Name
return m, writeCmd("Save theme", func() error {
return st.SetPreference(context.Background(), "theme", name)
return st.SetPreference(ctx, "theme", name)
})
case "d":
return m.handleDeleteItem()