feat(tui): move detail panel from bottom overlay to right side

Restructure dashboard from [Monitors 70% | Logs 30%] to
[Monitors 45% | Detail 55%] master-detail layout. Detail panel
now sits beside the monitor list instead of below it, keeping
dashboard context visible during inspection.

- Detail panel renders in horizontal split (right of monitors)
- Logs sidebar removed; `l` key opens fullscreen logs directly
- recalcLayout no longer subtracts detail height from table rows
- Remove unused renderCompactLogLine/viewLogsSidebar (Phase 4 recreates)
This commit is contained in:
2026-06-30 08:59:17 -04:00
parent 1856820c3e
commit 4321e094a3
3 changed files with 16 additions and 102 deletions
-68
View File
@@ -2,76 +2,8 @@ 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) viewLogsSidebar(width, maxLines int) string {
logs := m.engine.GetLogs()
if len(logs) == 0 {
return m.st.subtleStyle.Render(" No logs yet")
}
sidebarStyle := 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 sidebarStyle.Render(strings.Join(visible, "\n"))
}
func (m *Model) scrollLogs(delta int) {
logs := m.engine.GetLogs()
total := 0
+5 -14
View File
@@ -155,16 +155,11 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
const detailInlineHeight = 12
func (m *Model) recalcLayout() {
chrome := chromeBase
if m.filterMode || m.filterText != "" {
chrome++
}
if m.detailOpen {
chrome += detailInlineHeight
}
m.maxTableRows = m.termHeight - chrome
if m.maxTableRows < 1 {
m.maxTableRows = 1
@@ -259,7 +254,7 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "esc", "q":
m.state = stateDashboard
m.focusedPanel = panelLogs
m.focusedPanel = panelMonitors
case "ctrl+c":
return m, tea.Quit
case "f":
@@ -756,14 +751,10 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.recalcLayout()
}
case "l":
if m.focusedPanel == panelLogs {
m.logsOpen = false
m.focusedPanel = panelMonitors
} else {
m.logsOpen = true
m.focusedPanel = panelLogs
}
m.recalcLayout()
m.refreshLogContent()
m.logViewport.GotoTop()
m.state = stateLogs
return m, nil
case "up", "k":
if m.focusedPanel == panelMaint {
m.scrollMaintCursor(-1)
+11 -20
View File
@@ -156,9 +156,9 @@ func (m Model) viewMonitorsLayout() string {
wide := m.termWidth >= wideBreakpoint
showMaint := m.maintOpen && wide
showLogs := m.logsOpen && wide
showDetail := m.detailOpen && wide
var maintW, logsW, monW int
var maintW, detailW, monW int
if showMaint {
maintW = maintSidebarW
if maintW > availW/4 {
@@ -166,9 +166,9 @@ func (m Model) viewMonitorsLayout() string {
}
}
remaining := availW - maintW
if showLogs {
monW = remaining * 70 / 100
logsW = remaining - monW
if showDetail {
monW = remaining * 45 / 100
detailW = remaining - monW
} else {
monW = remaining
}
@@ -185,24 +185,17 @@ func (m Model) viewMonitorsLayout() string {
topParts = append(topParts, maintPanel)
}
topParts = append(topParts, monPanel)
if showLogs {
logContent := m.viewLogsSidebar(logsW-2, m.maxTableRows)
logPanel := m.zones.Mark("panel-logs", m.titledPanel("Logs", logContent, logsW, m.focusedPanel == panelLogs))
topParts = append(topParts, logPanel)
}
top := lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
if m.detailOpen {
if showDetail {
site := ""
if m.cursor < len(m.sites) {
site = m.sites[m.cursor].Name
}
detail := m.viewDetailInline(availW - 2)
detailPanel := m.zones.Mark("panel-detail", m.titledPanel(site, detail, availW, m.focusedPanel == panelDetail))
return top + "\n" + detailPanel
detail := m.viewDetailInline(detailW - 2)
detailPanel := m.zones.Mark("panel-detail", m.titledPanel(site, detail, detailW, m.focusedPanel == panelDetail))
topParts = append(topParts, detailPanel)
}
return top
return lipgloss.JoinHorizontal(lipgloss.Top, topParts...)
}
func (m Model) viewDashboard() string {
@@ -286,8 +279,6 @@ 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 {
keys = "[i]Close [Enter]Expand [h]History [s]SLA [e]Edit [m]Maint [l]Logs [S]Settings [T]Theme [q]Quit"
} else {