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"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
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.
|
// goroutine. View renders the cached result rather than querying the DB.
|
||||||
func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
|
func (m *Model) loadDetailCmd(siteID int) tea.Cmd {
|
||||||
eng := m.engine
|
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 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"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"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,
|
// tabRefreshTTL bounds how often the DB-backed tab data (alerts, users, nodes,
|
||||||
@@ -36,6 +37,7 @@ type tabDataMsg struct {
|
|||||||
type detailDataMsg struct {
|
type detailDataMsg struct {
|
||||||
siteID int
|
siteID int
|
||||||
changes []models.StateChange
|
changes []models.StateChange
|
||||||
|
dailyDays []monitor.DayReport
|
||||||
}
|
}
|
||||||
|
|
||||||
// historyDataMsg carries the full state-change history for the history view.
|
// historyDataMsg carries the full state-change history for the history view.
|
||||||
|
|||||||
@@ -199,6 +199,7 @@ type Model struct {
|
|||||||
detailOpen bool
|
detailOpen bool
|
||||||
detailChanges []models.StateChange
|
detailChanges []models.StateChange
|
||||||
detailChangesSiteID int
|
detailChangesSiteID int
|
||||||
|
detailDailyDays []monitor.DayReport
|
||||||
|
|
||||||
filterMode bool
|
filterMode bool
|
||||||
filterText string
|
filterText string
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.detailChanges = msg.changes
|
m.detailChanges = msg.changes
|
||||||
m.detailChangesSiteID = msg.siteID
|
m.detailChangesSiteID = msg.siteID
|
||||||
|
m.detailDailyDays = msg.dailyDays
|
||||||
return m, nil
|
return m, nil
|
||||||
case historyDataMsg:
|
case historyDataMsg:
|
||||||
if msg.siteID != m.historySiteID {
|
if msg.siteID != m.historySiteID {
|
||||||
@@ -142,7 +143,7 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const detailInlineHeight = 11
|
const detailInlineHeight = 12
|
||||||
|
|
||||||
func (m *Model) recalcLayout() {
|
func (m *Model) recalcLayout() {
|
||||||
chrome := chromeBase
|
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")
|
keys := m.st.subtleStyle.Render("[h] History [s] SLA [e] Edit [esc] Close")
|
||||||
b.WriteString(" " + keys + "\n")
|
b.WriteString(" " + keys + "\n")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user