diff --git a/cmd/uptop/keycache_test.go b/cmd/uptop/keycache_test.go index 09af153..6a7ca39 100644 --- a/cmd/uptop/keycache_test.go +++ b/cmd/uptop/keycache_test.go @@ -43,10 +43,10 @@ func TestKeyCache_AllowsKnownDeniesUnknown(t *testing.T) { _, unknown := testKey(t) kc := newKeyCache(&kcMockStore{users: []models.User{{PublicKey: authorized}}}) - if !kc.IsAllowed(known) { + if !kc.IsAllowed(context.Background(), known) { t.Error("known key denied") } - if kc.IsAllowed(unknown) { + if kc.IsAllowed(context.Background(), unknown) { t.Error("unknown key allowed") } } @@ -56,7 +56,7 @@ func TestKeyCache_RetainsKeysOnRefreshError(t *testing.T) { ms := &kcMockStore{users: []models.User{{PublicKey: authorized}}} kc := newKeyCache(ms) - if !kc.IsAllowed(known) { + if !kc.IsAllowed(context.Background(), known) { t.Fatal("known key denied on first refresh") } @@ -67,7 +67,7 @@ func TestKeyCache_RetainsKeysOnRefreshError(t *testing.T) { kc.updated = time.Now().Add(-time.Hour) kc.mu.Unlock() - if !kc.IsAllowed(known) { + if !kc.IsAllowed(context.Background(), known) { t.Error("transient refresh error locked out a previously valid key") } } @@ -77,7 +77,7 @@ func TestKeyCache_FailsClosedAfterInvalidate(t *testing.T) { ms := &kcMockStore{users: []models.User{{PublicKey: authorized}}} kc := newKeyCache(ms) - if !kc.IsAllowed(known) { + if !kc.IsAllowed(context.Background(), known) { t.Fatal("known key denied on first refresh") } @@ -86,7 +86,7 @@ func TestKeyCache_FailsClosedAfterInvalidate(t *testing.T) { ms.err = errors.New("db down") kc.Invalidate() - if kc.IsAllowed(known) { + if kc.IsAllowed(context.Background(), known) { t.Error("revoked key still allowed while DB is down โ€” fails open") } } @@ -97,7 +97,7 @@ func TestUserInvalidatingStore_DeleteDropsKeyCache(t *testing.T) { kc := newKeyCache(ms) s := &userInvalidatingStore{Store: ms, kc: kc} - if !kc.IsAllowed(known) { + if !kc.IsAllowed(context.Background(), known) { t.Fatal("known key denied on first refresh") } @@ -109,7 +109,7 @@ func TestUserInvalidatingStore_DeleteDropsKeyCache(t *testing.T) { ms.users = nil ms.err = errors.New("db down") - if kc.IsAllowed(known) { + if kc.IsAllowed(context.Background(), known) { t.Error("deleted user's key still allowed from stale cache") } } diff --git a/cmd/uptop/main.go b/cmd/uptop/main.go index ffc0835..f330120 100644 --- a/cmd/uptop/main.go +++ b/cmd/uptop/main.go @@ -179,7 +179,7 @@ func parseTrustedProxies(raw string) []*net.IPNet { return cidrs } -func openStore(dbType, dsn string) store.Store { +func openStore(ctx context.Context, dbType, dsn string) store.Store { var ss *store.SQLStore var err error if dbType == "postgres" { @@ -201,7 +201,7 @@ func openStore(dbType, dsn string) store.Store { } else { slog.Warn("no UPTOP_ENCRYPTION_KEY set, alert credentials stored unencrypted") } - if err := ss.Init(context.Background()); err != nil { + if err := ss.Init(ctx); err != nil { slog.Error("database init failed", "err", err) os.Exit(1) } @@ -223,7 +223,8 @@ func runApply(args []string) { os.Exit(1) } - s := openStore(*dbType, *dsn) + ctx := context.Background() + s := openStore(ctx, *dbType, *dsn) f, err := config.LoadFile(*filePath) if err != nil { @@ -231,7 +232,7 @@ func runApply(args []string) { os.Exit(1) } - changes, err := config.Apply(context.Background(), s, f, config.ApplyOpts{ + changes, err := config.Apply(ctx, s, f, config.ApplyOpts{ DryRun: *dryRun, Prune: *prune, }) @@ -250,9 +251,10 @@ func runExport(args []string) { dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN") _ = fs.Parse(args) // ExitOnError: parse errors exit before returning - s := openStore(*dbType, *dsn) + ctx := context.Background() + s := openStore(ctx, *dbType, *dsn) - f, err := config.Export(context.Background(), s) + f, err := config.Export(ctx, s) if err != nil { slog.Error("export failed", "err", err) os.Exit(1) @@ -281,6 +283,8 @@ func runMigrateSecrets(args []string) { os.Exit(1) } + ctx := context.Background() + var ss *store.SQLStore if *dbType == "postgres" { ss, err = store.NewPostgresStore(*dsn) @@ -291,21 +295,21 @@ func runMigrateSecrets(args []string) { slog.Error("database connection failed", "err", err) os.Exit(1) } - if err := ss.Init(context.Background()); err != nil { + if err := ss.Init(ctx); err != nil { slog.Error("database init failed", "err", err) os.Exit(1) } ss.SetEncryptor(enc) - alerts, err := ss.GetAllAlerts(context.Background()) + alerts, err := ss.GetAllAlerts(ctx) if err != nil { slog.Error("failed to load alerts", "err", err) os.Exit(1) } migrated := 0 for _, a := range alerts { - if err := ss.UpdateAlert(context.Background(), a.ID, a.Name, a.Type, a.Settings); err != nil { + if err := ss.UpdateAlert(ctx, a.ID, a.Name, a.Type, a.Settings); err != nil { slog.Error("alert migration failed", "alert", a.Name, "err", err) os.Exit(1) } @@ -392,15 +396,19 @@ func runServe(args []string) { kc := newKeyCache(ss) var s store.Store = &userInvalidatingStore{Store: ss, kc: kc} - if err := s.Init(context.Background()); err != nil { + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + if err := s.Init(ctx); err != nil { slog.Error("database init failed", "err", err) os.Exit(1) } if *demo { - seedDemoData(s) + seedDemoData(ctx, s) } - seedKeysFromEnv(s) + seedKeysFromEnv(ctx, s) if *importKuma != "" { kb, err := importer.LoadKumaFile(*importKuma) @@ -409,7 +417,7 @@ func runServe(args []string) { os.Exit(1) } backup := importer.ConvertKuma(kb) - if err := s.ImportData(context.Background(), backup); err != nil { + if err := s.ImportData(ctx, backup); err != nil { slog.Error("import failed", "err", err) os.Exit(1) } @@ -429,12 +437,9 @@ func runServe(args []string) { } eng.SetMaintRetention(cfg.MaintRetention) - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - eng.InitHistory() - eng.InitLogs() - eng.InitAlertHealth() + eng.InitHistory(ctx) + eng.InitLogs(ctx) + eng.InitAlertHealth(ctx) eng.Start(ctx) localTUI := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) @@ -450,7 +455,7 @@ func runServe(args []string) { sshSrv := startSSHServer(*port, s, eng, kc) if localTUI { - p := tea.NewProgram(tui.InitialModel(true, s, eng, version), tea.WithAltScreen(), tea.WithMouseCellMotion()) + p := tea.NewProgram(tui.InitialModel(ctx, true, s, eng, version), tea.WithAltScreen(), tea.WithMouseCellMotion()) if _, err := p.Run(); err != nil { slog.Error("TUI failed", "err", err) } @@ -484,11 +489,11 @@ func startSSHServer(port int, db store.Store, eng *monitor.Engine, kc *keyCache) wish.WithAddress(fmt.Sprintf(":%d", port)), wish.WithHostKeyPath(envOrDefault("UPTOP_SSH_HOST_KEY", ".ssh/id_ed25519")), wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { - return kc.IsAllowed(key) + return kc.IsAllowed(ctx, key) }), wish.WithMiddleware( bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) { - return tui.InitialModel(false, db, eng, version), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()} + return tui.InitialModel(s.Context(), false, db, eng, version), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()} }), ), ) @@ -504,8 +509,7 @@ func startSSHServer(port int, db store.Store, eng *monitor.Engine, kc *keyCache) return s } -func seedDemoData(s store.Store) { - ctx := context.Background() +func seedDemoData(ctx context.Context, s store.Store) { existing, _ := s.GetSites(ctx) if len(existing) > 0 { return @@ -566,8 +570,8 @@ func newKeyCache(db store.Store) *keyCache { return &keyCache{db: db, ttl: 30 * time.Second} } -func (c *keyCache) refresh() { - users, err := c.db.GetAllUsers(context.Background()) +func (c *keyCache) refresh(ctx context.Context) { + users, err := c.db.GetAllUsers(ctx) if err != nil { // Keep the previous key set: a transient DB error must not lock every // admin out. Revocation still fails closed because Invalidate clears @@ -600,13 +604,13 @@ func (c *keyCache) Invalidate() { c.mu.Unlock() } -func (c *keyCache) IsAllowed(incomingKey ssh.PublicKey) bool { +func (c *keyCache) IsAllowed(ctx context.Context, incomingKey ssh.PublicKey) bool { c.mu.RLock() stale := time.Since(c.updated) > c.ttl c.mu.RUnlock() if stale { - c.refresh() + c.refresh(ctx) } c.mu.RLock() @@ -652,8 +656,7 @@ func (s *userInvalidatingStore) ImportData(ctx context.Context, data models.Back return err } -func seedKeysFromEnv(s store.Store) { - ctx := context.Background() +func seedKeysFromEnv(ctx context.Context, s store.Store) { var keys []string if v := os.Getenv("UPTOP_ADMIN_KEY"); v != "" { diff --git a/internal/monitor/alerts.go b/internal/monitor/alerts.go index e17eaa1..5703051 100644 --- a/internal/monitor/alerts.go +++ b/internal/monitor/alerts.go @@ -19,8 +19,8 @@ type AlertHealth struct { // InitAlertHealth restores persisted alert send health so the dashboard shows real // "last sent" / health state on startup instead of resetting every channel to "never". -func (e *Engine) InitAlertHealth() { - records, err := e.db.LoadAlertHealth(context.Background()) +func (e *Engine) InitAlertHealth(ctx context.Context) { + records, err := e.db.LoadAlertHealth(ctx) if err != nil { return } @@ -198,7 +198,7 @@ func (e *Engine) triggerAlert(alertID int, title, message string) { if alertID <= 0 { return } - cfg, err := e.db.GetAlert(context.Background(), alertID) + cfg, err := e.db.GetAlert(e.ctx, alertID) if err != nil { e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err)) return @@ -206,7 +206,7 @@ func (e *Engine) triggerAlert(alertID int, title, message string) { provider := alert.GetProvider(cfg) if provider != nil { go func() { - ctx, cancel := context.WithTimeout(context.Background(), alertSendTimeout) + ctx, cancel := context.WithTimeout(e.ctx, alertSendTimeout) defer cancel() if err := provider.Send(ctx, title, message); err != nil { e.AddLog(fmt.Sprintf("Alert send failed (%s): %v", cfg.Name, err)) @@ -250,8 +250,8 @@ func (e *Engine) GetAlertHealth(alertID int) AlertHealth { return e.alertHealth[alertID] } -func (e *Engine) TestAlert(alertID int) error { - cfg, err := e.db.GetAlert(context.Background(), alertID) +func (e *Engine) TestAlert(ctx context.Context, alertID int) error { + cfg, err := e.db.GetAlert(ctx, alertID) if err != nil { return fmt.Errorf("failed to load alert: %w", err) } @@ -259,7 +259,7 @@ func (e *Engine) TestAlert(alertID int) error { if provider == nil { return fmt.Errorf("no provider for type %q", cfg.Type) } - ctx, cancel := context.WithTimeout(context.Background(), alertSendTimeout) + ctx, cancel := context.WithTimeout(ctx, alertSendTimeout) defer cancel() err = provider.Send(ctx, "๐Ÿงช Test Alert", fmt.Sprintf("Test notification from uptop for channel '%s'.", cfg.Name)) if err != nil { diff --git a/internal/monitor/history.go b/internal/monitor/history.go index 2a88154..c2d3b00 100644 --- a/internal/monitor/history.go +++ b/internal/monitor/history.go @@ -14,8 +14,8 @@ type SiteHistory struct { UpChecks int } -func (e *Engine) InitHistory() { - all, err := e.db.LoadAllHistory(context.Background(), maxHistoryLen) +func (e *Engine) InitHistory(ctx context.Context) { + all, err := e.db.LoadAllHistory(ctx, maxHistoryLen) if err != nil { e.AddLog("Failed to load check history: " + err.Error()) return diff --git a/internal/monitor/monitor.go b/internal/monitor/monitor.go index 8b43cc3..1497805 100644 --- a/internal/monitor/monitor.go +++ b/internal/monitor/monitor.go @@ -64,6 +64,7 @@ type Engine struct { dbWrites chan dbWrite writerWG sync.WaitGroup checkerWG sync.WaitGroup + ctx context.Context cancel context.CancelFunc stopOnce sync.Once } @@ -90,6 +91,7 @@ func newEngine(s store.Store, allowPrivateTargets bool) *Engine { allowPrivateTargets: allowPrivateTargets, maintRetention: defaultMaintRetention, dbWrites: make(chan dbWrite, dbWriteBuffer), + ctx: context.Background(), db: s, strictClient: &http.Client{ Transport: &http.Transport{ @@ -200,6 +202,8 @@ func (e *Engine) dbWriter(ctx context.Context) { } // drainWrites flushes everything still buffered, best-effort, at shutdown. +// Uses context.Background because the engine ctx is already cancelled when +// this runs โ€” writes still need to reach the DB. func (e *Engine) drainWrites() { for { select { @@ -238,8 +242,8 @@ func (e *Engine) Stop() { }) } -func (e *Engine) InitLogs() { - entries, err := e.db.LoadLogs(context.Background(), maxLogEntries) +func (e *Engine) InitLogs(ctx context.Context) { + entries, err := e.db.LoadLogs(ctx, maxLogEntries) if err != nil { return } @@ -264,6 +268,7 @@ func (e *Engine) Start(ctx context.Context) { // trace the cross-method call, and cancelling the parent reaps this child // regardless, so the leak it warns about can't occur. ctx, e.cancel = context.WithCancel(ctx) //nolint:gosec // cancel is called in Stop() + e.ctx = ctx e.writerWG.Add(1) go e.dbWriter(ctx) diff --git a/internal/monitor/monitor_test.go b/internal/monitor/monitor_test.go index 8d18af0..daa351c 100644 --- a/internal/monitor/monitor_test.go +++ b/internal/monitor/monitor_test.go @@ -208,7 +208,7 @@ func TestInitHistory_LoadsFromDB(t *testing.T) { {SiteID: 1, LatencyNs: 3000000, IsUp: false}, } e := newTestEngine(ms) - e.InitHistory() + e.InitHistory(context.Background()) h, ok := e.GetHistory(1) if !ok { @@ -248,7 +248,7 @@ func TestInitLogs_LoadsFromDB(t *testing.T) { {Message: "old-log-2"}, } e := newTestEngine(ms) - e.InitLogs() + e.InitLogs(context.Background()) logs := e.GetLogs() if len(logs) != 2 { diff --git a/internal/monitor/sites.go b/internal/monitor/sites.go index 555aa90..bb6ae69 100644 --- a/internal/monitor/sites.go +++ b/internal/monitor/sites.go @@ -141,16 +141,16 @@ func (e *Engine) GetDisplayStatus(site models.Site) string { return string(site.Status) } -func (e *Engine) GetStateChanges(siteID int, limit int) []models.StateChange { - changes, err := e.db.GetStateChanges(context.Background(), siteID, limit) +func (e *Engine) GetStateChanges(ctx context.Context, siteID int, limit int) []models.StateChange { + changes, err := e.db.GetStateChanges(ctx, siteID, limit) if err != nil { return nil } return changes } -func (e *Engine) GetStateChangesSince(siteID int, since time.Time) []models.StateChange { - changes, err := e.db.GetStateChangesSince(context.Background(), siteID, since) +func (e *Engine) GetStateChangesSince(ctx context.Context, siteID int, since time.Time) []models.StateChange { + changes, err := e.db.GetStateChangesSince(ctx, siteID, since) if err != nil { return nil } diff --git a/internal/tui/data.go b/internal/tui/data.go index 5731134..ab0bb28 100644 --- a/internal/tui/data.go +++ b/internal/tui/data.go @@ -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), } } } diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 1926eb1..d67eb4b 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -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) }) } diff --git a/internal/tui/tab_maint.go b/internal/tui/tab_maint.go index dcedb9e..5c3a097 100644 --- a/internal/tui/tab_maint.go +++ b/internal/tui/tab_maint.go @@ -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)) diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index 66890e3..8a65d29 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -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) }) } diff --git a/internal/tui/tab_users.go b/internal/tui/tab_users.go index 2bd86fa..5599948 100644 --- a/internal/tui/tab_users.go +++ b/internal/tui/tab_users.go @@ -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) }) } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 45c12d4..81b1d48 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -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, diff --git a/internal/tui/update.go b/internal/tui/update.go index 0b93c16..be85437 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -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()