fix(tui): cohesion polish across all views #161

Merged
lerko merged 1 commits from fix/tui-cohesion-polish into main 2026-06-29 15:21:59 +00:00
9 changed files with 96 additions and 77 deletions
+2 -2
View File
@@ -328,7 +328,7 @@ func (m Model) viewAlertDetailPanel() string {
} }
b.WriteString(m.divider() + "\n") b.WriteString(m.divider() + "\n")
b.WriteString(m.st.subtleStyle.Render(" CONFIGURATION") + "\n") b.WriteString(m.st.titleStyle.Render(" CONFIGURATION") + "\n")
// Render through the same allowlist the backup export uses — this panel // Render through the same allowlist the backup export uses — this panel
// ends up in screen shares and asciinema recordings. Keys are sorted so // ends up in screen shares and asciinema recordings. Keys are sorted so
// rows don't reshuffle every render. // rows don't reshuffle every render.
@@ -348,7 +348,7 @@ func (m Model) viewAlertDetailPanel() string {
} }
b.WriteString(m.divider() + "\n") b.WriteString(m.divider() + "\n")
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [t] Test")) b.WriteString(m.st.subtleStyle.Render(" [e] Edit [t] Test [q/Esc] Back"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
} }
+2 -2
View File
@@ -74,14 +74,14 @@ func (m Model) renderLogLine(entry models.LogEntry) string {
} }
func (m Model) viewLogsFullscreen() string { func (m Model) viewLogsFullscreen() string {
header := m.st.subtleStyle.Render(" Logs") + "\n" + m.divider() header := " " + m.st.titleStyle.Render("Logs") + "\n" + m.divider()
filterLabel := m.st.subtleStyle.Render("[f] All") filterLabel := m.st.subtleStyle.Render("[f] All")
if m.logFilterImportant { if m.logFilterImportant {
filterLabel = m.st.subtleStyle.Render("[f] Important only") filterLabel = m.st.subtleStyle.Render("[f] Important only")
} }
countLabel := m.st.subtleStyle.Render(fmt.Sprintf("%d/%d", m.logShown, m.logTotal)) countLabel := m.st.subtleStyle.Render(fmt.Sprintf("%d/%d", m.logShown, m.logTotal))
footer := m.divider() + "\n " + m.st.subtleStyle.Render("[q/Esc] Back") + " " + filterLabel + " " + countLabel footer := m.divider() + "\n " + filterLabel + " " + countLabel + " " + m.st.subtleStyle.Render("[q/Esc] Back")
m.logViewport.Width = m.termWidth - chromePadH m.logViewport.Width = m.termWidth - chromePadH
m.logViewport.Height = m.termHeight - 8 m.logViewport.Height = m.termHeight - 8
+6 -6
View File
@@ -14,13 +14,13 @@ func (m Model) viewNodesTab() string {
var headers []string var headers []string
var widths []int var widths []int
if m.isWide() { if m.isWide() {
headers = []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"} headers = []string{"#", "NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"}
widths = []int{24, 14, 16, 12, 10} widths = []int{4, 24, 14, 16, 12, 10}
} else { } else {
headers = []string{"NAME", "REGION", "SEEN", "VER", "STATUS"} headers = []string{"#", "NAME", "REGION", "SEEN", "VER", "STATUS"}
widths = []int{16, 10, 10, 8, 8} widths = []int{4, 16, 10, 10, 8, 8}
} }
nameW := widths[0] nameW := widths[1]
tbl := m.renderTable( tbl := m.renderTable(
headers, headers,
@@ -43,7 +43,7 @@ func (m Model) viewNodesTab() string {
version = m.st.subtleStyle.Render("—") version = m.st.subtleStyle.Render("—")
} }
status := m.fmtNodeStatus(node.LastSeen) status := m.fmtNodeStatus(node.LastSeen)
rows = append(rows, []string{name, region, lastSeen, version, status}) rows = append(rows, []string{fmt.Sprintf("%d", i+1), name, region, lastSeen, version, status})
} }
return rows return rows
}, },
+5 -2
View File
@@ -941,8 +941,11 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
if m.zones.Get(fmt.Sprintf("%s-%d", prefix, i)).InBounds(msg) { if m.zones.Get(fmt.Sprintf("%s-%d", prefix, i)).InBounds(msg) {
m.cursor = i m.cursor = i
m.syncSelectedID() m.syncSelectedID()
if m.detailOpen && m.currentTab == tabMonitors { if m.currentTab == tabMonitors {
return m, m.loadDetailCmd(m.sites[m.cursor].ID) m.focusedPanel = panelMonitors
if m.detailOpen {
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
}
} }
return m, nil return m, nil
} }
+4 -2
View File
@@ -198,10 +198,12 @@ func (m Model) viewDashboard() string {
} }
case tabMaint: case tabMaint:
m.contentWidth = m.termWidth m.contentWidth = m.termWidth
content = m.viewMaintTab() availW := m.termWidth - chromePadH
content = m.titledPanel("Maintenance", m.viewMaintTab(), availW, true)
case tabSettings: case tabSettings:
m.contentWidth = m.termWidth m.contentWidth = m.termWidth
content = m.viewSettingsTab() availW := m.termWidth - chromePadH
content = m.titledPanel("Settings", m.viewSettingsTab(), availW, true)
} }
content = strings.TrimSpace(content) content = strings.TrimSpace(content)
+61 -53
View File
@@ -68,7 +68,9 @@ func (m Model) viewDetailPanel() string {
if site.Port > 0 { if site.Port > 0 {
left = append(left, row("Port", strconv.Itoa(site.Port))) left = append(left, row("Port", strconv.Itoa(site.Port)))
} }
left = append(left, row("Interval", fmt.Sprintf("%ds", site.Interval))) if site.Interval > 0 {
left = append(left, row("Interval", fmt.Sprintf("%ds", site.Interval)))
}
if site.MaxRetries > 0 { if site.MaxRetries > 0 {
left = append(left, row("Retries", m.fmtRetries(site))) left = append(left, row("Retries", m.fmtRetries(site)))
} }
@@ -212,64 +214,66 @@ func (m Model) viewDetailPanel() string {
} }
bottomColW := graphW bottomColW := graphW
// Left: latency + histogram
var graphLines []string
sectionLabel := func(title string) string { sectionLabel := func(title string) string {
return m.st.titleStyle.Render(" " + title) return m.st.titleStyle.Render(" " + title)
} }
graphLines = append(graphLines, sectionLabel("LATENCY")) // Left: latency + histogram (skip for groups — they have no own checks)
if site.Type == "push" { var graphLines []string
sparkW := bottomColW - 4 if site.Type != "group" {
if sparkW > detailSparkWidth { graphLines = append(graphLines, sectionLabel("LATENCY"))
sparkW = detailSparkWidth if site.Type == "push" {
} sparkW := bottomColW - 4
graphLines = append(graphLines, " "+m.heartbeatSparkline(hist.Statuses, sparkW, nil)) if sparkW > detailSparkWidth {
if len(hist.Statuses) > 0 { sparkW = detailSparkWidth
up := 0 }
for _, s := range hist.Statuses { graphLines = append(graphLines, " "+m.heartbeatSparkline(hist.Statuses, sparkW, nil))
if s { if len(hist.Statuses) > 0 {
up++ up := 0
for _, s := range hist.Statuses {
if s {
up++
}
} }
graphLines = append(graphLines, fmt.Sprintf(" %s %d/%d checks up",
m.st.subtleStyle.Render("Heartbeats"), up, len(hist.Statuses)))
} }
graphLines = append(graphLines, fmt.Sprintf(" %s %d/%d checks up", } else {
m.st.subtleStyle.Render("Heartbeats"), up, len(hist.Statuses))) sparkW := bottomColW - 4
} if sparkW > detailSparkWidth {
} else { sparkW = detailSparkWidth
sparkW := bottomColW - 4
if sparkW > detailSparkWidth {
sparkW = detailSparkWidth
}
graphLines = append(graphLines, " "+m.latencySparkline(hist.Latencies, hist.Statuses, sparkW, nil))
var minL, maxL, total time.Duration
count := 0
for i, l := range hist.Latencies {
if i < len(hist.Statuses) && !hist.Statuses[i] {
continue
} }
if count == 0 { graphLines = append(graphLines, " "+m.latencySparkline(hist.Latencies, hist.Statuses, sparkW, nil))
minL, maxL = l, l var minL, maxL, total time.Duration
} else if l < minL { count := 0
minL = l for i, l := range hist.Latencies {
} else if l > maxL { if i < len(hist.Statuses) && !hist.Statuses[i] {
maxL = l continue
}
if count == 0 {
minL, maxL = l, l
} else if l < minL {
minL = l
} else if l > maxL {
maxL = l
}
total += l
count++
}
if count > 0 {
avg := total / time.Duration(count)
graphLines = append(graphLines, fmt.Sprintf(" %s %dms %s %dms %s %dms",
m.st.subtleStyle.Render("Min"), minL.Milliseconds(),
m.st.subtleStyle.Render("Avg"), avg.Milliseconds(),
m.st.subtleStyle.Render("Max"), maxL.Milliseconds()))
} }
total += l
count++
} }
if count > 0 {
avg := total / time.Duration(count)
graphLines = append(graphLines, fmt.Sprintf(" %s %dms %s %dms %s %dms",
m.st.subtleStyle.Render("Min"), minL.Milliseconds(),
m.st.subtleStyle.Render("Avg"), avg.Milliseconds(),
m.st.subtleStyle.Render("Max"), maxL.Milliseconds()))
}
}
if site.Type != "push" && len(hist.Latencies) > 5 { if site.Type != "push" && len(hist.Latencies) > 5 {
graphLines = append(graphLines, "") graphLines = append(graphLines, "")
graphLines = append(graphLines, sectionLabel("DISTRIBUTION")) graphLines = append(graphLines, sectionLabel("DISTRIBUTION"))
graphLines = append(graphLines, m.latencyHistogram(hist.Latencies, hist.Statuses, bottomColW)) graphLines = append(graphLines, m.latencyHistogram(hist.Latencies, hist.Statuses, bottomColW))
}
} }
// Right: state changes // Right: state changes
@@ -308,13 +312,17 @@ func (m Model) viewDetailPanel() string {
changeLines = append(changeLines, "") changeLines = append(changeLines, "")
} }
graphCol := lipgloss.NewStyle().Width(graphW).Render(strings.Join(graphLines, "\n")) if len(graphLines) > 0 {
changeCol := lipgloss.NewStyle().Width(changeW).Render(strings.Join(changeLines, "\n")) graphCol := lipgloss.NewStyle().Width(graphW).Render(strings.Join(graphLines, "\n"))
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, graphCol, changeCol) + "\n") changeCol := lipgloss.NewStyle().Width(changeW).Render(strings.Join(changeLines, "\n"))
b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, graphCol, changeCol) + "\n")
} else {
b.WriteString(strings.Join(changeLines, "\n") + "\n")
}
b.WriteString("\n") b.WriteString("\n")
b.WriteString(m.divider() + "\n") b.WriteString(m.divider() + "\n")
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect")) b.WriteString(m.st.subtleStyle.Render(" [e] Edit [h] History [s] SLA [click] Inspect [q/Esc] Back"))
// Wrap in a viewport for scrolling // Wrap in a viewport for scrolling
content := b.String() content := b.String()
+11 -3
View File
@@ -298,15 +298,23 @@ func (m Model) viewDetailSingleCol(site models.Site, hist monitor.SiteHistory, w
} }
func (m Model) detailKeys() string { func (m Model) detailKeys() string {
return m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [q/Esc] Back") return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [q/Esc] Back")
} }
func (m Model) fmtStatusWord(status string) string { func (m Model) fmtStatusWord(status string) string {
switch status { switch status {
case "DOWN": case "DOWN", "SSL EXP":
return m.st.dangerStyle.Render("DOWN") return m.st.dangerStyle.Render(status)
case "UP": case "UP":
return m.st.specialStyle.Render("UP") return m.st.specialStyle.Render("UP")
case "LATE":
return m.st.warnStyle.Render("LATE")
case "STALE":
return m.st.staleStyle.Render("STALE")
case "PENDING":
return m.st.subtleStyle.Render("PENDING")
case "PAUSED":
return m.st.warnStyle.Render("PAUSED")
default: default:
return m.st.subtleStyle.Render(status) return m.st.subtleStyle.Render(status)
} }
+2 -3
View File
@@ -151,8 +151,7 @@ func (m Model) buildHistoryContent() string {
func (m Model) viewHistoryPanel() string { func (m Model) viewHistoryPanel() string {
var b strings.Builder var b strings.Builder
header := " " + m.st.titleStyle.Render("STATE HISTORY: "+m.historySiteName) header := " " + m.st.subtleStyle.Render("History >") + " " + m.st.titleStyle.Render(m.historySiteName)
header += " " + m.st.subtleStyle.Render("[q/Esc] Back")
b.WriteString(header + "\n") b.WriteString(header + "\n")
divWidth := m.dividerWidth() divWidth := m.dividerWidth()
@@ -186,7 +185,7 @@ func (m Model) viewHistoryPanel() string {
parts = append(parts, "avg outage "+fmtDuration(avg)) parts = append(parts, "avg outage "+fmtDuration(avg))
} }
b.WriteString(" " + m.st.subtleStyle.Render(strings.Join(parts, " │ ")) + "\n") b.WriteString(" " + m.st.subtleStyle.Render(strings.Join(parts, " │ ")) + "\n")
b.WriteString(" " + m.st.subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back")) b.WriteString(" " + m.st.subtleStyle.Render("[↑/↓] Scroll [q/Esc] Back"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
} }
+3 -4
View File
@@ -24,8 +24,7 @@ var slaPeriods = []struct {
func (m Model) viewSLAPanel() string { func (m Model) viewSLAPanel() string {
var b strings.Builder var b strings.Builder
header := " " + m.st.titleStyle.Render("SLA REPORT: "+m.slaSiteName) header := " " + m.st.subtleStyle.Render("SLA >") + " " + m.st.titleStyle.Render(m.slaSiteName)
header += " " + m.st.subtleStyle.Render("[q/Esc] Back")
b.WriteString(header + "\n") b.WriteString(header + "\n")
b.WriteString(m.divider() + "\n") b.WriteString(m.divider() + "\n")
@@ -74,7 +73,7 @@ func (m Model) viewSLAPanel() string {
} }
} }
b.WriteString(" " + strings.Join(keys, " ")) b.WriteString(" " + strings.Join(keys, " "))
b.WriteString(" " + m.st.subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back")) b.WriteString(" " + m.st.subtleStyle.Render("[↑/↓] Scroll [q/Esc] Back"))
return lipgloss.NewStyle().Padding(1, 2).Render(b.String()) return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
} }
@@ -87,7 +86,7 @@ func (m Model) buildSLADailyContent() string {
barWidth = 10 barWidth = 10
} }
b.WriteString(" " + m.st.subtleStyle.Render("DAILY BREAKDOWN") + "\n") b.WriteString(" " + m.st.titleStyle.Render("DAILY BREAKDOWN") + "\n")
for _, day := range m.slaDailyBreakdown { for _, day := range m.slaDailyBreakdown {
dateStr := day.Date.Format("Jan 02") dateStr := day.Date.Format("Jan 02")
bar := m.uptimeBar(day.UptimePct, barWidth) bar := m.uptimeBar(day.UptimePct, barWidth)