diff --git a/internal/tui/tab_logs_sidebar.go b/internal/tui/tab_logs_sidebar.go index 1db54ba..ca5949c 100644 --- a/internal/tui/tab_logs_sidebar.go +++ b/internal/tui/tab_logs_sidebar.go @@ -60,7 +60,7 @@ func (m Model) viewLogsSidebar(width, maxLines int) string { sidebarStyle := lipgloss.NewStyle().Width(width).MaxWidth(width) - var lines []string + var all []string for _, line := range logs { if strings.TrimSpace(line) == "" { continue @@ -68,11 +68,43 @@ func (m Model) viewLogsSidebar(width, maxLines int) string { if m.logFilterImportant && !isImportantLog(classifyLog(line)) { continue } - lines = append(lines, m.renderCompactLogLine(line, width)) - if maxLines > 0 && len(lines) >= maxLines { - break - } + all = append(all, m.renderCompactLogLine(line, width)) } - return sidebarStyle.Render(strings.Join(lines, "\n")) + start := m.logScrollOffset + if start > len(all) { + start = len(all) + } + end := start + maxLines + if end > len(all) { + end = len(all) + } + visible := all[start:end] + + return sidebarStyle.Render(strings.Join(visible, "\n")) +} + +func (m *Model) scrollLogs(delta int) { + logs := m.engine.GetLogs() + total := 0 + for _, line := range logs { + if strings.TrimSpace(line) == "" { + continue + } + if m.logFilterImportant && !isImportantLog(classifyLog(line)) { + continue + } + total++ + } + + m.logScrollOffset += delta + if m.logScrollOffset < 0 { + m.logScrollOffset = 0 + } + if m.logScrollOffset > total-1 { + m.logScrollOffset = total - 1 + } + if m.logScrollOffset < 0 { + m.logScrollOffset = 0 + } } diff --git a/internal/tui/tui.go b/internal/tui/tui.go index dd97daf..7dce56f 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -96,6 +96,12 @@ const ( sectionUsers = 2 ) +const ( + panelMonitors = 0 + panelLogs = 1 + panelDetail = 2 +) + type sessionState int const ( @@ -124,6 +130,8 @@ type Model struct { termWidth int termHeight int contentWidth int + focusedPanel int + logScrollOffset int editID int editToken string diff --git a/internal/tui/update.go b/internal/tui/update.go index efd1795..4dcc996 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -274,6 +274,15 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { return m, nil } + if m.currentTab == tabMonitors && m.focusedPanel == panelLogs { + if msg.Button == tea.MouseButtonWheelUp { + m.scrollLogs(-3) + } else { + m.scrollLogs(3) + } + return m, nil + } + listLen := m.currentListLen() if msg.Button == tea.MouseButtonWheelUp { if m.cursor > 0 { @@ -291,6 +300,9 @@ func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { } } m.syncSelectedID() + if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) { + return m, m.loadDetailCmd(m.sites[m.cursor].ID) + } return m, nil } @@ -526,11 +538,26 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.currentTab == tabSettings { m.switchSettingsSection(m.settingsSection - 1) } - case "right", "l": + case "right": if m.currentTab == tabSettings { m.switchSettingsSection(m.settingsSection + 1) } + case "l": + switch m.currentTab { + case tabSettings: + m.switchSettingsSection(m.settingsSection + 1) + case tabMonitors: + if m.focusedPanel == panelLogs { + m.focusedPanel = panelMonitors + } else { + m.focusedPanel = panelLogs + } + } case "up", "k": + if m.currentTab == tabMonitors && m.focusedPanel == panelLogs { + m.scrollLogs(-1) + return m, nil + } if m.cursor > 0 { m.cursor-- if m.cursor < m.tableOffset { @@ -542,6 +569,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } } case "down", "j": + if m.currentTab == tabMonitors && m.focusedPanel == panelLogs { + m.scrollLogs(1) + return m, nil + } max := m.currentListLen() - 1 if m.cursor < max { m.cursor++ @@ -600,9 +631,13 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.state = stateAlertDetail } case "esc": - if m.currentTab == tabMonitors && m.detailOpen { - m.detailOpen = false - m.recalcLayout() + if m.currentTab == tabMonitors { + if m.focusedPanel != panelMonitors { + m.focusedPanel = panelMonitors + } else if m.detailOpen { + m.detailOpen = false + m.recalcLayout() + } } case "h": if m.detailOpen && m.currentTab == tabMonitors && m.cursor < len(m.sites) { @@ -750,6 +785,18 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) { } } + if m.currentTab == tabMonitors { + if m.zones.Get("panel-monitors").InBounds(msg) { + m.focusedPanel = panelMonitors + } else if m.zones.Get("panel-logs").InBounds(msg) { + m.focusedPanel = panelLogs + return m, nil + } else if m.detailOpen && m.zones.Get("panel-detail").InBounds(msg) { + m.focusedPanel = panelDetail + return m, nil + } + } + prefix, listLen := m.currentZonePrefix() end := m.tableOffset + m.maxTableRows if end > listLen { @@ -759,6 +806,9 @@ 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) + } return m, nil } } diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index d5d1a92..d218b73 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -159,9 +159,9 @@ func (m Model) viewDashboard() string { rightW := availW - leftW m.contentWidth = leftW - 2 monitors := m.viewSitesTab() - monPanel := m.titledPanel("Monitors", monitors, leftW, !m.detailOpen) + monPanel := m.zones.Mark("panel-monitors", m.titledPanel("Monitors", monitors, leftW, m.focusedPanel == panelMonitors)) sidebarContent := m.viewLogsSidebar(rightW-2, m.maxTableRows) - logPanel := m.titledPanel("Logs", sidebarContent, rightW, false) + logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", sidebarContent, rightW, m.focusedPanel == panelLogs)) top := lipgloss.JoinHorizontal(lipgloss.Top, monPanel, logPanel) if m.detailOpen { site := "" @@ -169,7 +169,7 @@ func (m Model) viewDashboard() string { site = m.sites[m.cursor].Name } detail := m.viewDetailInline(availW - 2) - detailPanel := m.titledPanel(site, detail, availW, true) + detailPanel := m.zones.Mark("panel-detail", m.titledPanel(site, detail, availW, m.focusedPanel == panelDetail)) content = top + "\n" + detailPanel } else { content = top @@ -178,14 +178,14 @@ func (m Model) viewDashboard() string { m.contentWidth = m.termWidth - 2 monitors := m.viewSitesTab() availW := m.termWidth - chromePadH - monPanel := m.titledPanel("Monitors", monitors, availW, !m.detailOpen) + monPanel := m.zones.Mark("panel-monitors", m.titledPanel("Monitors", monitors, availW, m.focusedPanel == panelMonitors)) if m.detailOpen { site := "" if m.cursor < len(m.sites) { site = m.sites[m.cursor].Name } detail := m.viewDetailInline(availW - 2) - detailPanel := m.titledPanel(site, detail, availW, true) + detailPanel := m.zones.Mark("panel-detail", m.titledPanel(site, detail, availW, m.focusedPanel == panelDetail)) content = monPanel + "\n" + detailPanel } else { content = monPanel @@ -303,10 +303,12 @@ func (m Model) renderFooter(stats dashboardStats) string { var keys string switch m.currentTab { case tabMonitors: - if m.detailOpen { - keys = "[i]Close [Enter]Expand [h]History [s]SLA [e]Edit [↑/↓]Select [T]Theme [q]Quit" + if m.focusedPanel == panelLogs { + keys = "[↑/↓]Scroll [l/Esc]Back [T]Theme [q]Quit" + } else if m.detailOpen { + keys = "[i]Close [Enter]Expand [h]History [s]SLA [e]Edit [l]Logs [↑/↓]Select [T]Theme [q]Quit" } else { - keys = "[/]Filter [i]Info [Enter]Detail [n]New [e]Edit [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit" + keys = "[/]Filter [i]Info [Enter]Detail [n]New [e]Edit [d]Del [l]Logs [T]Theme [Tab]Switch [q]Quit" } case tabMaint: keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"