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) + "%"