feat(tui): master-detail layout with sidebar SLA/history #163

Merged
lerko merged 5 commits from feat/master-detail-layout into main 2026-06-30 13:44:54 +00:00
3 changed files with 111 additions and 10 deletions
Showing only changes of commit 3e02833df4 - Show all commits
+68
View File
@@ -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
+30 -6
View File
@@ -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()
+13 -4
View File
@@ -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"
}