diff --git a/internal/monitor/sla.go b/internal/monitor/sla.go index cfa815a..7d0b1fb 100644 --- a/internal/monitor/sla.go +++ b/internal/monitor/sla.go @@ -131,14 +131,11 @@ func ComputeDailyBreakdown(changes []models.StateChange, currentStatus models.St reports := make([]DayReport, days) for i := 0; i < days; i++ { - dayEnd := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Add(-time.Duration(i) * 24 * time.Hour) + dayStart := time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location()) + dayEnd := time.Date(now.Year(), now.Month(), now.Day()-i+1, 0, 0, 0, 0, now.Location()) if i == 0 { dayEnd = now } - dayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Add(-time.Duration(i) * 24 * time.Hour) - if i > 0 { - dayEnd = dayStart.Add(24 * time.Hour) - } windowChanges := filterChangesForWindow(changes, dayStart, dayEnd) diff --git a/internal/server/ratelimit.go b/internal/server/ratelimit.go index ca3aac7..e963b26 100644 --- a/internal/server/ratelimit.go +++ b/internal/server/ratelimit.go @@ -25,6 +25,7 @@ type RateLimiter struct { rate float64 burst float64 trusted []*net.IPNet + stop chan struct{} } func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter { @@ -33,11 +34,16 @@ func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter { rate: float64(requestsPerMinute) / 60.0, burst: float64(requestsPerMinute), trusted: trusted, + stop: make(chan struct{}), } go rl.cleanup() return rl } +func (rl *RateLimiter) Stop() { + close(rl.stop) +} + func (rl *RateLimiter) Allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() @@ -84,16 +90,22 @@ func (rl *RateLimiter) evictOldest() { } func (rl *RateLimiter) cleanup() { + ticker := time.NewTicker(5 * time.Minute) + defer ticker.Stop() for { - time.Sleep(5 * time.Minute) - rl.mu.Lock() - cutoff := time.Now().Add(-10 * time.Minute) - for ip, v := range rl.visitors { - if v.lastSeen.Before(cutoff) { - delete(rl.visitors, ip) + select { + case <-ticker.C: + rl.mu.Lock() + cutoff := time.Now().Add(-10 * time.Minute) + for ip, v := range rl.visitors { + if v.lastSeen.Before(cutoff) { + delete(rl.visitors, ip) + } } + rl.mu.Unlock() + case <-rl.stop: + return } - rl.mu.Unlock() } } diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 2edc900..c936cad 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -75,10 +75,7 @@ func fmtAlertType(t string) string { } } -func (m Model) fmtAlertConfig(alert struct { - Type string - Settings map[string]string -}) string { +func (m Model) fmtAlertConfig(alert models.AlertConfig) string { switch alert.Type { case "email": host := alert.Settings["host"] @@ -201,10 +198,7 @@ func (m Model) viewAlertsTab() string { m.fmtAlertHealth(h), m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)), fmtAlertType(a.Type), - limitStr(m.fmtAlertConfig(struct { - Type string - Settings map[string]string - }{a.Type, a.Settings}), cfgW-2), + limitStr(m.fmtAlertConfig(a), cfgW-2), m.fmtAlertLastSent(h), }) } diff --git a/internal/tui/tab_alerts_test.go b/internal/tui/tab_alerts_test.go index 71e967d..66041aa 100644 --- a/internal/tui/tab_alerts_test.go +++ b/internal/tui/tab_alerts_test.go @@ -44,10 +44,7 @@ func TestAlertDetailPanel_MasksSecretsStableOrder(t *testing.T) { func TestFmtAlertConfig_MasksSecrets(t *testing.T) { m := newTestModel(&tuiMockStore{}) - webhook := m.fmtAlertConfig(struct { - Type string - Settings map[string]string - }{"discord", map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}}) + webhook := m.fmtAlertConfig(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}}) if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") { t.Errorf("webhook URL path (the credential) rendered in table: %q", webhook) } @@ -55,10 +52,7 @@ func TestFmtAlertConfig_MasksSecrets(t *testing.T) { t.Errorf("webhook host missing from table config: %q", webhook) } - pd := m.fmtAlertConfig(struct { - Type string - Settings map[string]string - }{"pagerduty", map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}}) + pd := m.fmtAlertConfig(models.AlertConfig{Type: "pagerduty", Settings: map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}}) if strings.Contains(pd, "R0123456789ABCDEFGHIJ") { t.Errorf("pagerduty routing key rendered raw in table: %q", pd) } diff --git a/internal/tui/update.go b/internal/tui/update.go index 19b866b..633cc2b 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -110,10 +110,6 @@ func (m *Model) handleConfirmDelete(msg tea.Msg) (tea.Model, tea.Cmd) { } func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) { - if wsm, ok := msg.(tea.WindowSizeMsg); ok { - m.termWidth = wsm.Width - m.termHeight = wsm.Height - } if keyMsg, ok := msg.(tea.KeyMsg); ok { if keyMsg.String() == "ctrl+c" { return m, tea.Quit @@ -609,7 +605,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, writeCmd("Save theme", func() error { return st.SetPreference(context.Background(), "theme", name) }) - case "d", "backspace": + case "d": return m.handleDeleteItem() } return m, nil diff --git a/internal/tui/view_detail.go b/internal/tui/view_detail.go index d591575..b9810bb 100644 --- a/internal/tui/view_detail.go +++ b/internal/tui/view_detail.go @@ -2,6 +2,7 @@ package tui import ( "fmt" + "sort" "strconv" "strings" "time" @@ -163,8 +164,14 @@ func (m Model) viewDetailPanel() string { probeResults := m.engine.GetProbeResults(site.ID) if len(probeResults) > 0 { + nodeIDs := make([]string, 0, len(probeResults)) + for id := range probeResults { + nodeIDs = append(nodeIDs, id) + } + sort.Strings(nodeIDs) b.WriteString("\n" + m.st.subtleStyle.Render(" PROBE RESULTS") + "\n") - for nodeID, result := range probeResults { + for _, nodeID := range nodeIDs { + result := probeResults[nodeID] status := m.st.specialStyle.Render("UP") if !result.IsUp { status = m.st.dangerStyle.Render("DN")