diff --git a/internal/tui/tab_logs_sidebar.go b/internal/tui/tab_logs_sidebar.go index 3d3bfd0..dcd5da5 100644 --- a/internal/tui/tab_logs_sidebar.go +++ b/internal/tui/tab_logs_sidebar.go @@ -2,8 +2,76 @@ package tui import ( "strings" + + "gitea.lerkolabs.com/lerkolabs/uptop/internal/models" + "github.com/charmbracelet/lipgloss" ) +func (m Model) renderCompactLogLine(entry models.LogEntry, maxW int) string { + sev := classifyLog(entry.Message) + + var tag string + switch sev { + case severityDown: + tag = m.st.dangerStyle.Render("▼") + case severityUp: + tag = m.st.specialStyle.Render("▲") + case severityWarn: + tag = m.st.warnStyle.Render("◆") + case severitySystem: + tag = m.st.titleStyle.Render("●") + default: + tag = m.st.subtleStyle.Render("·") + } + + ts := entry.CreatedAt.Local().Format("01/02 15:04") + + msg := entry.Message + msg = strings.TrimPrefix(msg, "Monitor ") + msg = strings.TrimPrefix(msg, "Push ") + + prefixW := len(ts) + 4 + msgW := maxW - prefixW + if msgW < 5 { + msgW = 5 + } + msg = limitStr(msg, msgW) + + return " " + m.st.subtleStyle.Render(ts) + " " + tag + " " + msg +} + +func (m Model) viewLogsStrip(width, maxLines int) string { + logs := m.engine.GetLogs() + if len(logs) == 0 { + return m.st.subtleStyle.Render(" No logs yet") + } + + style := lipgloss.NewStyle().Width(width).MaxWidth(width) + + var all []string + for _, entry := range logs { + if strings.TrimSpace(entry.Message) == "" { + continue + } + if m.logFilterImportant && !isImportantLog(classifyLog(entry.Message)) { + continue + } + all = append(all, m.renderCompactLogLine(entry, width)) + } + + 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 style.Render(strings.Join(visible, "\n")) +} + func (m *Model) scrollLogs(delta int) { logs := m.engine.GetLogs() total := 0 diff --git a/internal/tui/update.go b/internal/tui/update.go index 8f64d04..674744d 100644 --- a/internal/tui/update.go +++ b/internal/tui/update.go @@ -155,11 +155,16 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) { return m, nil } +const logsStripHeight = 6 + func (m *Model) recalcLayout() { chrome := chromeBase if m.filterMode || m.filterText != "" { chrome++ } + if m.logsOpen { + chrome += logsStripHeight + } m.maxTableRows = m.termHeight - chrome if m.maxTableRows < 1 { m.maxTableRows = 1 @@ -751,10 +756,14 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { m.recalcLayout() } case "l": - m.refreshLogContent() - m.logViewport.GotoTop() - m.state = stateLogs - return m, nil + if m.logsOpen { + m.logsOpen = false + m.focusedPanel = panelMonitors + } else { + m.logsOpen = true + m.focusedPanel = panelLogs + } + m.recalcLayout() case "up", "k": if m.focusedPanel == panelMaint { m.scrollMaintCursor(-1) @@ -818,8 +827,23 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { return m, nil } if len(m.sites) > 0 { - m.state = stateDetail - return m, m.loadDetailCmd(m.sites[m.cursor].ID) + site := m.sites[m.cursor] + if site.Type == "group" { + m.collapsed[site.ID] = !m.collapsed[site.ID] + payload := collapsedJSON(m.collapsed) + st := m.store + ctx := m.ctx + m.refreshLive() + return m, writeCmd("Save collapsed groups", func() error { + return st.SetPreference(ctx, "collapsed_groups", payload) + }) + } + if !m.detailOpen { + m.detailOpen = true + m.detailMode = detailDefault + m.recalcLayout() + } + return m, m.loadDetailCmd(site.ID) } case "e": return m.handleEditItem() diff --git a/internal/tui/view_dashboard.go b/internal/tui/view_dashboard.go index fdc0497..8a05234 100644 --- a/internal/tui/view_dashboard.go +++ b/internal/tui/view_dashboard.go @@ -201,7 +201,14 @@ func (m Model) viewMonitorsLayout() string { topParts = append(topParts, detailPanel) } - return lipgloss.JoinHorizontal(lipgloss.Top, topParts...) + top := lipgloss.JoinHorizontal(lipgloss.Top, topParts...) + + if m.logsOpen { + logContent := m.viewLogsStrip(availW-2, logsStripHeight-2) + logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, availW, m.focusedPanel == panelLogs)) + return top + "\n" + logPanel + } + return top } func (m Model) viewDashboard() string { @@ -285,12 +292,14 @@ func (m Model) renderFooter(_ dashboardStats) string { var keys string if m.focusedPanel == panelMaint { keys = "[n]New [x]End [d]Del [Esc]Back [S]Settings [T]Theme [q]Quit" + } else if m.focusedPanel == panelLogs { + keys = "[↑/↓]Scroll [Enter]Expand [l/Esc]Back [S]Settings [T]Theme [q]Quit" } else if m.detailOpen && m.detailMode == detailSLA { - keys = "[1-4]Period [Esc]Back [S]Settings [T]Theme [q]Quit" + keys = "[1-4]Period [Esc]Back [l]Logs [S]Settings [T]Theme [q]Quit" } else if m.detailOpen && m.detailMode == detailHistory { - keys = "[Esc]Back [S]Settings [T]Theme [q]Quit" + keys = "[Esc]Back [l]Logs [S]Settings [T]Theme [q]Quit" } else if m.detailOpen { - keys = "[i]Close [Enter]Expand [h]History [s]SLA [e]Edit [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit" + keys = "[i]Close [h]History [s]SLA [e]Edit [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit" } else { keys = "[/]Filter [i]Info [Enter]Detail [n]New [e]Edit [d]Del [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit" }