Compare commits
2 Commits
bcb2678a20
...
ba75be194d
| Author | SHA1 | Date | |
|---|---|---|---|
| ba75be194d | |||
|
e0cb0adebd
|
+2
-2
@@ -413,7 +413,7 @@ func runServe(args []string) {
|
||||
sshSrv := startSSHServer(*port, s, eng, kc)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
p := tea.NewProgram(tui.InitialModel(true, s, eng), tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
p := tea.NewProgram(tui.InitialModel(true, s, eng, version), tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||
}
|
||||
@@ -449,7 +449,7 @@ func startSSHServer(port int, db store.Store, eng *monitor.Engine, kc *keyCache)
|
||||
}),
|
||||
wish.WithMiddleware(
|
||||
bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
return tui.InitialModel(false, db, eng), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
|
||||
return tui.InitialModel(false, db, eng, version), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2,11 +2,37 @@ package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
func (m Model) dividerWidth() int {
|
||||
w := m.termWidth - chromePadH - 4
|
||||
if w < 40 {
|
||||
w = 40
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
func (m Model) divider() string {
|
||||
return " " + subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
|
||||
}
|
||||
|
||||
func (m Model) emptyState(message, hint string) string {
|
||||
content := message
|
||||
if hint != "" {
|
||||
content += "\n\n" + subtleStyle.Render(hint)
|
||||
}
|
||||
return "\n" + lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(m.theme.Accent).
|
||||
Padding(1, 3).
|
||||
Render(content)
|
||||
}
|
||||
|
||||
func limitStr(text string, max int) string {
|
||||
runes := []rune(text)
|
||||
if len(runes) > max {
|
||||
|
||||
@@ -131,9 +131,9 @@ func (m Model) groupSparkline(groupID int, width int) string {
|
||||
}
|
||||
for _, up := range aggregated {
|
||||
if up {
|
||||
sb.WriteString(specialStyle.Render("●"))
|
||||
sb.WriteString(specialStyle.Render("•"))
|
||||
} else {
|
||||
sb.WriteString(dangerStyle.Render("●"))
|
||||
sb.WriteString(dangerStyle.Render("•"))
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
|
||||
@@ -164,7 +164,7 @@ func fmtAlertLastSent(h monitor.AlertHealth) string {
|
||||
|
||||
func (m Model) viewAlertsTab() string {
|
||||
if len(m.alerts) == 0 {
|
||||
return "\n No alert channels configured. Press [n] to add one."
|
||||
return m.emptyState("No alert channels configured.", "[n] Add your first alert")
|
||||
}
|
||||
|
||||
var headers []string
|
||||
@@ -214,7 +214,8 @@ func (m Model) viewAlertDetailPanel() string {
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n\n")
|
||||
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
|
||||
row := func(label, value string) {
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
||||
@@ -240,12 +241,13 @@ func (m Model) viewAlertDetailPanel() string {
|
||||
row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60)))
|
||||
}
|
||||
|
||||
b.WriteString("\n" + subtleStyle.Render(" CONFIGURATION") + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
b.WriteString(subtleStyle.Render(" CONFIGURATION") + "\n")
|
||||
for k, v := range a.Settings {
|
||||
row(k, v)
|
||||
}
|
||||
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||
|
||||
@@ -85,7 +85,7 @@ func renderLogLine(line string) string {
|
||||
func (m Model) viewLogsTab() string {
|
||||
content := m.logViewport.View()
|
||||
if strings.TrimSpace(content) == "" || content == "Waiting for logs..." {
|
||||
return "\n No log entries yet. Logs appear as monitors run checks."
|
||||
return m.emptyState("No log entries yet.", "Logs appear as monitors run checks")
|
||||
}
|
||||
|
||||
lines := strings.Split(content, "\n")
|
||||
@@ -112,7 +112,7 @@ func (m Model) viewLogsTab() string {
|
||||
}
|
||||
|
||||
header := subtleStyle.Render(fmt.Sprintf(
|
||||
" %d entries [↑/↓] Scroll [PgUp/PgDn] Page [f] Filter: %s", shown, filterLabel))
|
||||
" %d entries Filter: %s", shown, filterLabel))
|
||||
|
||||
if m.logFilterImportant && shown < total {
|
||||
header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
|
||||
|
||||
@@ -93,7 +93,7 @@ func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
||||
|
||||
func (m Model) viewMaintTab() string {
|
||||
if len(m.maintenanceWindows) == 0 {
|
||||
return "\n No maintenance windows or incidents. Press [n] to create one."
|
||||
return m.emptyState("No maintenance windows or incidents.", "[n] Create one")
|
||||
}
|
||||
|
||||
var headers []string
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
func (m Model) viewNodesTab() string {
|
||||
if len(m.nodes) == 0 {
|
||||
return "\n No probe nodes connected."
|
||||
return m.emptyState("No probe nodes connected.", "")
|
||||
}
|
||||
|
||||
var headers []string
|
||||
|
||||
@@ -78,13 +78,13 @@ func (m Model) computeLayout() tableLayout {
|
||||
if nameW < 13 {
|
||||
nameW = 13
|
||||
}
|
||||
if nameW > 40 {
|
||||
nameW = 40
|
||||
if nameW > 35 {
|
||||
nameW = 35
|
||||
}
|
||||
|
||||
sparkW := avail - nameW
|
||||
if sparkW < 10 {
|
||||
sparkW = 10
|
||||
if sparkW < 15 {
|
||||
sparkW = 15
|
||||
}
|
||||
|
||||
widths[1] = nameW
|
||||
@@ -101,16 +101,7 @@ func (m Model) computeLayout() tableLayout {
|
||||
func (m Model) viewSitesTab() string {
|
||||
|
||||
if len(m.sites) == 0 {
|
||||
welcome := lipgloss.NewStyle().
|
||||
Border(lipgloss.RoundedBorder()).
|
||||
BorderForeground(m.theme.Accent).
|
||||
Padding(1, 3).
|
||||
Render(
|
||||
titleStyle.Render("uptop") + "\n\n" +
|
||||
"No monitors configured yet.\n\n" +
|
||||
subtleStyle.Render("[n] Add your first monitor"),
|
||||
)
|
||||
return "\n" + welcome
|
||||
return m.emptyState(titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor")
|
||||
}
|
||||
|
||||
layout := m.computeLayout()
|
||||
|
||||
@@ -29,7 +29,7 @@ func fmtKey(key string) string {
|
||||
|
||||
func (m Model) viewUsersTab() string {
|
||||
if len(m.users) == 0 {
|
||||
return "\n No users configured. Press [n] to add one."
|
||||
return m.emptyState("No users configured.", "[n] Add a user")
|
||||
}
|
||||
|
||||
var headers []string
|
||||
|
||||
@@ -66,6 +66,9 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
||||
if styleOverride != nil {
|
||||
if s := styleOverride(row, col); s != nil {
|
||||
style := *s
|
||||
if row%2 == 1 {
|
||||
style = style.Background(tableZebraStyle.GetBackground())
|
||||
}
|
||||
if isSelected {
|
||||
style = tableSelectedStyle.Foreground(s.GetForeground())
|
||||
}
|
||||
|
||||
+3
-1
@@ -138,9 +138,10 @@ type Model struct {
|
||||
// demoMode renders a stable status dot instead of the animated pulse so
|
||||
// screenshots/recordings don't capture the spinner mid-frame. Set via UPTOP_DEMO=1.
|
||||
demoMode bool
|
||||
version string
|
||||
}
|
||||
|
||||
func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
||||
func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine, version string) Model {
|
||||
vpLogs := viewport.New(100, 20)
|
||||
vpLogs.SetContent("Waiting for logs...")
|
||||
z := zone.New()
|
||||
@@ -172,6 +173,7 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
||||
theme: theme,
|
||||
themeIndex: themeIdx,
|
||||
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
||||
version: version,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
||||
case 1:
|
||||
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
|
||||
case 2:
|
||||
keys = "[f]Filter [T]Theme [Tab]Switch [q]Quit"
|
||||
keys = "[↑/↓]Scroll [PgUp/PgDn]Page [f]Filter [T]Theme [Tab]Switch [q]Quit"
|
||||
case 4:
|
||||
keys = "[n]New [x]End [d]Del [T]Theme [Tab]Switch [q]Quit"
|
||||
case 5:
|
||||
@@ -275,9 +275,10 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
||||
keys = "[T]Theme [Tab]Switch [q]Quit"
|
||||
}
|
||||
|
||||
footer := "\n" + statusLine + " " + subtleStyle.Render(keys)
|
||||
ver := subtleStyle.Render("v" + m.version)
|
||||
footer := "\n" + statusLine + " " + subtleStyle.Render(keys) + " " + ver
|
||||
if m.filterText != "" && m.currentTab == 0 {
|
||||
footer = "\n" + subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + subtleStyle.Render(keys)
|
||||
footer = "\n" + subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + subtleStyle.Render(keys) + " " + ver
|
||||
}
|
||||
return footer
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ func (m Model) viewDetailPanel() string {
|
||||
if breadcrumb == "" {
|
||||
breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name)
|
||||
}
|
||||
b.WriteString(breadcrumb + "\n\n")
|
||||
b.WriteString(breadcrumb + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
|
||||
row := func(label, value string) {
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
||||
@@ -200,7 +201,7 @@ func (m Model) viewDetailPanel() string {
|
||||
b.WriteString(" " + subtleStyle.Render("[h] History") + "\n")
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
const sparkWidth = 40
|
||||
if site.Type == "push" {
|
||||
b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth))
|
||||
@@ -242,7 +243,8 @@ func (m Model) viewDetailPanel() string {
|
||||
}
|
||||
}
|
||||
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [q] Quit"))
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||
|
||||
@@ -153,16 +153,13 @@ func (m Model) viewHistoryPanel() string {
|
||||
header += " " + subtleStyle.Render("[q] Back")
|
||||
b.WriteString(header + "\n")
|
||||
|
||||
divWidth := m.termWidth - chromePadH - 4
|
||||
if divWidth < 40 {
|
||||
divWidth = 40
|
||||
}
|
||||
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
divWidth := m.dividerWidth()
|
||||
b.WriteString(m.divider() + "\n")
|
||||
|
||||
sparkline := stateChangeSparkline(m.historyChanges, divWidth)
|
||||
if sparkline != "" {
|
||||
b.WriteString(" " + sparkline + "\n")
|
||||
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n",
|
||||
@@ -177,7 +174,7 @@ func (m Model) viewHistoryPanel() string {
|
||||
b.WriteString(m.historyViewport.View())
|
||||
}
|
||||
|
||||
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
b.WriteString("\n" + m.divider() + "\n")
|
||||
|
||||
stats := computeHistoryStats(m.historyChanges)
|
||||
parts := []string{fmt.Sprintf("%d events", stats.totalEvents)}
|
||||
|
||||
+12
-18
@@ -27,20 +27,14 @@ func (m Model) viewSLAPanel() string {
|
||||
header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName)
|
||||
header += " " + subtleStyle.Render("[q] Back")
|
||||
b.WriteString(header + "\n")
|
||||
|
||||
divWidth := m.termWidth - chromePadH - 4
|
||||
if divWidth < 40 {
|
||||
divWidth = 40
|
||||
}
|
||||
b.WriteString(" " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
|
||||
period := slaPeriods[m.slaPeriodIdx]
|
||||
b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n")
|
||||
|
||||
r := m.slaReport
|
||||
|
||||
// Uptime bar
|
||||
barWidth := divWidth - 30
|
||||
barWidth := m.dividerWidth() - 30
|
||||
if barWidth < 10 {
|
||||
barWidth = 10
|
||||
}
|
||||
@@ -52,23 +46,23 @@ func (m Model) viewSLAPanel() string {
|
||||
if r.UptimePct < 99.0 {
|
||||
uptimeColor = dangerStyle
|
||||
}
|
||||
fmt.Fprintf(&b, " %-14s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
|
||||
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
|
||||
fmt.Fprintf(&b, " %-14s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
|
||||
fmt.Fprintf(&b, " %-16s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
|
||||
fmt.Fprintf(&b, " %-16s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
|
||||
|
||||
if r.OutageCount > 0 {
|
||||
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
|
||||
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
|
||||
fmt.Fprintf(&b, " %-14s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
|
||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
|
||||
}
|
||||
|
||||
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
b.WriteString("\n" + m.divider() + "\n")
|
||||
|
||||
if len(m.slaDailyBreakdown) > 0 {
|
||||
b.WriteString(m.slaViewport.View())
|
||||
}
|
||||
|
||||
b.WriteString("\n " + subtleStyle.Render(strings.Repeat("─", divWidth)) + "\n")
|
||||
b.WriteString("\n" + m.divider() + "\n")
|
||||
|
||||
var keys []string
|
||||
for i, p := range slaPeriods {
|
||||
@@ -80,7 +74,7 @@ func (m Model) viewSLAPanel() string {
|
||||
}
|
||||
}
|
||||
b.WriteString(" " + strings.Join(keys, " "))
|
||||
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll"))
|
||||
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
|
||||
|
||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||
}
|
||||
@@ -88,7 +82,7 @@ func (m Model) viewSLAPanel() string {
|
||||
func (m Model) buildSLADailyContent() string {
|
||||
var b strings.Builder
|
||||
|
||||
barWidth := m.termWidth - chromePadH - 30
|
||||
barWidth := m.dividerWidth() - 30
|
||||
if barWidth < 10 {
|
||||
barWidth = 10
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user