refactor: propagate context.Context through call chains #155
@@ -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