Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
99121d07d8
|
|||
|
a59edf8410
|
+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}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
type latencyBucket struct {
|
||||
label string
|
||||
min int64
|
||||
max int64
|
||||
count int
|
||||
}
|
||||
|
||||
func (m Model) latencyHistogram(latencies []time.Duration, statuses []bool, width int) string {
|
||||
if len(latencies) == 0 || width < 30 {
|
||||
return ""
|
||||
}
|
||||
|
||||
buckets := []latencyBucket{
|
||||
{"0-50ms", 0, 50, 0},
|
||||
{"50-100ms", 50, 100, 0},
|
||||
{"100-200ms", 100, 200, 0},
|
||||
{"200-500ms", 200, 500, 0},
|
||||
{"500ms+", 500, 999999, 0},
|
||||
}
|
||||
|
||||
for i, l := range latencies {
|
||||
if i < len(statuses) && !statuses[i] {
|
||||
continue
|
||||
}
|
||||
ms := l.Milliseconds()
|
||||
for j := range buckets {
|
||||
if ms >= buckets[j].min && ms < buckets[j].max {
|
||||
buckets[j].count++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxCount := 0
|
||||
for _, b := range buckets {
|
||||
if b.count > maxCount {
|
||||
maxCount = b.count
|
||||
}
|
||||
}
|
||||
if maxCount == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
labelW := 10
|
||||
countW := len(fmt.Sprintf("%d", maxCount))
|
||||
barW := width - labelW - countW - 4
|
||||
if barW < 5 {
|
||||
barW = 5
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
for _, b := range buckets {
|
||||
fill := 0
|
||||
if maxCount > 0 {
|
||||
fill = b.count * barW / maxCount
|
||||
}
|
||||
|
||||
var barColor lipgloss.Style
|
||||
switch {
|
||||
case b.min < 100:
|
||||
barColor = m.st.specialStyle
|
||||
case b.min < 500:
|
||||
barColor = m.st.warnStyle
|
||||
default:
|
||||
barColor = m.st.dangerStyle
|
||||
}
|
||||
|
||||
bar := barColor.Render(strings.Repeat("█", fill))
|
||||
if fill < barW {
|
||||
bar += m.st.subtleStyle.Render(strings.Repeat("░", barW-fill))
|
||||
}
|
||||
|
||||
label := fmt.Sprintf("%*s", labelW, b.label)
|
||||
count := fmt.Sprintf("%*d", countW, b.count)
|
||||
sb.WriteString(" " + m.st.subtleStyle.Render(label) + " " + bar + " " + count + "\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -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,
|
||||
@@ -34,8 +35,9 @@ type tabDataMsg struct {
|
||||
// on entry and refreshed on the tab-data cadence so View never touches the
|
||||
// database.
|
||||
type detailDataMsg struct {
|
||||
siteID int
|
||||
changes []models.StateChange
|
||||
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()
|
||||
}
|
||||
@@ -258,6 +258,15 @@ func (m Model) viewDetailPanel() string {
|
||||
b.WriteString("\n" + m.renderSparkTooltip(site, hist, detailSparkWidth))
|
||||
}
|
||||
|
||||
if site.Type != "push" && len(hist.Latencies) > 5 {
|
||||
histW := m.termWidth - chromePadH - 4
|
||||
if histW < 30 {
|
||||
histW = 30
|
||||
}
|
||||
b.WriteString("\n\n" + m.st.subtleStyle.Render(" RESPONSE TIME DISTRIBUTION") + "\n")
|
||||
b.WriteString(m.latencyHistogram(hist.Latencies, hist.Statuses, histW))
|
||||
}
|
||||
|
||||
b.WriteString("\n")
|
||||
b.WriteString(m.divider() + "\n")
|
||||
b.WriteString(m.st.subtleStyle.Render(" [q/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect"))
|
||||
|
||||
@@ -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