fix: six small fixes — rate limiter leak, DST SLA, probe sort, TUI cleanup
1. Rate limiter cleanup goroutine now stoppable via Stop() channel instead of looping forever. Prevents goroutine leak in tests. 2. Dead WindowSizeMsg branch in handleFormMsg removed — top-level Update handles resize before forms see it. 3. Probe results sorted by node ID — map iteration no longer reorders rows every render. 4. fmtAlertConfig takes models.AlertConfig directly instead of an anonymous struct the caller builds inline. 5. Backspace no longer aliases delete — d is the documented key. Prevents accidental delete-confirm on habitual backspace. 6. SLA daily buckets use time.Date day arithmetic instead of Add(-i*24h) — lands on midnight correctly across DST transitions.
This commit was merged in pull request #117.
This commit is contained in:
@@ -131,14 +131,11 @@ func ComputeDailyBreakdown(changes []models.StateChange, currentStatus models.St
|
|||||||
reports := make([]DayReport, days)
|
reports := make([]DayReport, days)
|
||||||
|
|
||||||
for i := 0; i < days; i++ {
|
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 {
|
if i == 0 {
|
||||||
dayEnd = now
|
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)
|
windowChanges := filterChangesForWindow(changes, dayStart, dayEnd)
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type RateLimiter struct {
|
|||||||
rate float64
|
rate float64
|
||||||
burst float64
|
burst float64
|
||||||
trusted []*net.IPNet
|
trusted []*net.IPNet
|
||||||
|
stop chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRateLimiter(requestsPerMinute int, trusted []*net.IPNet) *RateLimiter {
|
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,
|
rate: float64(requestsPerMinute) / 60.0,
|
||||||
burst: float64(requestsPerMinute),
|
burst: float64(requestsPerMinute),
|
||||||
trusted: trusted,
|
trusted: trusted,
|
||||||
|
stop: make(chan struct{}),
|
||||||
}
|
}
|
||||||
go rl.cleanup()
|
go rl.cleanup()
|
||||||
return rl
|
return rl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rl *RateLimiter) Stop() {
|
||||||
|
close(rl.stop)
|
||||||
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) Allow(ip string) bool {
|
func (rl *RateLimiter) Allow(ip string) bool {
|
||||||
rl.mu.Lock()
|
rl.mu.Lock()
|
||||||
defer rl.mu.Unlock()
|
defer rl.mu.Unlock()
|
||||||
@@ -84,16 +90,22 @@ func (rl *RateLimiter) evictOldest() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (rl *RateLimiter) cleanup() {
|
func (rl *RateLimiter) cleanup() {
|
||||||
|
ticker := time.NewTicker(5 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
for {
|
for {
|
||||||
time.Sleep(5 * time.Minute)
|
select {
|
||||||
rl.mu.Lock()
|
case <-ticker.C:
|
||||||
cutoff := time.Now().Add(-10 * time.Minute)
|
rl.mu.Lock()
|
||||||
for ip, v := range rl.visitors {
|
cutoff := time.Now().Add(-10 * time.Minute)
|
||||||
if v.lastSeen.Before(cutoff) {
|
for ip, v := range rl.visitors {
|
||||||
delete(rl.visitors, ip)
|
if v.lastSeen.Before(cutoff) {
|
||||||
|
delete(rl.visitors, ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
rl.mu.Unlock()
|
||||||
|
case <-rl.stop:
|
||||||
|
return
|
||||||
}
|
}
|
||||||
rl.mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,10 +75,7 @@ func fmtAlertType(t string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) fmtAlertConfig(alert struct {
|
func (m Model) fmtAlertConfig(alert models.AlertConfig) string {
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}) string {
|
|
||||||
switch alert.Type {
|
switch alert.Type {
|
||||||
case "email":
|
case "email":
|
||||||
host := alert.Settings["host"]
|
host := alert.Settings["host"]
|
||||||
@@ -201,10 +198,7 @@ func (m Model) viewAlertsTab() string {
|
|||||||
m.fmtAlertHealth(h),
|
m.fmtAlertHealth(h),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
||||||
fmtAlertType(a.Type),
|
fmtAlertType(a.Type),
|
||||||
limitStr(m.fmtAlertConfig(struct {
|
limitStr(m.fmtAlertConfig(a), cfgW-2),
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{a.Type, a.Settings}), cfgW-2),
|
|
||||||
m.fmtAlertLastSent(h),
|
m.fmtAlertLastSent(h),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ func TestAlertDetailPanel_MasksSecretsStableOrder(t *testing.T) {
|
|||||||
func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
func TestFmtAlertConfig_MasksSecrets(t *testing.T) {
|
||||||
m := newTestModel(&tuiMockStore{})
|
m := newTestModel(&tuiMockStore{})
|
||||||
|
|
||||||
webhook := m.fmtAlertConfig(struct {
|
webhook := m.fmtAlertConfig(models.AlertConfig{Type: "discord", Settings: map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}})
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{"discord", map[string]string{"url": "https://discord.com/api/webhooks/123456/SeCrEtToKeN"}})
|
|
||||||
if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") {
|
if strings.Contains(webhook, "SeCrEtToKeN") || strings.Contains(webhook, "123456") {
|
||||||
t.Errorf("webhook URL path (the credential) rendered in table: %q", webhook)
|
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)
|
t.Errorf("webhook host missing from table config: %q", webhook)
|
||||||
}
|
}
|
||||||
|
|
||||||
pd := m.fmtAlertConfig(struct {
|
pd := m.fmtAlertConfig(models.AlertConfig{Type: "pagerduty", Settings: map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}})
|
||||||
Type string
|
|
||||||
Settings map[string]string
|
|
||||||
}{"pagerduty", map[string]string{"routing_key": "R0123456789ABCDEFGHIJ"}})
|
|
||||||
if strings.Contains(pd, "R0123456789ABCDEFGHIJ") {
|
if strings.Contains(pd, "R0123456789ABCDEFGHIJ") {
|
||||||
t.Errorf("pagerduty routing key rendered raw in table: %q", pd)
|
t.Errorf("pagerduty routing key rendered raw in table: %q", pd)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
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, ok := msg.(tea.KeyMsg); ok {
|
||||||
if keyMsg.String() == "ctrl+c" {
|
if keyMsg.String() == "ctrl+c" {
|
||||||
return m, tea.Quit
|
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 m, writeCmd("Save theme", func() error {
|
||||||
return st.SetPreference(context.Background(), "theme", name)
|
return st.SetPreference(context.Background(), "theme", name)
|
||||||
})
|
})
|
||||||
case "d", "backspace":
|
case "d":
|
||||||
return m.handleDeleteItem()
|
return m.handleDeleteItem()
|
||||||
}
|
}
|
||||||
return m, nil
|
return m, nil
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -163,8 +164,14 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
probeResults := m.engine.GetProbeResults(site.ID)
|
probeResults := m.engine.GetProbeResults(site.ID)
|
||||||
if len(probeResults) > 0 {
|
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")
|
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")
|
status := m.st.specialStyle.Render("UP")
|
||||||
if !result.IsUp {
|
if !result.IsUp {
|
||||||
status = m.st.dangerStyle.Render("DN")
|
status = m.st.dangerStyle.Render("DN")
|
||||||
|
|||||||
Reference in New Issue
Block a user