From 0badc2ddf5cf10ca437050ca75a1d29f2c250dfe Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 20:18:39 -0400 Subject: [PATCH 1/6] fix(tui): cap detail sidebar height to match monitors panel SLA daily breakdown no longer overflows past the monitors panel. Detail sidebar height is capped to monitors panel height via titledPanelH. SLA daily rows are limited to fit within available space. Logs strip stays visible in all detail modes. --- internal/tui/panel.go | 12 ++++++++++++ internal/tui/update_test.go | 2 +- internal/tui/view_dashboard.go | 5 +++-- internal/tui/view_detail_inline.go | 30 ++++++++++++++++++++++-------- 4 files changed, 38 insertions(+), 11 deletions(-) diff --git a/internal/tui/panel.go b/internal/tui/panel.go index 6935334..a8480f1 100644 --- a/internal/tui/panel.go +++ b/internal/tui/panel.go @@ -6,6 +6,18 @@ import ( "github.com/charmbracelet/lipgloss" ) +func (m Model) titledPanelH(title, content string, width, height int, focused bool) string { + s := m.titledPanel(title, content, width, focused) + if height <= 0 { + return s + } + lines := strings.Split(s, "\n") + if len(lines) <= height { + return s + } + return strings.Join(lines[:height-1], "\n") + "\n" + lines[len(lines)-1] +} + func (m Model) titledPanel(title, content string, width int, focused bool) string { borderColor := m.theme.Border titleColor := m.theme.Muted diff --git a/internal/tui/update_test.go b/internal/tui/update_test.go index d98d564..9d8aae0 100644 --- a/internal/tui/update_test.go +++ b/internal/tui/update_test.go @@ -142,7 +142,7 @@ func TestDetailLoad_CachesAndViewDoesNoIO(t *testing.T) { } for i := 0; i < 3; i++ { - _ = m.viewDetailInline(80) + _ = m.viewDetailInline(80, 30) } if ms.stateChangeCalls != 1 { t.Errorf("View performed DB IO: store hit %d times (want 1, from the Cmd only)", ms.stateChangeCalls) diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index 5999fbb..24fd8eb 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -190,8 +190,9 @@ func (m Model) viewMonitorsLayout() string { case detailHistory: title = "History · " + title } - detail := m.viewDetailInline(detailW - 2) - detailPanel := m.zones.Mark("panel-detail", m.titledPanel(title, detail, detailW, m.focusedPanel == panelDetail)) + monHeight := lipgloss.Height(monPanel) + detail := m.viewDetailInline(detailW-2, monHeight) + detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.focusedPanel == panelDetail)) topParts = append(topParts, detailPanel) } diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index dbb4127..22e5834 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -10,23 +10,23 @@ import ( "github.com/charmbracelet/lipgloss" ) -func (m Model) viewDetailInline(width int) string { +func (m Model) viewDetailInline(width, height int) string { if m.cursor >= len(m.sites) { return "" } switch m.detailMode { case detailSLA: - return m.viewSLASidebar(width) + return m.viewSLASidebar(width, height) case detailHistory: - return m.viewHistorySidebar(width) + return m.viewHistorySidebar(width, height) default: site := m.sites[m.cursor] hist, _ := m.engine.GetHistory(site.ID) - return m.viewDetailSidebar(site, hist, width) + return m.viewDetailSidebar(site, hist, width, height) } } -func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width int) string { +func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width, _ int) string { dot := m.st.subtleStyle.Render(" · ") label := m.st.subtleStyle var b strings.Builder @@ -235,7 +235,7 @@ func (m Model) fmtStatusWord(status string) string { } } -func (m Model) viewSLASidebar(width int) string { +func (m Model) viewSLASidebar(width, height int) string { var b strings.Builder label := m.st.subtleStyle innerW := width - 4 @@ -277,7 +277,21 @@ func (m Model) viewSLASidebar(width int) string { if dayBarW < 10 { dayBarW = 10 } - for _, day := range m.slaDailyBreakdown { + slaChrome := 14 + if r.OutageCount > 0 { + slaChrome += 3 + } + maxDaily := len(m.slaDailyBreakdown) + if height > 0 && height-slaChrome < maxDaily { + maxDaily = height - slaChrome + if maxDaily < 1 { + maxDaily = 1 + } + } + for i, day := range m.slaDailyBreakdown { + if i >= maxDaily { + break + } dateStr := day.Date.Format("Jan 02") dayBar := m.uptimeBar(day.UptimePct, dayBarW) pctStr := fmtPct(day.UptimePct) + "%" @@ -309,7 +323,7 @@ func (m Model) viewSLASidebar(width int) string { return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String()) } -func (m Model) viewHistorySidebar(width int) string { +func (m Model) viewHistorySidebar(width, _ int) string { var b strings.Builder label := m.st.subtleStyle innerW := width - 4 -- 2.52.0 From 631f07c242e673e37cf5bde3bae0fe1546979ef9 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 20:36:25 -0400 Subject: [PATCH 2/6] feat(tui): stable detail panel height with scroll support Detail sidebar now maintains consistent height matching the monitors panel across all modes (default, SLA, history). Content scrolls within the fixed frame via mouse wheel or j/k when the detail panel is focused. Scroll offset resets on mode/monitor/period changes. Logs strip stays visible in all states. --- internal/tui/panel.go | 67 ++++++++++++++++++++++++++---- internal/tui/tui.go | 11 ++--- internal/tui/update.go | 32 ++++++++++++++ internal/tui/view_dashboard.go | 2 +- internal/tui/view_detail_inline.go | 18 +------- 5 files changed, 101 insertions(+), 29 deletions(-) diff --git a/internal/tui/panel.go b/internal/tui/panel.go index a8480f1..55a3291 100644 --- a/internal/tui/panel.go +++ b/internal/tui/panel.go @@ -6,16 +6,69 @@ import ( "github.com/charmbracelet/lipgloss" ) -func (m Model) titledPanelH(title, content string, width, height int, focused bool) string { - s := m.titledPanel(title, content, width, focused) +func (m Model) titledPanelH(title, content string, width, height, scrollOffset int, focused bool) string { if height <= 0 { - return s + return m.titledPanel(title, content, width, focused) } - lines := strings.Split(s, "\n") - if len(lines) <= height { - return s + + borderColor := m.theme.Border + titleColor := m.theme.Muted + if focused { + borderColor = m.theme.Accent + titleColor = m.theme.Accent } - return strings.Join(lines[:height-1], "\n") + "\n" + lines[len(lines)-1] + + bc := lipgloss.NewStyle().Foreground(borderColor) + tc := lipgloss.NewStyle().Foreground(titleColor).Bold(true) + + innerW := width - 2 + if innerW < 10 { + innerW = 10 + } + + titleRendered := tc.Render(" " + title + " ") + titleLen := len([]rune(title)) + 2 + fillLen := innerW - titleLen - 1 + if fillLen < 0 { + fillLen = 0 + } + + top := bc.Render("╭─") + titleRendered + bc.Render(strings.Repeat("─", fillLen)+"╮") + bottom := bc.Render("╰" + strings.Repeat("─", innerW) + "╯") + + contentStyle := lipgloss.NewStyle().Width(innerW).MaxWidth(innerW) + inner := contentStyle.Render(content) + contentLines := strings.Split(inner, "\n") + + bodyH := height - 2 + if bodyH < 1 { + bodyH = 1 + } + + if scrollOffset > len(contentLines)-bodyH { + scrollOffset = len(contentLines) - bodyH + } + if scrollOffset < 0 { + scrollOffset = 0 + } + + end := scrollOffset + bodyH + if end > len(contentLines) { + end = len(contentLines) + } + visible := contentLines[scrollOffset:end] + + var lines []string + lines = append(lines, top) + for _, line := range visible { + lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│")) + } + for len(lines) < height-1 { + lines = append(lines, bc.Render("│")+strings.Repeat(" ", innerW)+bc.Render("│")) + } + lines = append(lines, bottom) + + return strings.Join(lines, "\n") } func (m Model) titledPanel(title, content string, width int, focused bool) string { diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 77c2834..d54d23e 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -160,11 +160,12 @@ type Model struct { historySiteName string historySiteID int - slaReport monitor.SLAReport - slaDailyBreakdown []monitor.DayReport - slaSiteName string - slaSiteID int - slaPeriodIdx int + slaReport monitor.SLAReport + slaDailyBreakdown []monitor.DayReport + slaSiteName string + slaSiteID int + slaPeriodIdx int + detailScrollOffset int isAdmin bool zones *zone.Manager diff --git a/internal/tui/update.go b/internal/tui/update.go index 9ddcbb6..97e9183 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -298,6 +298,18 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { return m, nil } + if m.focusedPanel == panelDetail && m.detailOpen { + if msg.Button == tea.MouseButtonWheelUp { + m.detailScrollOffset -= 3 + } else { + m.detailScrollOffset += 3 + } + if m.detailScrollOffset < 0 { + m.detailScrollOffset = 0 + } + return m, nil + } + listLen := m.currentListLen() if msg.Button == tea.MouseButtonWheelUp { if m.cursor > 0 { @@ -588,6 +600,13 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } m.recalcLayout() case "up", "k": + if m.focusedPanel == panelDetail && m.detailOpen { + m.detailScrollOffset-- + if m.detailScrollOffset < 0 { + m.detailScrollOffset = 0 + } + return m, nil + } if m.focusedPanel == panelMaint { m.scrollMaintCursor(-1) return m, nil @@ -604,10 +623,15 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.syncSelectedID() if m.detailOpen && m.cursor < len(m.sites) { m.detailMode = detailDefault + m.detailScrollOffset = 0 return m, m.loadDetailCmd(m.sites[m.cursor].ID) } } case "down", "j": + if m.focusedPanel == panelDetail && m.detailOpen { + m.detailScrollOffset++ + return m, nil + } if m.focusedPanel == panelMaint { m.scrollMaintCursor(1) return m, nil @@ -625,6 +649,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.syncSelectedID() if m.detailOpen && m.cursor < len(m.sites) { m.detailMode = detailDefault + m.detailScrollOffset = 0 return m, m.loadDetailCmd(m.sites[m.cursor].ID) } } @@ -652,6 +677,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if len(m.sites) > 0 { m.detailOpen = !m.detailOpen m.detailMode = detailDefault + m.detailScrollOffset = 0 m.recalcLayout() st := m.store ctx := m.ctx @@ -702,9 +728,11 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.focusedPanel = panelMonitors } else if m.detailOpen && m.detailMode != detailDefault { m.detailMode = detailDefault + m.detailScrollOffset = 0 } else if m.detailOpen { m.detailOpen = false m.detailMode = detailDefault + m.detailScrollOffset = 0 m.recalcLayout() st := m.store ctx := m.ctx @@ -719,6 +747,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.historySiteID = site.ID m.historyChanges = nil m.detailMode = detailHistory + m.detailScrollOffset = 0 return m, m.loadHistoryCmd(site.ID) } case "s": @@ -728,6 +757,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.slaSiteID = site.ID m.slaPeriodIdx = 2 m.detailMode = detailSLA + m.detailScrollOffset = 0 return m, m.loadSLACmd(site.ID, m.slaPeriodIdx) } case "1", "2", "3", "4": @@ -735,6 +765,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { idx := int(msg.String()[0]-'0') - 1 if idx >= 0 && idx < len(slaPeriods) { m.slaPeriodIdx = idx + m.detailScrollOffset = 0 return m, m.loadSLACmd(m.slaSiteID, idx) } } @@ -860,6 +891,7 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) { m.syncSelectedID() m.focusedPanel = panelMonitors if m.detailOpen { + m.detailScrollOffset = 0 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 24fd8eb..6b81089 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -192,7 +192,7 @@ func (m Model) viewMonitorsLayout() string { } monHeight := lipgloss.Height(monPanel) detail := m.viewDetailInline(detailW-2, monHeight) - detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.focusedPanel == panelDetail)) + detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail)) topParts = append(topParts, detailPanel) } diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index 22e5834..ca5cc33 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -235,7 +235,7 @@ func (m Model) fmtStatusWord(status string) string { } } -func (m Model) viewSLASidebar(width, height int) string { +func (m Model) viewSLASidebar(width, _ int) string { var b strings.Builder label := m.st.subtleStyle innerW := width - 4 @@ -277,21 +277,7 @@ func (m Model) viewSLASidebar(width, height int) string { if dayBarW < 10 { dayBarW = 10 } - slaChrome := 14 - if r.OutageCount > 0 { - slaChrome += 3 - } - maxDaily := len(m.slaDailyBreakdown) - if height > 0 && height-slaChrome < maxDaily { - maxDaily = height - slaChrome - if maxDaily < 1 { - maxDaily = 1 - } - } - for i, day := range m.slaDailyBreakdown { - if i >= maxDaily { - break - } + for _, day := range m.slaDailyBreakdown { dateStr := day.Date.Format("Jan 02") dayBar := m.uptimeBar(day.UptimePct, dayBarW) pctStr := fmtPct(day.UptimePct) + "%" -- 2.52.0 From 1d14f640f4014416141334be62a921805f58f36b Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 20:46:43 -0400 Subject: [PATCH 3/6] feat(tui): pin detail panel footer keys at bottom of sidebar Footer keys (period selector, edit/history/SLA shortcuts, Esc) are now pinned at the bottom of the detail panel, always visible regardless of scroll position. Scrollable content fills the space between the header and the pinned footer. --- internal/tui/panel.go | 24 ++++++++++++---- internal/tui/view_dashboard.go | 3 +- internal/tui/view_detail_inline.go | 44 ++++++++++++++++++------------ 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/internal/tui/panel.go b/internal/tui/panel.go index 55a3291..4d90525 100644 --- a/internal/tui/panel.go +++ b/internal/tui/panel.go @@ -6,7 +6,7 @@ import ( "github.com/charmbracelet/lipgloss" ) -func (m Model) titledPanelH(title, content string, width, height, scrollOffset int, focused bool) string { +func (m Model) titledPanelH(title, content, footer string, width, height, scrollOffset int, focused bool) string { if height <= 0 { return m.titledPanel(title, content, width, focused) } @@ -40,7 +40,13 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i inner := contentStyle.Render(content) contentLines := strings.Split(inner, "\n") - bodyH := height - 2 + var footerLines []string + if footer != "" { + footerRendered := contentStyle.Render(footer) + footerLines = strings.Split(footerRendered, "\n") + } + + bodyH := height - 2 - len(footerLines) if bodyH < 1 { bodyH = 1 } @@ -58,13 +64,21 @@ func (m Model) titledPanelH(title, content string, width, height, scrollOffset i } visible := contentLines[scrollOffset:end] + borderLine := func(line string) string { + return bc.Render("│") + line + strings.Repeat(" ", max(0, innerW-lipgloss.Width(line))) + bc.Render("│") + } + emptyLine := borderLine(strings.Repeat(" ", innerW)) + var lines []string lines = append(lines, top) for _, line := range visible { - lines = append(lines, bc.Render("│")+line+strings.Repeat(" ", max(0, innerW-lipgloss.Width(line)))+bc.Render("│")) + lines = append(lines, borderLine(line)) } - for len(lines) < height-1 { - lines = append(lines, bc.Render("│")+strings.Repeat(" ", innerW)+bc.Render("│")) + for len(lines) < height-1-len(footerLines) { + lines = append(lines, emptyLine) + } + for _, line := range footerLines { + lines = append(lines, borderLine(line)) } lines = append(lines, bottom) diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index 6b81089..bdcde86 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -192,7 +192,8 @@ func (m Model) viewMonitorsLayout() string { } monHeight := lipgloss.Height(monPanel) detail := m.viewDetailInline(detailW-2, monHeight) - detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail)) + footer := m.detailFooter(detailW - 2) + detailPanel := m.zones.Mark("panel-detail", m.titledPanelH(title, detail, footer, detailW, monHeight, m.detailScrollOffset, m.focusedPanel == panelDetail)) topParts = append(topParts, detailPanel) } diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index ca5cc33..5c1ab94 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -143,8 +143,6 @@ func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, wid b.WriteString(" " + label.Render("No state changes") + "\n") } - b.WriteString("\n " + m.detailKeys() + "\n") - return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String()) } @@ -216,6 +214,33 @@ func (m Model) detailKeys() string { return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [Esc] Back") } +func (m Model) detailFooter(width int) string { + label := m.st.subtleStyle + var lines []string + + switch m.detailMode { + case detailSLA: + var keys []string + for i, p := range slaPeriods { + k := fmt.Sprintf("[%s] %s", p.key, p.label) + if i == m.slaPeriodIdx { + keys = append(keys, m.st.titleStyle.Render(k)) + } else { + keys = append(keys, label.Render(k)) + } + } + lines = append(lines, " "+strings.Join(keys, " ")) + lines = append(lines, " "+label.Render("[Esc] Back")) + case detailHistory: + lines = append(lines, " "+label.Render("[Esc] Back")) + default: + lines = append(lines, " "+m.detailKeys()) + } + + content := strings.Join(lines, "\n") + return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(content) +} + func (m Model) fmtStatusWord(status string) string { switch status { case "DOWN", "SSL EXP": @@ -292,20 +317,6 @@ func (m Model) viewSLASidebar(width, _ int) string { } } - b.WriteString("\n") - - var keys []string - for i, p := range slaPeriods { - k := fmt.Sprintf("[%s] %s", p.key, p.label) - if i == m.slaPeriodIdx { - keys = append(keys, m.st.titleStyle.Render(k)) - } else { - keys = append(keys, label.Render(k)) - } - } - b.WriteString(" " + strings.Join(keys, " ") + "\n") - b.WriteString(" " + label.Render("[Esc] Back") + "\n") - return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String()) } @@ -368,7 +379,6 @@ func (m Model) viewHistorySidebar(width, _ int) string { statParts = append(statParts, "avg "+fmtDuration(avg)) } b.WriteString(" " + label.Render(strings.Join(statParts, " │ ")) + "\n") - b.WriteString(" " + label.Render("[Esc] Back") + "\n") return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String()) } -- 2.52.0 From 33df597dda97a41d069f7a95b9a840a3ba667d50 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 20:59:04 -0400 Subject: [PATCH 4/6] fix(tui): merge SLA footer into single line Period keys and Esc Back now render on one line instead of two. --- internal/tui/view_detail_inline.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index 5c1ab94..70bffed 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -229,8 +229,8 @@ func (m Model) detailFooter(width int) string { keys = append(keys, label.Render(k)) } } + keys = append(keys, label.Render("[Esc] Back")) lines = append(lines, " "+strings.Join(keys, " ")) - lines = append(lines, " "+label.Render("[Esc] Back")) case detailHistory: lines = append(lines, " "+label.Render("[Esc] Back")) default: -- 2.52.0 From 04cf12f52b7d0c2d5e4b7fc3904533add42f7ace Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 21:07:47 -0400 Subject: [PATCH 5/6] style(tui): match detail panel footer to hotbar styling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Detail panel footer now uses accent keys, muted descriptions, and dot separators — same visual style as the bottom hotbar. --- internal/tui/view_detail_inline.go | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/internal/tui/view_detail_inline.go b/internal/tui/view_detail_inline.go index 70bffed..1d35f30 100644 --- a/internal/tui/view_detail_inline.go +++ b/internal/tui/view_detail_inline.go @@ -210,34 +210,27 @@ func (m Model) detailTypeLine(site models.Site) []string { return parts } -func (m Model) detailKeys() string { - return m.st.subtleStyle.Render("[e] Edit [h] History [s] SLA [Esc] Back") -} - func (m Model) detailFooter(width int) string { - label := m.st.subtleStyle - var lines []string + dot := m.st.subtleStyle.Render(" · ") + var parts []string switch m.detailMode { case detailSLA: - var keys []string for i, p := range slaPeriods { - k := fmt.Sprintf("[%s] %s", p.key, p.label) if i == m.slaPeriodIdx { - keys = append(keys, m.st.titleStyle.Render(k)) + parts = append(parts, m.st.titleStyle.Render(p.key)+" "+m.st.titleStyle.Render(p.label)) } else { - keys = append(keys, label.Render(k)) + parts = append(parts, m.hotkey(p.key, p.label)) } } - keys = append(keys, label.Render("[Esc] Back")) - lines = append(lines, " "+strings.Join(keys, " ")) + parts = append(parts, m.hotkey("Esc", "Back")) case detailHistory: - lines = append(lines, " "+label.Render("[Esc] Back")) + parts = append(parts, m.hotkey("Esc", "Back")) default: - lines = append(lines, " "+m.detailKeys()) + parts = append(parts, m.hotkey("e", "Edit"), m.hotkey("h", "History"), m.hotkey("s", "SLA"), m.hotkey("Esc", "Back")) } - content := strings.Join(lines, "\n") + content := " " + strings.Join(parts, dot) return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(content) } -- 2.52.0 From 6e936ecce35d441ef8a48d84f91539fc14fd9561 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Tue, 30 Jun 2026 21:12:00 -0400 Subject: [PATCH 6/6] style(tui): adjust monitor/detail split to 60/40 Gives monitors panel more room for columns while detail sidebar content stacks vertically and doesn't need the extra width. --- internal/tui/view_dashboard.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index bdcde86..9b33d5e 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -161,7 +161,7 @@ func (m Model) viewMonitorsLayout() string { } remaining := availW - maintW if showDetail { - monW = remaining * 45 / 100 + monW = remaining * 60 / 100 detailW = remaining - monW } else { monW = remaining -- 2.52.0