Files
uptop/internal/tui/format.go
T
lerko 5d2b7a3e66
CI / test (pull_request) Successful in 1m55s
CI / lint (pull_request) Successful in 1m27s
CI / vulncheck (pull_request) Successful in 1m1s
fix: seven quick-win bug fixes across engine, server, TUI, CLI
1. Alertless monitors no longer spam error logs — triggerAlert
   returns early when alertID <= 0.

2. HTTP response body drained before close — enables connection
   reuse via keep-alive instead of fresh TCP+TLS per check.

3. /api/backup/export enforces GET — was the only endpoint
   accepting any HTTP method.

4. limitStr guards against max < 3 — prevents negative slice
   index panic on very narrow terminals.

5. Filter input accepts multibyte characters — len(msg.Runes)
   instead of len(msg.String()) for proper Unicode support.

6. Startup warning corrected — with no UPTOP_CLUSTER_SECRET,
   endpoints reject (401), not accept. Warning now says so.

7. UPTOP_KEYS file open failure logged — was silently swallowed,
   leaving operators with no admin seeded and no message.
2026-06-11 18:28:32 -04:00

220 lines
4.5 KiB
Go

package tui
import (
"fmt"
"strings"
"time"
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
"github.com/charmbracelet/lipgloss"
)
func (m Model) dividerWidth() int {
w := m.termWidth - chromePadH - 4
if w < 40 {
w = 40
}
return w
}
func (m Model) divider() string {
return " " + m.st.subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
}
func (m Model) emptyState(message, hint string) string {
content := message
if hint != "" {
content += "\n\n" + m.st.subtleStyle.Render(hint)
}
return "\n" + lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Accent).
Padding(1, 3).
Render(content)
}
func limitStr(text string, max int) string {
if max < 3 {
return text
}
runes := []rune(text)
if len(runes) > max {
return string(runes[:max-3]) + "..."
}
return text
}
func siteOrder(s models.Site) int {
if s.Paused {
return 3
}
switch s.Status {
case "DOWN", "SSL EXP":
return 0
case "STALE":
return 1
case "LATE":
return 1
case "PENDING":
return 3
default:
return 2
}
}
func typeIcon(siteType string, collapsed bool) string {
switch siteType {
case "http":
return "→"
case "push":
return "↓"
case "ping":
return "↔"
case "port":
return "⊡"
case "dns":
return "◆"
case "group":
if collapsed {
return "▶"
}
return "▼"
default:
return "·"
}
}
func (m Model) fmtLatency(d time.Duration) string {
ms := d.Milliseconds()
if ms == 0 {
return m.st.subtleStyle.Render("—")
}
var s string
if ms < 1000 {
s = fmt.Sprintf("%dms", ms)
} else {
s = fmt.Sprintf("%.1fs", float64(ms)/1000)
}
if ms < 200 {
return m.st.specialStyle.Render(s)
}
if ms < 500 {
return m.st.warnStyle.Render(s)
}
return m.st.dangerStyle.Render(s)
}
func (m Model) fmtUptime(statuses []bool) string {
if len(statuses) == 0 {
return m.st.subtleStyle.Render("—")
}
up := 0
for _, s := range statuses {
if s {
up++
}
}
pct := float64(up) / float64(len(statuses)) * 100
s := fmt.Sprintf("%.1f%%", pct)
if pct >= 99 {
return m.st.specialStyle.Render(s)
}
if pct >= 95 {
return m.st.warnStyle.Render(s)
}
return m.st.dangerStyle.Render(s)
}
func (m Model) fmtSSL(site models.Site) string {
if site.Type != "http" || !site.CheckSSL || !site.HasSSL {
return m.st.subtleStyle.Render("-")
}
days := int(time.Until(site.CertExpiry).Hours() / 24)
s := fmt.Sprintf("%dd", days)
if days <= 0 {
return m.st.dangerStyle.Render("EXPIRED")
}
if days <= site.ExpiryThreshold {
return m.st.warnStyle.Render(s)
}
return m.st.specialStyle.Render(s)
}
func (m Model) fmtRetries(site models.Site) string {
dispCount := site.FailureCount
if dispCount > site.MaxRetries {
dispCount = site.MaxRetries
}
s := fmt.Sprintf("%d/%d", dispCount, site.MaxRetries)
if site.Status == models.StatusDown {
return m.st.dangerStyle.Render(s)
}
if site.Status == models.StatusUp && site.FailureCount > 0 {
return m.st.warnStyle.Render(s)
}
return s
}
func (m Model) fmtStatus(status models.Status, paused bool, inMaint bool) string {
if paused {
return m.st.warnStyle.Render("◇ PAUSED")
}
if inMaint {
return m.st.maintStyle.Render("◼ MAINT")
}
switch status {
case models.StatusDown:
return m.st.dangerStyle.Render("▼ DOWN")
case models.StatusSSLExp:
return m.st.dangerStyle.Render("▼ SSL EXP")
case models.StatusLate:
return m.st.warnStyle.Render("◆ LATE")
case models.StatusStale:
return m.st.staleStyle.Render("◆ STALE")
case models.StatusPending:
return m.st.subtleStyle.Render("○ PENDING")
default:
return m.st.specialStyle.Render("▲ " + string(status))
}
}
func (m Model) fmtTimeAgo(t time.Time) string {
if t.IsZero() {
return m.st.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 {
if d < time.Minute {
return fmt.Sprintf("%ds", int(d.Seconds()))
}
if d < time.Hour {
return fmt.Sprintf("%dm", int(d.Minutes()))
}
if d < 24*time.Hour {
h := int(d.Hours())
m := int(d.Minutes()) % 60
if m > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
return fmt.Sprintf("%dh", h)
}
days := int(d.Hours()) / 24
hours := int(d.Hours()) % 24
if hours > 0 {
return fmt.Sprintf("%dd %dh", days, hours)
}
return fmt.Sprintf("%dd", days)
}