fix(tui): cohesion polish across all views
- Hide nonsense latency/histogram data for group monitors in detail view - Add missing status colors to fmtStatusWord (LATE, STALE, PENDING, etc) - Wrap Maint and Settings tabs in titled panels to match Monitors tab - Standardize headers: breadcrumb style (History > Name, SLA > Name) - Standardize footer key order: actions first, [q/Esc] Back last - Add # column to Nodes table for consistency with all other tables - Use titleStyle for section headers (CONFIGURATION, DAILY BREAKDOWN) - Fix panel focus not switching when clicking monitor rows from logs panel
This commit was merged in pull request #161.
This commit is contained in:
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user