refactor: propagate context.Context through call chains #155

Merged
lerko merged 1 commits from refactor/context-propagation into main 2026-06-27 23:56:11 +00:00
14 changed files with 108 additions and 89 deletions
+8 -8
View File
@@ -43,10 +43,10 @@ func TestKeyCache_AllowsKnownDeniesUnknown(t *testing.T) {
_, unknown := testKey(t) _, unknown := testKey(t)
kc := newKeyCache(&kcMockStore{users: []models.User{{PublicKey: authorized}}}) kc := newKeyCache(&kcMockStore{users: []models.User{{PublicKey: authorized}}})
if !kc.IsAllowed(known) { if !kc.IsAllowed(context.Background(), known) {
t.Error("known key denied") t.Error("known key denied")
} }
if kc.IsAllowed(unknown) { if kc.IsAllowed(context.Background(), unknown) {
t.Error("unknown key allowed") t.Error("unknown key allowed")
} }
} }
@@ -56,7 +56,7 @@ func TestKeyCache_RetainsKeysOnRefreshError(t *testing.T) {
ms := &kcMockStore{users: []models.User{{PublicKey: authorized}}} ms := &kcMockStore{users: []models.User{{PublicKey: authorized}}}
kc := newKeyCache(ms) kc := newKeyCache(ms)
if !kc.IsAllowed(known) { if !kc.IsAllowed(context.Background(), known) {
t.Fatal("known key denied on first refresh") 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.updated = time.Now().Add(-time.Hour)
kc.mu.Unlock() kc.mu.Unlock()
if !kc.IsAllowed(known) { if !kc.IsAllowed(context.Background(), known) {
t.Error("transient refresh error locked out a previously valid key") 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}}} ms := &kcMockStore{users: []models.User{{PublicKey: authorized}}}
kc := newKeyCache(ms) kc := newKeyCache(ms)
if !kc.IsAllowed(known) { if !kc.IsAllowed(context.Background(), known) {
t.Fatal("known key denied on first refresh") t.Fatal("known key denied on first refresh")
} }
@@ -86,7 +86,7 @@ func TestKeyCache_FailsClosedAfterInvalidate(t *testing.T) {
ms.err = errors.New("db down") ms.err = errors.New("db down")
kc.Invalidate() kc.Invalidate()
if kc.IsAllowed(known) { if kc.IsAllowed(context.Background(), known) {
t.Error("revoked key still allowed while DB is down — fails open") 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) kc := newKeyCache(ms)
s := &userInvalidatingStore{Store: ms, kc: kc} s := &userInvalidatingStore{Store: ms, kc: kc}
if !kc.IsAllowed(known) { if !kc.IsAllowed(context.Background(), known) {
t.Fatal("known key denied on first refresh") t.Fatal("known key denied on first refresh")
} }
@@ -109,7 +109,7 @@ func TestUserInvalidatingStore_DeleteDropsKeyCache(t *testing.T) {
ms.users = nil ms.users = nil
ms.err = errors.New("db down") 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") t.Error("deleted user's key still allowed from stale cache")
} }
} }
+33 -30
View File
@@ -179,7 +179,7 @@ func parseTrustedProxies(raw string) []*net.IPNet {
return cidrs return cidrs
} }
func openStore(dbType, dsn string) store.Store { func openStore(ctx context.Context, dbType, dsn string) store.Store {
var ss *store.SQLStore var ss *store.SQLStore
var err error var err error
if dbType == "postgres" { if dbType == "postgres" {
@@ -201,7 +201,7 @@ func openStore(dbType, dsn string) store.Store {
} else { } else {
slog.Warn("no UPTOP_ENCRYPTION_KEY set, alert credentials stored unencrypted") 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) slog.Error("database init failed", "err", err)
os.Exit(1) os.Exit(1)
} }
@@ -223,7 +223,8 @@ func runApply(args []string) {
os.Exit(1) os.Exit(1)
} }
s := openStore(*dbType, *dsn) ctx := context.Background()
s := openStore(ctx, *dbType, *dsn)
f, err := config.LoadFile(*filePath) f, err := config.LoadFile(*filePath)
if err != nil { if err != nil {
@@ -231,7 +232,7 @@ func runApply(args []string) {
os.Exit(1) os.Exit(1)
} }
changes, err := config.Apply(context.Background(), s, f, config.ApplyOpts{ changes, err := config.Apply(ctx, s, f, config.ApplyOpts{
DryRun: *dryRun, DryRun: *dryRun,
Prune: *prune, Prune: *prune,
}) })
@@ -250,9 +251,10 @@ func runExport(args []string) {
dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN") dsn := fs.String("dsn", envOrDefault("UPTOP_DB_DSN", "uptop.db"), "Database DSN")
_ = fs.Parse(args) // ExitOnError: parse errors exit before returning _ = 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 { if err != nil {
slog.Error("export failed", "err", err) slog.Error("export failed", "err", err)
os.Exit(1) os.Exit(1)
@@ -281,6 +283,8 @@ func runMigrateSecrets(args []string) {
os.Exit(1) os.Exit(1)
} }
ctx := context.Background()
var ss *store.SQLStore var ss *store.SQLStore
if *dbType == "postgres" { if *dbType == "postgres" {
ss, err = store.NewPostgresStore(*dsn) ss, err = store.NewPostgresStore(*dsn)
@@ -291,21 +295,21 @@ func runMigrateSecrets(args []string) {
slog.Error("database connection failed", "err", err) slog.Error("database connection failed", "err", err)
os.Exit(1) 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) slog.Error("database init failed", "err", err)
os.Exit(1) os.Exit(1)
} }
ss.SetEncryptor(enc) ss.SetEncryptor(enc)
alerts, err := ss.GetAllAlerts(context.Background()) alerts, err := ss.GetAllAlerts(ctx)
if err != nil { if err != nil {
slog.Error("failed to load alerts", "err", err) slog.Error("failed to load alerts", "err", err)
os.Exit(1) os.Exit(1)
} }
migrated := 0 migrated := 0
for _, a := range alerts { 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) slog.Error("alert migration failed", "alert", a.Name, "err", err)
os.Exit(1) os.Exit(1)
} }
@@ -392,15 +396,19 @@ func runServe(args []string) {
kc := newKeyCache(ss) kc := newKeyCache(ss)
var s store.Store = &userInvalidatingStore{Store: ss, kc: kc} 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) slog.Error("database init failed", "err", err)
os.Exit(1) os.Exit(1)
} }
if *demo { if *demo {
seedDemoData(s) seedDemoData(ctx, s)
} }
seedKeysFromEnv(s) seedKeysFromEnv(ctx, s)
if *importKuma != "" { if *importKuma != "" {
kb, err := importer.LoadKumaFile(*importKuma) kb, err := importer.LoadKumaFile(*importKuma)
@@ -409,7 +417,7 @@ func runServe(args []string) {
os.Exit(1) os.Exit(1)
} }
backup := importer.ConvertKuma(kb) 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) slog.Error("import failed", "err", err)
os.Exit(1) os.Exit(1)
} }
@@ -429,12 +437,9 @@ func runServe(args []string) {
} }
eng.SetMaintRetention(cfg.MaintRetention) eng.SetMaintRetention(cfg.MaintRetention)
ctx, cancel := context.WithCancel(context.Background()) eng.InitHistory(ctx)
defer cancel() eng.InitLogs(ctx)
eng.InitAlertHealth(ctx)
eng.InitHistory()
eng.InitLogs()
eng.InitAlertHealth()
eng.Start(ctx) eng.Start(ctx)
localTUI := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) 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) sshSrv := startSSHServer(*port, s, eng, kc)
if localTUI { 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 { if _, err := p.Run(); err != nil {
slog.Error("TUI failed", "err", err) 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.WithAddress(fmt.Sprintf(":%d", port)),
wish.WithHostKeyPath(envOrDefault("UPTOP_SSH_HOST_KEY", ".ssh/id_ed25519")), wish.WithHostKeyPath(envOrDefault("UPTOP_SSH_HOST_KEY", ".ssh/id_ed25519")),
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool { wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
return kc.IsAllowed(key) return kc.IsAllowed(ctx, key)
}), }),
wish.WithMiddleware( wish.WithMiddleware(
bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) { 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 return s
} }
func seedDemoData(s store.Store) { func seedDemoData(ctx context.Context, s store.Store) {
ctx := context.Background()
existing, _ := s.GetSites(ctx) existing, _ := s.GetSites(ctx)
if len(existing) > 0 { if len(existing) > 0 {
return return
@@ -566,8 +570,8 @@ func newKeyCache(db store.Store) *keyCache {
return &keyCache{db: db, ttl: 30 * time.Second} return &keyCache{db: db, ttl: 30 * time.Second}
} }
func (c *keyCache) refresh() { func (c *keyCache) refresh(ctx context.Context) {
users, err := c.db.GetAllUsers(context.Background()) users, err := c.db.GetAllUsers(ctx)
if err != nil { if err != nil {
// Keep the previous key set: a transient DB error must not lock every // Keep the previous key set: a transient DB error must not lock every
// admin out. Revocation still fails closed because Invalidate clears // admin out. Revocation still fails closed because Invalidate clears
@@ -600,13 +604,13 @@ func (c *keyCache) Invalidate() {
c.mu.Unlock() 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() c.mu.RLock()
stale := time.Since(c.updated) > c.ttl stale := time.Since(c.updated) > c.ttl
c.mu.RUnlock() c.mu.RUnlock()
if stale { if stale {
c.refresh() c.refresh(ctx)
} }
c.mu.RLock() c.mu.RLock()
@@ -652,8 +656,7 @@ func (s *userInvalidatingStore) ImportData(ctx context.Context, data models.Back
return err return err
} }
func seedKeysFromEnv(s store.Store) { func seedKeysFromEnv(ctx context.Context, s store.Store) {
ctx := context.Background()
var keys []string var keys []string
if v := os.Getenv("UPTOP_ADMIN_KEY"); v != "" { if v := os.Getenv("UPTOP_ADMIN_KEY"); v != "" {
+7 -7
View File
@@ -19,8 +19,8 @@ type AlertHealth struct {
// InitAlertHealth restores persisted alert send health so the dashboard shows real // InitAlertHealth restores persisted alert send health so the dashboard shows real
// "last sent" / health state on startup instead of resetting every channel to "never". // "last sent" / health state on startup instead of resetting every channel to "never".
func (e *Engine) InitAlertHealth() { func (e *Engine) InitAlertHealth(ctx context.Context) {
records, err := e.db.LoadAlertHealth(context.Background()) records, err := e.db.LoadAlertHealth(ctx)
if err != nil { if err != nil {
return return
} }
@@ -198,7 +198,7 @@ func (e *Engine) triggerAlert(alertID int, title, message string) {
if alertID <= 0 { if alertID <= 0 {
return return
} }
cfg, err := e.db.GetAlert(context.Background(), alertID) cfg, err := e.db.GetAlert(e.ctx, alertID)
if err != nil { if err != nil {
e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err)) e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err))
return return
@@ -206,7 +206,7 @@ func (e *Engine) triggerAlert(alertID int, title, message string) {
provider := alert.GetProvider(cfg) provider := alert.GetProvider(cfg)
if provider != nil { if provider != nil {
go func() { go func() {
ctx, cancel := context.WithTimeout(context.Background(), alertSendTimeout) ctx, cancel := context.WithTimeout(e.ctx, alertSendTimeout)
defer cancel() defer cancel()
if err := provider.Send(ctx, title, message); err != nil { if err := provider.Send(ctx, title, message); err != nil {
e.AddLog(fmt.Sprintf("Alert send failed (%s): %v", cfg.Name, err)) 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] return e.alertHealth[alertID]
} }
func (e *Engine) TestAlert(alertID int) error { func (e *Engine) TestAlert(ctx context.Context, alertID int) error {
cfg, err := e.db.GetAlert(context.Background(), alertID) cfg, err := e.db.GetAlert(ctx, alertID)
if err != nil { if err != nil {
return fmt.Errorf("failed to load alert: %w", err) return fmt.Errorf("failed to load alert: %w", err)
} }
@@ -259,7 +259,7 @@ func (e *Engine) TestAlert(alertID int) error {
if provider == nil { if provider == nil {
return fmt.Errorf("no provider for type %q", cfg.Type) 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() defer cancel()
err = provider.Send(ctx, "🧪 Test Alert", fmt.Sprintf("Test notification from uptop for channel '%s'.", cfg.Name)) err = provider.Send(ctx, "🧪 Test Alert", fmt.Sprintf("Test notification from uptop for channel '%s'.", cfg.Name))
if err != nil { if err != nil {
+2 -2
View File
@@ -14,8 +14,8 @@ type SiteHistory struct {
UpChecks int UpChecks int
} }
func (e *Engine) InitHistory() { func (e *Engine) InitHistory(ctx context.Context) {
all, err := e.db.LoadAllHistory(context.Background(), maxHistoryLen) all, err := e.db.LoadAllHistory(ctx, maxHistoryLen)
if err != nil { if err != nil {
e.AddLog("Failed to load check history: " + err.Error()) e.AddLog("Failed to load check history: " + err.Error())
return return
+7 -2
View File
@@ -64,6 +64,7 @@ type Engine struct {
dbWrites chan dbWrite dbWrites chan dbWrite
writerWG sync.WaitGroup writerWG sync.WaitGroup
checkerWG sync.WaitGroup checkerWG sync.WaitGroup
ctx context.Context
cancel context.CancelFunc cancel context.CancelFunc
stopOnce sync.Once stopOnce sync.Once
} }
@@ -90,6 +91,7 @@ func newEngine(s store.Store, allowPrivateTargets bool) *Engine {
allowPrivateTargets: allowPrivateTargets, allowPrivateTargets: allowPrivateTargets,
maintRetention: defaultMaintRetention, maintRetention: defaultMaintRetention,
dbWrites: make(chan dbWrite, dbWriteBuffer), dbWrites: make(chan dbWrite, dbWriteBuffer),
ctx: context.Background(),
db: s, db: s,
strictClient: &http.Client{ strictClient: &http.Client{
Transport: &http.Transport{ Transport: &http.Transport{
@@ -200,6 +202,8 @@ func (e *Engine) dbWriter(ctx context.Context) {
} }
// drainWrites flushes everything still buffered, best-effort, at shutdown. // 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() { func (e *Engine) drainWrites() {
for { for {
select { select {
@@ -238,8 +242,8 @@ func (e *Engine) Stop() {
}) })
} }
func (e *Engine) InitLogs() { func (e *Engine) InitLogs(ctx context.Context) {
entries, err := e.db.LoadLogs(context.Background(), maxLogEntries) entries, err := e.db.LoadLogs(ctx, maxLogEntries)
if err != nil { if err != nil {
return return
} }
@@ -264,6 +268,7 @@ func (e *Engine) Start(ctx context.Context) {
// trace the cross-method call, and cancelling the parent reaps this child // trace the cross-method call, and cancelling the parent reaps this child
// regardless, so the leak it warns about can't occur. // regardless, so the leak it warns about can't occur.
ctx, e.cancel = context.WithCancel(ctx) //nolint:gosec // cancel is called in Stop() ctx, e.cancel = context.WithCancel(ctx) //nolint:gosec // cancel is called in Stop()
e.ctx = ctx
e.writerWG.Add(1) e.writerWG.Add(1)
go e.dbWriter(ctx) go e.dbWriter(ctx)
+2 -2
View File
@@ -208,7 +208,7 @@ func TestInitHistory_LoadsFromDB(t *testing.T) {
{SiteID: 1, LatencyNs: 3000000, IsUp: false}, {SiteID: 1, LatencyNs: 3000000, IsUp: false},
} }
e := newTestEngine(ms) e := newTestEngine(ms)
e.InitHistory() e.InitHistory(context.Background())
h, ok := e.GetHistory(1) h, ok := e.GetHistory(1)
if !ok { if !ok {
@@ -248,7 +248,7 @@ func TestInitLogs_LoadsFromDB(t *testing.T) {
{Message: "old-log-2"}, {Message: "old-log-2"},
} }
e := newTestEngine(ms) e := newTestEngine(ms)
e.InitLogs() e.InitLogs(context.Background())
logs := e.GetLogs() logs := e.GetLogs()
if len(logs) != 2 { if len(logs) != 2 {
+4 -4
View File
@@ -141,16 +141,16 @@ func (e *Engine) GetDisplayStatus(site models.Site) string {
return string(site.Status) return string(site.Status)
} }
func (e *Engine) GetStateChanges(siteID int, limit int) []models.StateChange { func (e *Engine) GetStateChanges(ctx context.Context, siteID int, limit int) []models.StateChange {
changes, err := e.db.GetStateChanges(context.Background(), siteID, limit) changes, err := e.db.GetStateChanges(ctx, siteID, limit)
if err != nil { if err != nil {
return nil return nil
} }
return changes return changes
} }
func (e *Engine) GetStateChangesSince(siteID int, since time.Time) []models.StateChange { func (e *Engine) GetStateChangesSince(ctx context.Context, siteID int, since time.Time) []models.StateChange {
changes, err := e.db.GetStateChangesSince(context.Background(), siteID, since) changes, err := e.db.GetStateChangesSince(ctx, siteID, since)
if err != nil { if err != nil {
return nil return nil
} }
+10 -7
View File
@@ -13,9 +13,9 @@ import (
tea "github.com/charmbracelet/bubbletea" 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) m := make(map[int]bool)
raw, err := s.GetPreference(context.Background(), "collapsed_groups") raw, err := s.GetPreference(ctx, "collapsed_groups")
if err != nil || raw == "" { if err != nil || raw == "" {
return m return m
} }
@@ -163,9 +163,9 @@ func (m *Model) loadTabDataCmd() tea.Cmd {
m.tabSeq++ m.tabSeq++
seq := m.tabSeq seq := m.tabSeq
st := m.store st := m.store
ctx := m.ctx
isAdmin := m.isAdmin isAdmin := m.isAdmin
return func() tea.Msg { return func() tea.Msg {
ctx := context.Background()
alerts, err := st.GetAllAlerts(ctx) alerts, err := st.GetAllAlerts(ctx)
if err != nil { if err != nil {
return tabDataMsg{seq: seq, err: err} 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. // goroutine. View renders the cached result rather than querying the DB.
func (m *Model) loadDetailCmd(siteID int) tea.Cmd { func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
eng := m.engine eng := m.engine
ctx := m.ctx
var currentStatus models.Status var currentStatus models.Status
for _, s := range m.sites { for _, s := range m.sites {
if s.ID == siteID { if s.ID == siteID {
@@ -200,9 +201,9 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
} }
} }
return func() tea.Msg { return func() tea.Msg {
changes := eng.GetStateChanges(siteID, 5) changes := eng.GetStateChanges(ctx, siteID, 5)
now := time.Now() 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) daily := monitor.ComputeDailyBreakdown(allChanges, currentStatus, stateHistoryDays, now)
return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily} return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily}
} }
@@ -212,8 +213,9 @@ func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
// the UI goroutine. // the UI goroutine.
func (m *Model) loadHistoryCmd(siteID int) tea.Cmd { func (m *Model) loadHistoryCmd(siteID int) tea.Cmd {
eng := m.engine eng := m.engine
ctx := m.ctx
return func() tea.Msg { 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. // can be recognized and dropped.
func (m *Model) loadSLACmd(siteID, periodIdx int) tea.Cmd { func (m *Model) loadSLACmd(siteID, periodIdx int) tea.Cmd {
eng := m.engine eng := m.engine
ctx := m.ctx
since := time.Now().Add(-slaPeriods[periodIdx].duration) since := time.Now().Add(-slaPeriods[periodIdx].duration)
return func() tea.Msg { return func() tea.Msg {
return slaDataMsg{ return slaDataMsg{
siteID: siteID, siteID: siteID,
periodIdx: periodIdx, periodIdx: periodIdx,
changes: eng.GetStateChangesSince(siteID, since), changes: eng.GetStateChangesSince(ctx, siteID, since),
} }
} }
} }
+3 -3
View File
@@ -1,7 +1,6 @@
package tui package tui
import ( import (
"context"
"fmt" "fmt"
neturl "net/url" neturl "net/url"
"sort" "sort"
@@ -537,15 +536,16 @@ func (m *Model) submitAlertForm() tea.Cmd {
} }
st := m.store st := m.store
ctx := m.ctx
id := m.editID id := m.editID
name, aType := d.Name, d.AlertType name, aType := d.Name, d.AlertType
m.state = stateDashboard m.state = stateDashboard
if id > 0 { if id > 0 {
return writeCmd("Update alert", func() error { 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 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 package tui
import ( import (
"context"
"fmt" "fmt"
"strconv" "strconv"
"time" "time"
@@ -254,9 +253,9 @@ func (m *Model) submitMaintForm() tea.Cmd {
} }
st := m.store st := m.store
ctx := m.ctx
m.state = stateDashboard m.state = stateDashboard
return writeCmd("Add maintenance window", func() error { return writeCmd("Add maintenance window", func() error {
ctx := context.Background()
overlaps, _ := st.GetOverlappingMaintenanceWindows(ctx, mw.MonitorID, mw.StartTime, mw.EndTime) overlaps, _ := st.GetOverlappingMaintenanceWindows(ctx, mw.MonitorID, mw.StartTime, mw.EndTime)
if len(overlaps) > 0 { if len(overlaps) > 0 {
_ = st.SaveLog(ctx, fmt.Sprintf("Overlap: new window %q overlaps with existing %q", mw.Title, overlaps[0].Title)) _ = 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 package tui
import ( import (
"context"
"fmt" "fmt"
"net/url" "net/url"
"strconv" "strconv"
@@ -604,10 +603,11 @@ func (m *Model) submitSiteForm() tea.Cmd {
} }
st := m.store st := m.store
ctx := m.ctx
m.state = stateDashboard m.state = stateDashboard
if m.editID > 0 { if m.editID > 0 {
m.engine.UpdateSiteConfig(cfg) 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 package tui
import ( import (
"context"
"fmt" "fmt"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -114,15 +113,16 @@ func (m *Model) initUserHuhForm() tea.Cmd {
func (m *Model) submitUserForm() tea.Cmd { func (m *Model) submitUserForm() tea.Cmd {
d := m.userFormData d := m.userFormData
st := m.store st := m.store
ctx := m.ctx
id := m.editID id := m.editID
username, key, role := d.Username, d.PublicKey, d.Role username, key, role := d.Username, d.PublicKey, d.Role
m.state = stateDashboard m.state = stateDashboard
if id > 0 { if id > 0 {
return writeCmd("Update user", func() error { 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 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 deleteName string
deleteTab int deleteTab int
ctx context.Context
collapsed map[int]bool collapsed map[int]bool
store store.Store store store.Store
engine *monitor.Engine engine *monitor.Engine
@@ -213,14 +214,14 @@ type Model struct {
version string 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 := viewport.New(100, 20)
vpLogs.SetContent("Waiting for logs...") vpLogs.SetContent("Waiting for logs...")
z := zone.New() z := zone.New()
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4) 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) theme := themeByName(themeName)
themeIdx := 0 themeIdx := 0
for i, t := range themes { 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{ return Model{
ctx: ctx,
state: stateDashboard, state: stateDashboard,
logViewport: vpLogs, logViewport: vpLogs,
maxTableRows: 5, maxTableRows: 5,
+19 -12
View File
@@ -1,7 +1,6 @@
package tui package tui
import ( import (
"context"
"fmt" "fmt"
"time" "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 // 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). // engine poll loop re-adds a site that is still in the DB).
st := m.store st := m.store
ctx := m.ctx
id := m.deleteID id := m.deleteID
var cmd tea.Cmd var cmd tea.Cmd
switch m.deleteTab { switch m.deleteTab {
case tabMonitors: 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.engine.RemoveSite(id)
m.adjustCursor(len(m.sites) - 1) m.adjustCursor(len(m.sites) - 1)
case tabMaint: 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) m.adjustCursor(len(m.maintenanceWindows) - 1)
case tabSettings: case tabSettings:
switch m.settingsSection { switch m.settingsSection {
case sectionAlerts: 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) m.adjustCursor(len(m.alerts) - 1)
case sectionUsers: 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) 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). // surfaces through the engine log (picked up by the next refreshLive).
func (m *Model) testAlertCmd(id int, name string) tea.Cmd { func (m *Model) testAlertCmd(id int, name string) tea.Cmd {
eng := m.engine eng := m.engine
ctx := m.ctx
return func() tea.Msg { 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)) eng.AddLog(fmt.Sprintf("Test alert failed (%s): %v", name, err))
} }
return nil return nil
@@ -635,9 +636,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.collapsed[gid] = !m.collapsed[gid] m.collapsed[gid] = !m.collapsed[gid]
payload := collapsedJSON(m.collapsed) payload := collapsedJSON(m.collapsed)
st := m.store st := m.store
ctx := m.ctx
m.refreshLive() m.refreshLive()
return m, writeCmd("Save collapsed groups", func() error { 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": case "p":
@@ -645,9 +647,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
id := m.sites[m.cursor].ID id := m.sites[m.cursor].ID
paused := m.engine.ToggleSitePause(id) paused := m.engine.ToggleSitePause(id)
st := m.store st := m.store
ctx := m.ctx
m.refreshLive() m.refreshLive()
return m, writeCmd("Update pause state", func() error { return m, writeCmd("Update pause state", func() error {
return st.UpdateSitePaused(context.Background(), id, paused) return st.UpdateSitePaused(ctx, id, paused)
}) })
} }
case "i": case "i":
@@ -655,6 +658,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.detailOpen = !m.detailOpen m.detailOpen = !m.detailOpen
m.recalcLayout() m.recalcLayout()
st := m.store st := m.store
ctx := m.ctx
open := m.detailOpen open := m.detailOpen
var cmd tea.Cmd var cmd tea.Cmd
if m.detailOpen { if m.detailOpen {
@@ -665,7 +669,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
if open { if open {
v = "true" v = "true"
} }
return st.SetPreference(context.Background(), "detail_open", v) return st.SetPreference(ctx, "detail_open", v)
}) })
if cmd != nil { if cmd != nil {
return m, tea.Batch(cmd, saveCmd) 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.detailOpen = false
m.recalcLayout() m.recalcLayout()
st := m.store st := m.store
ctx := m.ctx
return m, writeCmd("Save detail preference", func() error { 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)) isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
if isActive { if isActive {
st := m.store st := m.store
ctx := m.ctx
id := mw.ID id := mw.ID
m.refreshLive() m.refreshLive()
return m, writeCmd("End maintenance", func() error { 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.theme = themes[m.themeIndex]
m.st = newStyles(m.theme) m.st = newStyles(m.theme)
st := m.store st := m.store
ctx := m.ctx
name := m.theme.Name name := m.theme.Name
return m, writeCmd("Save theme", func() error { return m, writeCmd("Save theme", func() error {
return st.SetPreference(context.Background(), "theme", name) return st.SetPreference(ctx, "theme", name)
}) })
case "d": case "d":
return m.handleDeleteItem() return m.handleDeleteItem()