diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index f9b6b3e..fa884a3 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -328,7 +328,7 @@ func (m Model) viewAlertDetailPanel() string { } 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 // ends up in screen shares and asciinema recordings. Keys are sorted so // rows don't reshuffle every render. @@ -348,7 +348,7 @@ func (m Model) viewAlertDetailPanel() string { } 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()) } diff --git a/internal/tui/tab_logs.go b/internal/tui/tab_logs.go index 7e1c896..6902538 100644 --- a/internal/tui/tab_logs.go +++ b/internal/tui/tab_logs.go @@ -74,14 +74,14 @@ func (m Model) renderLogLine(entry models.LogEntry) 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") if m.logFilterImportant { filterLabel = m.st.subtleStyle.Render("[f] Important only") } 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.Height = m.termHeight - 8 diff --git a/internal/tui/tab_nodes.go b/internal/tui/tab_nodes.go index ed5944f..dcb0b3b 100644 --- a/internal/tui/tab_nodes.go +++ b/internal/tui/tab_nodes.go @@ -14,13 +14,13 @@ func (m Model) viewNodesTab() string { var headers []string var widths []int if m.isWide() { - headers = []string{"NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"} - widths = []int{24, 14, 16, 12, 10} + headers = []string{"#", "NAME", "REGION", "LAST SEEN", "VERSION", "STATUS"} + widths = []int{4, 24, 14, 16, 12, 10} } else { - headers = []string{"NAME", "REGION", "SEEN", "VER", "STATUS"} - widths = []int{16, 10, 10, 8, 8} + headers = []string{"#", "NAME", "REGION", "SEEN", "VER", "STATUS"} + widths = []int{4, 16, 10, 10, 8, 8} } - nameW := widths[0] + nameW := widths[1] tbl := m.renderTable( headers, @@ -43,7 +43,7 @@ func (m Model) viewNodesTab() string { version = m.st.subtleStyle.Render("—") } 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 }, diff --git a/internal/tui/update.go b/internal/tui/update.go index ebea0d7..0326f46 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -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) { m.cursor = i m.syncSelectedID() - if m.detailOpen && m.currentTab == tabMonitors { - return m, m.loadDetailCmd(m.sites[m.cursor].ID) + if m.currentTab == tabMonitors { + m.focusedPanel = panelMonitors + if m.detailOpen { + return m, m.loadDetailCmd(m.sites[m.cursor].ID) + } } return m, nil } diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index 6bcde7a..28d57fc 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -198,10 +198,12 @@ func (m Model) viewDashboard() string { } case tabMaint: m.contentWidth = m.termWidth - content = m.viewMaintTab() + availW := m.termWidth - chromePadH + content = m.titledPanel("Maintenance", m.viewMaintTab(), availW, true) case tabSettings: m.contentWidth = m.termWidth - content = m.viewSettingsTab() + availW := m.termWidth - chromePadH + content = m.titledPanel("Settings", m.viewSettingsTab(), availW, true) } content = strings.TrimSpace(content) diff --git a/internal/tui/view_detail.go b/internal/tui/view_detail.go index c20ae58..8ba944e 100644 --- a/internal/tui/view_detail.go +++ b/internal/tui/view_detail.go @@ -68,7 +68,9 @@ func (m Model) viewDetailPanel() string { if site.Port > 0 { 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 { left = append(left, row("Retries", m.fmtRetries(site))) } @@ -212,64 +214,66 @@ func (m Model) viewDetailPanel() string { } bottomColW := graphW - // Left: latency + histogram - var graphLines []string sectionLabel := func(title string) string { return m.st.titleStyle.Render(" " + title) } - graphLines = append(graphLines, sectionLabel("LATENCY")) - if site.Type == "push" { - sparkW := bottomColW - 4 - if sparkW > detailSparkWidth { - sparkW = detailSparkWidth - } - graphLines = append(graphLines, " "+m.heartbeatSparkline(hist.Statuses, sparkW, nil)) - if len(hist.Statuses) > 0 { - up := 0 - for _, s := range hist.Statuses { - if s { - up++ + // Left: latency + histogram (skip for groups — they have no own checks) + var graphLines []string + if site.Type != "group" { + graphLines = append(graphLines, sectionLabel("LATENCY")) + if site.Type == "push" { + sparkW := bottomColW - 4 + if sparkW > detailSparkWidth { + sparkW = detailSparkWidth + } + graphLines = append(graphLines, " "+m.heartbeatSparkline(hist.Statuses, sparkW, nil)) + if len(hist.Statuses) > 0 { + 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", - m.st.subtleStyle.Render("Heartbeats"), up, len(hist.Statuses))) - } - } else { - 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 + } else { + sparkW := bottomColW - 4 + if sparkW > detailSparkWidth { + sparkW = detailSparkWidth } - if count == 0 { - minL, maxL = l, l - } else if l < minL { - minL = l - } else if l > maxL { - maxL = l + 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 { + 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 { - graphLines = append(graphLines, "") - graphLines = append(graphLines, sectionLabel("DISTRIBUTION")) - graphLines = append(graphLines, m.latencyHistogram(hist.Latencies, hist.Statuses, bottomColW)) + if site.Type != "push" && len(hist.Latencies) > 5 { + graphLines = append(graphLines, "") + graphLines = append(graphLines, sectionLabel("DISTRIBUTION")) + graphLines = append(graphLines, m.latencyHistogram(hist.Latencies, hist.Statuses, bottomColW)) + } } // Right: state changes @@ -308,13 +312,17 @@ func (m Model) viewDetailPanel() string { changeLines = append(changeLines, "") } - graphCol := lipgloss.NewStyle().Width(graphW).Render(strings.Join(graphLines, "\n")) - changeCol := lipgloss.NewStyle().Width(changeW).Render(strings.Join(changeLines, "\n")) - b.WriteString(lipgloss.JoinHorizontal(lipgloss.Top, graphCol, changeCol) + "\n") + if len(graphLines) > 0 { + graphCol := lipgloss.NewStyle().Width(graphW).Render(strings.Join(graphLines, "\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(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 content := b.String() diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index a11eeae..d67ba6f 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -298,15 +298,23 @@ func (m Model) viewDetailSingleCol(site models.Site, hist monitor.SiteHistory, w } 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 { switch status { - case "DOWN": - return m.st.dangerStyle.Render("DOWN") + case "DOWN", "SSL EXP": + return m.st.dangerStyle.Render(status) case "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: return m.st.subtleStyle.Render(status) } diff --git a/internal/tui/view_history.go b/internal/tui/view_history.go index a06563b..58c7b19 100644 --- a/internal/tui/view_history.go +++ b/internal/tui/view_history.go @@ -151,8 +151,7 @@ func (m Model) buildHistoryContent() string { func (m Model) viewHistoryPanel() string { var b strings.Builder - header := " " + m.st.titleStyle.Render("STATE HISTORY: "+m.historySiteName) - header += " " + m.st.subtleStyle.Render("[q/Esc] Back") + header := " " + m.st.subtleStyle.Render("History >") + " " + m.st.titleStyle.Render(m.historySiteName) b.WriteString(header + "\n") divWidth := m.dividerWidth() @@ -186,7 +185,7 @@ func (m Model) viewHistoryPanel() string { parts = append(parts, "avg outage "+fmtDuration(avg)) } 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()) } diff --git a/internal/tui/view_sla.go b/internal/tui/view_sla.go index f9c292b..27767c9 100644 --- a/internal/tui/view_sla.go +++ b/internal/tui/view_sla.go @@ -24,8 +24,7 @@ var slaPeriods = []struct { func (m Model) viewSLAPanel() string { var b strings.Builder - header := " " + m.st.titleStyle.Render("SLA REPORT: "+m.slaSiteName) - header += " " + m.st.subtleStyle.Render("[q/Esc] Back") + header := " " + m.st.subtleStyle.Render("SLA >") + " " + m.st.titleStyle.Render(m.slaSiteName) b.WriteString(header + "\n") b.WriteString(m.divider() + "\n") @@ -74,7 +73,7 @@ func (m Model) viewSLAPanel() string { } } 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()) } @@ -87,7 +86,7 @@ func (m Model) buildSLADailyContent() string { 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 { dateStr := day.Date.Format("Jan 02") bar := m.uptimeBar(day.UptimePct, barWidth)