feat(tui): 30-day uptime timeline in inline detail panel
Statuspage-style colored bar — one █ per day, colored by uptime: green (>= 99%), yellow (>= 95%), red (< 95%), gray (no data). 30-day percentage shown to the right. Daily breakdown computed via ComputeDailyBreakdown from state_changes and loaded alongside detail data in loadDetailCmd. Auto-updates on cursor move.
This commit is contained in:
+13
-1
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
)
|
||||
@@ -191,8 +192,19 @@ func (m *Model) loadTabDataCmd() tea.Cmd {
|
||||
// goroutine. View renders the cached result rather than querying the DB.
|
||||
func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
|
||||
eng := m.engine
|
||||
var currentStatus models.Status
|
||||
for _, s := range m.sites {
|
||||
if s.ID == siteID {
|
||||
currentStatus = s.Status
|
||||
break
|
||||
}
|
||||
}
|
||||
return func() tea.Msg {
|
||||
return detailDataMsg{siteID: siteID, changes: eng.GetStateChanges(siteID, 5)}
|
||||
changes := eng.GetStateChanges(siteID, 5)
|
||||
now := time.Now()
|
||||
allChanges := eng.GetStateChangesSince(siteID, now.Add(-30*24*time.Hour))
|
||||
daily := monitor.ComputeDailyBreakdown(allChanges, currentStatus, 30, now)
|
||||
return detailDataMsg{siteID: siteID, changes: changes, dailyDays: daily}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
)
|
||||
|
||||
// tabRefreshTTL bounds how often the DB-backed tab data (alerts, users, nodes,
|
||||
@@ -36,6 +37,7 @@ type tabDataMsg struct {
|
||||
type detailDataMsg struct {
|
||||
siteID int
|
||||
changes []models.StateChange
|
||||
dailyDays []monitor.DayReport
|
||||
}
|
||||
|
||||
// historyDataMsg carries the full state-change history for the history view.
|
||||
|
||||
@@ -199,6 +199,7 @@ type Model struct {
|
||||
detailOpen bool
|
||||
detailChanges []models.StateChange
|
||||
detailChangesSiteID int
|
||||
detailDailyDays []monitor.DayReport
|
||||
|
||||
filterMode bool
|
||||
filterText string
|
||||
|
||||
@@ -28,6 +28,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.detailChanges = msg.changes
|
||||
m.detailChangesSiteID = msg.siteID
|
||||
m.detailDailyDays = msg.dailyDays
|
||||
return m, nil
|
||||
case historyDataMsg:
|
||||
if msg.siteID != m.historySiteID {
|
||||
@@ -142,7 +143,7 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
const detailInlineHeight = 11
|
||||
const detailInlineHeight = 12
|
||||
|
||||
func (m *Model) recalcLayout() {
|
||||
chrome := chromeBase
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
)
|
||||
|
||||
func (m Model) uptimeTimeline(days []monitor.DayReport, width int) string {
|
||||
if len(days) == 0 {
|
||||
return m.st.subtleStyle.Render("No uptime data")
|
||||
}
|
||||
|
||||
maxDays := width - 10
|
||||
if maxDays < 10 {
|
||||
maxDays = 10
|
||||
}
|
||||
|
||||
display := days
|
||||
if len(display) > maxDays {
|
||||
display = display[len(display)-maxDays:]
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, d := range display {
|
||||
ch := "█"
|
||||
switch {
|
||||
case d.UptimePct >= 99.0:
|
||||
sb.WriteString(m.st.specialStyle.Render(ch))
|
||||
case d.UptimePct >= 95.0:
|
||||
sb.WriteString(m.st.warnStyle.Render(ch))
|
||||
case d.UptimePct > 0:
|
||||
sb.WriteString(m.st.dangerStyle.Render(ch))
|
||||
default:
|
||||
sb.WriteString(m.st.subtleStyle.Render("░"))
|
||||
}
|
||||
}
|
||||
|
||||
pct := days[len(days)-1].UptimePct
|
||||
pctStyle := m.st.specialStyle
|
||||
if pct < 99.0 {
|
||||
pctStyle = m.st.dangerStyle
|
||||
} else if pct < 99.9 {
|
||||
pctStyle = m.st.warnStyle
|
||||
}
|
||||
sb.WriteString(" " + pctStyle.Render(fmt.Sprintf("%.2f%%", pct)))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -77,6 +77,14 @@ func (m Model) viewDetailInline(width int) string {
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.detailDailyDays) > 0 && m.detailChangesSiteID == site.ID {
|
||||
timelineW := width - 4
|
||||
if timelineW < 20 {
|
||||
timelineW = 20
|
||||
}
|
||||
b.WriteString(" " + m.st.subtleStyle.Render("30d") + " " + m.uptimeTimeline(m.detailDailyDays, timelineW) + "\n")
|
||||
}
|
||||
|
||||
keys := m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [esc] Close")
|
||||
b.WriteString(" " + keys + "\n")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user