refactor(tui): consistent chrome across all views
CI / test (pull_request) Successful in 2m36s
CI / lint (pull_request) Successful in 56s
CI / vulncheck (pull_request) Successful in 51s

- Extract divider() and emptyState() helpers to format.go
- All empty states now use bordered box with accent color
- Detail and alert detail panels get header/section dividers
- SLA label width 14→16 to match detail/alert panels
- Logs key hints moved from content to dashboard footer
- History/SLA panels use shared divider helper
This commit is contained in:
2026-06-04 15:08:29 -04:00
parent e0cb0adebd
commit ba75be194d
11 changed files with 60 additions and 48 deletions
+26
View File
@@ -2,11 +2,37 @@ 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 " " + subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
}
func (m Model) emptyState(message, hint string) string {
content := message
if hint != "" {
content += "\n\n" + 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 {
runes := []rune(text)
if len(runes) > max {
+6 -4
View File
@@ -164,7 +164,7 @@ func fmtAlertLastSent(h monitor.AlertHealth) string {
func (m Model) viewAlertsTab() string {
if len(m.alerts) == 0 {
return "\n No alert channels configured. Press [n] to add one."
return m.emptyState("No alert channels configured.", "[n] Add your first alert")
}
var headers []string
@@ -214,7 +214,8 @@ func (m Model) viewAlertDetailPanel() string {
var b strings.Builder
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n")
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n")
b.WriteString(m.divider() + "\n")
row := func(label, value string) {
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
@@ -240,12 +241,13 @@ func (m Model) viewAlertDetailPanel() string {
row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60)))
}
b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n")
b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" CONFIGURATION") + "\n")
for k, v := range a.Settings {
row(k, v)
}
b.WriteString("\n\n")
b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
+2 -2
View File
@@ -85,7 +85,7 @@ func renderLogLine(line string) string {
func (m Model) viewLogsTab() string {
content := m.logViewport.View()
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
return "\n No log entries yet. Logs appear as monitors run checks."
return m.emptyState("No log entries yet.", "Logs appear as monitors run checks")
}
lines := strings.Split(content, "\n")
@@ -112,7 +112,7 @@ func (m Model) viewLogsTab() string {
}
header := subtleStyle.Render(fmt.Sprintf(
" %d entries [↑/↓] Scroll [PgUp/PgDn] Page [f] Filter: %s", shown, filterLabel))
" %d entries Filter: %s", shown, filterLabel))
if m.logFilterImportant && shown < total {
header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
+1 -1
View File
@@ -93,7 +93,7 @@ func (m Model) isMonitorInMaintenance(monitorID int) bool {
func (m Model) viewMaintTab() string {
if len(m.maintenanceWindows) == 0 {
return "\n No maintenance windows or incidents. Press [n] to create one."
return m.emptyState("No maintenance windows or incidents.", "[n] Create one")
}
var headers []string
+1 -1
View File
@@ -7,7 +7,7 @@ import (
func (m Model) viewNodesTab() string {
if len(m.nodes) == 0 {
return "\n No probe nodes connected."
return m.emptyState("No probe nodes connected.", "")
}
var headers []string
+1 -10
View File
@@ -101,16 +101,7 @@ func (m Model) computeLayout() tableLayout {
func (m Model) viewSitesTab() string {
if len(m.sites) == 0 {
welcome := lipgloss.NewStyle().
Border(lipgloss.RoundedBorder()).
BorderForeground(m.theme.Accent).
Padding(1, 3).
Render(
titleStyle.Render("uptop") + "\n\n" +
"No monitors configured yet.\n\n" +
subtleStyle.Render("[n] Add your first monitor"),
)
return "\n" + welcome
return m.emptyState(titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor")
}
layout := m.computeLayout()
+1 -1
View File
@@ -29,7 +29,7 @@ func fmtKey(key string) string {
func (m Model) viewUsersTab() string {
if len(m.users) == 0 {
return "\n No users configured. Press [n] to add one."
return m.emptyState("No users configured.", "[n] Add a user")
}
var headers []string
+1 -1
View File
@@ -266,7 +266,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
case 1:
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"
keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit"
case 4:
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
case 5:
+5 -3
View File
@@ -30,7 +30,8 @@ func (m Model) viewDetailPanel() string {
if breadcrumb == "" {
breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name)
}
b.WriteString(breadcrumb + "\n\n")
b.WriteString(breadcrumb + "\n")
b.WriteString(m.divider() + "\n")
row := func(label, value string) {
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
@@ -200,7 +201,7 @@ func (m Model) viewDetailPanel() string {
b.WriteString(" " + subtleStyle.Render("[h] History") + "\n")
}
b.WriteString("\n")
b.WriteString(m.divider() + "\n")
const sparkWidth = 40
if site.Type == "push" {
b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth))
@@ -242,7 +243,8 @@ func (m Model) viewDetailPanel() string {
}
}
b.WriteString("\n\n")
b.WriteString("\n")
b.WriteString(m.divider() + "\n")
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [q] Quit"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
+4 -7
View File
@@ -153,16 +153,13 @@ func (m Model) viewHistoryPanel() string {
header += " " + subtleStyle.Render("[q] Back")
b.WriteString(header + "\n")
divWidth := m.termWidth - chromePadH - 4
if divWidth < 40 {
divWidth = 40
}
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
divWidth := m.dividerWidth()
b.WriteString(m.divider() + "\n")
sparkline := stateChangeSparkline(m.historyChanges, divWidth)
if sparkline != "" {
b.WriteString(" " + sparkline + "\n")
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
b.WriteString(m.divider() + "\n")
}
fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n",
@@ -177,7 +174,7 @@ func (m Model) viewHistoryPanel() string {
b.WriteString(m.historyViewport.View())
}
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
b.WriteString("\n" + m.divider() + "\n")
stats := computeHistoryStats(m.historyChanges)
parts := []string{fmt.Sprintf("%d events", stats.totalEvents)}
+12 -18
View File
@@ -27,20 +27,14 @@ func (m Model) viewSLAPanel() string {
header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName)
header += " " + subtleStyle.Render("[q] Back")
b.WriteString(header + "\n")
divWidth := m.termWidth - chromePadH - 4
if divWidth < 40 {
divWidth = 40
}
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
b.WriteString(m.divider() + "\n")
period := slaPeriods[m.slaPeriodIdx]
b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n")
r := m.slaReport
// Uptime bar
barWidth := divWidth - 30
barWidth := m.dividerWidth() - 30
if barWidth < 10 {
barWidth = 10
}
@@ -52,23 +46,23 @@ func (m Model) viewSLAPanel() string {
if r.UptimePct < 99.0 {
uptimeColor = dangerStyle
}
fmt.Fprintf(&b, " %-14s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
fmt.Fprintf(&b, " %-14s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
fmt.Fprintf(&b, " %-16s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
fmt.Fprintf(&b, " %-16s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
if r.OutageCount > 0 {
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
}
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
b.WriteString("\n" + m.divider() + "\n")
if len(m.slaDailyBreakdown) > 0 {
b.WriteString(m.slaViewport.View())
}
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
b.WriteString("\n" + m.divider() + "\n")
var keys []string
for i, p := range slaPeriods {
@@ -80,7 +74,7 @@ func (m Model) viewSLAPanel() string {
}
}
b.WriteString(" " + strings.Join(keys, " "))
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll"))
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
}
@@ -88,7 +82,7 @@ func (m Model) viewSLAPanel() string {
func (m Model) buildSLADailyContent() string {
var b strings.Builder
barWidth := m.termWidth - chromePadH - 30
barWidth := m.dividerWidth() - 30
if barWidth < 10 {
barWidth = 10
}