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