refactor: propagate context.Context through call chains #155
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
+33
-30
@@ -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 != "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+10
-7
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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))
|
||||
|
||||
@@ -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) })
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user