From af5246e7772385aecc199da5b39f49cf0bdd7742 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Thu, 28 May 2026 13:18:27 -0400 Subject: [PATCH] =?UTF-8?q?chore(tui):=20visual=20polish=20=E2=80=94=20det?= =?UTF-8?q?ail=20sections,=20column=20headers,=20alert=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detail panel: - Grouped fields into sections (ENDPOINT, TIMING, HTTP, CONFIG) - Omit Timeout when 0 (unconfigured) - Omit Method when default GET - Show explicit "200-299" when AcceptedCodes empty Table: - LATENCY header → LAT (design short, never truncate) Alerts: - Press [i] for alert detail panel: full config, health status, send counts, last error - Keybinding display updated with [i]Info Bundled remaining UX polish items from screenshot review. --- internal/tui/tab_alerts.go | 47 +++++++++++++++++++++++++++++++++++ internal/tui/tab_sites.go | 50 ++++++++++++++++++++++++++------------ internal/tui/tui.go | 15 +++++++++++- 3 files changed, 96 insertions(+), 16 deletions(-) diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index b86d905..0ac4316 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -2,6 +2,7 @@ package tui import ( "fmt" + "strings" "time" "gitea.lerkolabs.com/lerko/uptop/internal/monitor" @@ -173,6 +174,52 @@ func (m Model) viewAlertsTab() string { ) } +func (m Model) viewAlertDetailPanel() string { + if m.cursor >= len(m.alerts) { + return "" + } + a := m.alerts[m.cursor] + h := m.engine.GetAlertHealth(a.ID) + + var b strings.Builder + + b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n") + + row := func(label, value string) { + fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) + } + + row("Type", fmtAlertType(a.Type)) + + if h.LastSendAt.IsZero() { + row("Health", subtleStyle.Render("never sent")) + } else if h.LastSendOK { + row("Health", specialStyle.Render("OK")) + } else { + row("Health", dangerStyle.Render("FAILED")) + } + + if !h.LastSendAt.IsZero() { + row("Last Sent", h.LastSendAt.Format("2006-01-02 15:04:05")+" ("+fmtAlertLastSent(h)+")") + } + if h.SendCount > 0 { + row("Sends", fmt.Sprintf("%d sent, %d failed", h.SendCount, h.FailCount)) + } + if h.LastError != "" { + row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60))) + } + + b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n") + for k, v := range a.Settings { + row(k, v) + } + + b.WriteString("\n\n") + b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit")) + + return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) +} + func (m *Model) initAlertHuhForm() tea.Cmd { m.alertFormData = &alertFormData{ AlertType: "discord", diff --git a/internal/tui/tab_sites.go b/internal/tui/tab_sites.go index 838ea00..c35cafe 100644 --- a/internal/tui/tab_sites.go +++ b/internal/tui/tab_sites.go @@ -378,7 +378,7 @@ func (m Model) viewSitesTab() string { var groupRows map[int]bool return m.renderTable( - []string{"#", "NAME", "TYPE", "STATUS", "LATENCY", "UPTIME", "HISTORY", "SSL", "RETRY"}, + []string{"#", "NAME", "TYPE", "STATUS", "LAT", "UPTIME", "HISTORY", "SSL", "RETRY"}, len(m.sites), func(start, end int) [][]string { groupRows = make(map[int]bool) @@ -764,6 +764,10 @@ func (m Model) viewDetailPanel() string { fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value) } + section := func(label string) { + b.WriteString("\n" + subtleStyle.Render(" "+label) + "\n") + } + row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))) if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE") && site.LastError != "" { @@ -792,6 +796,8 @@ func (m Model) viewDetailPanel() string { } } } + + section("ENDPOINT") row("Type", site.Type) if site.URL != "" { row("URL", site.URL) @@ -802,31 +808,45 @@ func (m Model) viewDetailPanel() string { if site.Port > 0 { row("Port", strconv.Itoa(site.Port)) } + + section("TIMING") row("Interval", fmt.Sprintf("%ds", site.Interval)) - row("Timeout", fmt.Sprintf("%ds", site.Timeout)) + if site.Timeout > 0 { + row("Timeout", fmt.Sprintf("%ds", site.Timeout)) + } row("Latency", fmtLatency(site.Latency)) row("Uptime", fmtUptime(hist.Statuses)) + if !site.LastCheck.IsZero() { + row("Last Check", site.LastCheck.Format("15:04:05")) + } if site.Type == "http" { - row("Method", site.Method) - row("Codes", site.AcceptedCodes) + section("HTTP") + if site.Method != "" && site.Method != "GET" { + row("Method", site.Method) + } + codes := site.AcceptedCodes + if codes == "" { + codes = "200-299" + } + row("Codes", codes) row("SSL", fmtSSL(site)) if site.IgnoreTLS { row("TLS Verify", dangerStyle.Render("disabled")) } } - if site.MaxRetries > 0 { - row("Retries", fmtRetries(site)) - } - if site.Regions != "" { - row("Regions", site.Regions) - } - if site.Description != "" { - row("Description", site.Description) - } - if !site.LastCheck.IsZero() { - row("Last Check", site.LastCheck.Format("15:04:05")) + if site.MaxRetries > 0 || site.Regions != "" || site.Description != "" { + section("CONFIG") + if site.MaxRetries > 0 { + row("Retries", fmtRetries(site)) + } + if site.Regions != "" { + row("Regions", site.Regions) + } + if site.Description != "" { + row("Description", site.Description) + } } probeResults := m.engine.GetProbeResults(site.ID) diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 3b0418a..0677643 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -68,6 +68,7 @@ const ( stateLogs stateUsers stateDetail + stateAlertDetail stateFormSite stateFormAlert stateFormUser @@ -384,6 +385,14 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Quit } return m, nil + case stateAlertDetail: + switch msg.String() { + case "i", "esc": + m.state = stateDashboard + case "q": + return m, tea.Quit + } + return m, nil case stateDashboard, stateLogs, stateUsers: switch msg.String() { case "q": @@ -497,6 +506,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "i": if m.currentTab == 0 && len(m.sites) > 0 { m.state = stateDetail + } else if m.currentTab == 1 && len(m.alerts) > 0 { + m.state = stateAlertDetail } case "x": if m.currentTab == 4 && len(m.maintenanceWindows) > 0 { @@ -818,6 +829,8 @@ func (m Model) View() string { return "" case stateDetail: return m.viewDetailPanel() + case stateAlertDetail: + return m.viewAlertDetailPanel() default: return m.zones.Scan(m.viewDashboard()) } @@ -954,7 +967,7 @@ func (m Model) viewDashboard() string { case 0: keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit" case 1: - keys = "[n]New [e]Edit [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit" + keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit" case 2: keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit" case 4: