refactor(tui): status icons, clean STATUS column, relative time
CI / test (pull_request) Successful in 2m30s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 51s

- STATUS column shows icon + clean state only (▲ UP, ▼ DOWN, ◆ LATE,
  ◆ STALE, ◇ PAUSED, ◼ MAINT, ○ PENDING). Error classification
  (DNS/TLS/TMO) removed from STATUS — stays in NAME inline hint.
- Detail panel Last Check shows relative time ("12s ago") instead of
  absolute timestamp.
- Extract shared fmtTimeAgo() to format.go, consolidate duplicate
  formatters in tab_alerts.go and tab_nodes.go.
This commit was merged in pull request #62.
This commit is contained in:
2026-06-04 17:03:04 -04:00
parent 33a3ff9bcb
commit fb709b34c5
6 changed files with 44 additions and 62 deletions
+26 -13
View File
@@ -152,33 +152,46 @@ func fmtRetries(site models.Site) string {
return s return s
} }
func fmtStatus(status string, paused bool, inMaint bool, errCategory ErrorCategory) string { func fmtStatus(status string, paused bool, inMaint bool) string {
if paused { if paused {
return warnStyle.Render("PAUSED") return warnStyle.Render("PAUSED")
} }
if inMaint { if inMaint {
return maintStyle.Render("MAINT") return maintStyle.Render("MAINT")
} }
switch status { switch status {
case "DOWN": case "DOWN":
label := "DOWN" return dangerStyle.Render("▼ DOWN")
if errCategory != ErrCatUnknown {
label = "DOWN:" + string(errCategory)
}
return dangerStyle.Render(label)
case "SSL EXP": case "SSL EXP":
return dangerStyle.Render(status) return dangerStyle.Render("▼ SSL EXP")
case "LATE": case "LATE":
return warnStyle.Render(status) return warnStyle.Render("◆ LATE")
case "STALE": case "STALE":
return staleStyle.Render(status) return staleStyle.Render("◆ STALE")
case "PENDING": case "PENDING":
return subtleStyle.Render(status) return subtleStyle.Render("○ PENDING")
default: default:
return specialStyle.Render(status) return specialStyle.Render("▲ " + status)
} }
} }
func fmtTimeAgo(t time.Time) string {
if t.IsZero() {
return subtleStyle.Render("never")
}
d := time.Since(t)
if d < time.Minute {
return fmt.Sprintf("%ds ago", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm ago", int(d.Minutes()))
}
if d < 24*time.Hour {
return fmt.Sprintf("%dh ago", int(d.Hours()))
}
return fmt.Sprintf("%dd ago", int(d.Hours())/24)
}
func fmtDuration(d time.Duration) string { func fmtDuration(d time.Duration) string {
if d < time.Minute { if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds())) return fmt.Sprintf("%ds", int(d.Seconds()))
+12 -17
View File
@@ -55,32 +55,27 @@ func TestSiteOrder(t *testing.T) {
} }
} }
func TestFmtStatus_ErrorCategory(t *testing.T) { func TestFmtStatus(t *testing.T) {
tests := []struct { tests := []struct {
status string status string
paused bool paused bool
inMaint bool inMaint bool
cat ErrorCategory
wantSub string wantSub string
}{ }{
{"DOWN", false, false, ErrCatDNS, "DOWN:DNS"}, {"DOWN", false, false, "▼ DOWN"},
{"DOWN", false, false, ErrCatTLS, "DOWN:TLS"}, {"UP", false, false, "▲ UP"},
{"DOWN", false, false, ErrCatHTTP, "DOWN:HTTP"}, {"SSL EXP", false, false, "▼ SSL EXP"},
{"DOWN", false, false, ErrCatTCP, "DOWN:TCP"}, {"LATE", false, false, "◆ LATE"},
{"DOWN", false, false, ErrCatTimeout, "DOWN:TMO"}, {"STALE", false, false, "◆ STALE"},
{"DOWN", false, false, ErrCatICMP, "DOWN:ICMP"}, {"PENDING", false, false, "○ PENDING"},
{"DOWN", false, false, ErrCatPrivate, "DOWN:PRIV"}, {"DOWN", true, false, "◇ PAUSED"},
{"DOWN", false, false, ErrCatUnknown, "DOWN"}, {"DOWN", false, true, "◼ MAINT"},
{"UP", false, false, ErrCatUnknown, "UP"},
{"SSL EXP", false, false, ErrCatUnknown, "SSL EXP"},
{"DOWN", true, false, ErrCatDNS, "PAUSED"},
{"DOWN", false, true, ErrCatDNS, "MAINT"},
} }
for _, tt := range tests { for _, tt := range tests {
got := fmtStatus(tt.status, tt.paused, tt.inMaint, tt.cat) got := fmtStatus(tt.status, tt.paused, tt.inMaint)
if !containsPlain(got, tt.wantSub) { if !containsPlain(got, tt.wantSub) {
t.Errorf("fmtStatus(%q, paused=%v, maint=%v, %q): %q missing %q", t.Errorf("fmtStatus(%q, paused=%v, maint=%v): %q missing %q",
tt.status, tt.paused, tt.inMaint, tt.cat, got, tt.wantSub) tt.status, tt.paused, tt.inMaint, got, tt.wantSub)
} }
} }
} }
+1 -15
View File
@@ -3,7 +3,6 @@ package tui
import ( import (
"fmt" "fmt"
"strings" "strings"
"time"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor" "gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -146,20 +145,7 @@ func fmtAlertHealth(h monitor.AlertHealth) string {
} }
func fmtAlertLastSent(h monitor.AlertHealth) string { func fmtAlertLastSent(h monitor.AlertHealth) string {
if h.LastSendAt.IsZero() { return fmtTimeAgo(h.LastSendAt)
return subtleStyle.Render("never")
}
d := time.Since(h.LastSendAt)
if d < time.Minute {
return fmt.Sprintf("%ds ago", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm ago", int(d.Minutes()))
}
if d < 24*time.Hour {
return fmt.Sprintf("%dh ago", int(d.Hours()))
}
return fmt.Sprintf("%dd ago", int(d.Hours())/24)
} }
func (m Model) viewAlertsTab() string { func (m Model) viewAlertsTab() string {
+1 -12
View File
@@ -1,7 +1,6 @@
package tui package tui
import ( import (
"fmt"
"time" "time"
) )
@@ -66,15 +65,5 @@ func fmtNodeStatus(lastSeen time.Time) string {
} }
func fmtNodeLastSeen(t time.Time) string { func fmtNodeLastSeen(t time.Time) string {
if t.IsZero() { return fmtTimeAgo(t)
return subtleStyle.Render("never")
}
ago := time.Since(t)
if ago < time.Minute {
return fmt.Sprintf("%ds ago", int(ago.Seconds()))
}
if ago < time.Hour {
return fmt.Sprintf("%dm ago", int(ago.Minutes()))
}
return fmt.Sprintf("%dh ago", int(ago.Hours()))
} }
+2 -2
View File
@@ -128,7 +128,7 @@ func (m Model) viewSitesTab() string {
strconv.Itoa(i + 1), strconv.Itoa(i + 1),
m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-4)), m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-4)),
"group", "group",
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID), ErrCatUnknown), fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
subtleStyle.Render("—"), subtleStyle.Render("—"),
m.groupUptime(site.ID), m.groupUptime(site.ID),
m.groupSparkline(site.ID, sparkWidth), m.groupSparkline(site.ID, sparkWidth),
@@ -175,7 +175,7 @@ func (m Model) viewSitesTab() string {
strconv.Itoa(i + 1), strconv.Itoa(i + 1),
m.zones.Mark(fmt.Sprintf("site-%d", i), name), m.zones.Mark(fmt.Sprintf("site-%d", i), name),
typeIcon(site.Type, false) + " " + site.Type, typeIcon(site.Type, false) + " " + site.Type,
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID), classifyError(site.LastError, site.Type, site.StatusCode)), fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
fmtLatency(site.Latency), fmtLatency(site.Latency),
fmtUptime(hist.Statuses), fmtUptime(hist.Statuses),
spark, spark,
+2 -3
View File
@@ -41,8 +41,7 @@ func (m Model) viewDetailPanel() string {
b.WriteString("\n" + subtleStyle.Render(" "+label) + "\n") b.WriteString("\n" + subtleStyle.Render(" "+label) + "\n")
} }
errCat := classifyError(site.LastError, site.Type, site.StatusCode) row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID), errCat))
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" { if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" {
errWidth := m.termWidth - chromePadH - 19 errWidth := m.termWidth - chromePadH - 19
@@ -128,7 +127,7 @@ func (m Model) viewDetailPanel() string {
row("Latency", fmtLatency(site.Latency)) row("Latency", fmtLatency(site.Latency))
row("Uptime", fmtUptime(hist.Statuses)) row("Uptime", fmtUptime(hist.Statuses))
if !site.LastCheck.IsZero() { if !site.LastCheck.IsZero() {
row("Last Check", site.LastCheck.Format("15:04:05")) row("Last Check", fmtTimeAgo(site.LastCheck))
} }
if site.Type == "http" { if site.Type == "http" {