5d362fdbe6
tui.go (1032→164) and tab_sites.go (993→482) violated "small functions" and "testable in isolation" standards. Extracted 6 new files by concern: - format.go: pure formatting functions (fmtLatency, fmtUptime, etc.) - sparkline.go: sparkline rendering (latency, heartbeat, group) - update.go: Update method decomposed into 15 named handlers - view_dashboard.go: View, dashboard composition, tab bar, footer - view_detail.go: site detail panel - data.go: data refresh with extracted sortSitesForDisplay/filterSites Added 17 unit tests for the newly-testable pure functions covering format, sparkline, sort ordering, and filter logic. No behavioral changes — strict move-and-extract refactor.
175 lines
3.8 KiB
Go
175 lines
3.8 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var sparkChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
|
|
|
func latencySparkline(latencies []time.Duration, statuses []bool, width int) string {
|
|
if len(latencies) == 0 {
|
|
return subtleStyle.Render(strings.Repeat("·", width))
|
|
}
|
|
|
|
samples := latencies
|
|
sampledStatuses := statuses
|
|
if len(samples) > width {
|
|
samples = samples[len(samples)-width:]
|
|
if len(sampledStatuses) > width {
|
|
sampledStatuses = sampledStatuses[len(sampledStatuses)-width:]
|
|
}
|
|
}
|
|
|
|
minL, maxL := samples[0], samples[0]
|
|
for _, l := range samples {
|
|
if l < minL {
|
|
minL = l
|
|
}
|
|
if l > maxL {
|
|
maxL = l
|
|
}
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if remaining := width - len(samples); remaining > 0 {
|
|
sb.WriteString(subtleStyle.Render(strings.Repeat("·", remaining)))
|
|
}
|
|
spread := maxL - minL
|
|
for i, l := range samples {
|
|
idx := 0
|
|
if spread > 0 {
|
|
idx = int(float64(l-minL) / float64(spread) * 7)
|
|
if idx > 7 {
|
|
idx = 7
|
|
}
|
|
}
|
|
ch := string(sparkChars[idx])
|
|
isDown := i < len(sampledStatuses) && !sampledStatuses[i]
|
|
if isDown {
|
|
sb.WriteString(dangerStyle.Render(ch))
|
|
} else {
|
|
ms := l.Milliseconds()
|
|
if ms < 200 {
|
|
sb.WriteString(specialStyle.Render(ch))
|
|
} else if ms < 500 {
|
|
sb.WriteString(warnStyle.Render(ch))
|
|
} else {
|
|
sb.WriteString(dangerStyle.Render(ch))
|
|
}
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func heartbeatSparkline(statuses []bool, width int) string {
|
|
if len(statuses) == 0 {
|
|
return subtleStyle.Render(strings.Repeat("·", width))
|
|
}
|
|
|
|
samples := statuses
|
|
if len(samples) > width {
|
|
samples = samples[len(samples)-width:]
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if remaining := width - len(samples); remaining > 0 {
|
|
sb.WriteString(subtleStyle.Render(strings.Repeat("·", remaining)))
|
|
}
|
|
for _, up := range samples {
|
|
if up {
|
|
sb.WriteString(specialStyle.Render("▁"))
|
|
} else {
|
|
sb.WriteString(dangerStyle.Render("█"))
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func (m Model) groupSparkline(groupID int, width int) string {
|
|
allSites := m.engine.GetAllSites()
|
|
var childStatuses [][]bool
|
|
for _, s := range allSites {
|
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
|
hist, _ := m.engine.GetHistory(s.ID)
|
|
if len(hist.Statuses) > 0 {
|
|
childStatuses = append(childStatuses, hist.Statuses)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(childStatuses) == 0 {
|
|
return subtleStyle.Render(strings.Repeat("·", width))
|
|
}
|
|
|
|
maxLen := 0
|
|
for _, s := range childStatuses {
|
|
if len(s) > maxLen {
|
|
maxLen = len(s)
|
|
}
|
|
}
|
|
if maxLen > width {
|
|
maxLen = width
|
|
}
|
|
|
|
aggregated := make([]bool, maxLen)
|
|
for i := 0; i < maxLen; i++ {
|
|
allUp := true
|
|
for _, statuses := range childStatuses {
|
|
idx := len(statuses) - maxLen + i
|
|
if idx >= 0 && !statuses[idx] {
|
|
allUp = false
|
|
break
|
|
}
|
|
}
|
|
aggregated[i] = allUp
|
|
}
|
|
|
|
var sb strings.Builder
|
|
if remaining := width - len(aggregated); remaining > 0 {
|
|
sb.WriteString(subtleStyle.Render(strings.Repeat("·", remaining)))
|
|
}
|
|
for _, up := range aggregated {
|
|
if up {
|
|
sb.WriteString(specialStyle.Render("●"))
|
|
} else {
|
|
sb.WriteString(dangerStyle.Render("●"))
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func (m Model) groupUptime(groupID int) string {
|
|
allSites := m.engine.GetAllSites()
|
|
var allStatuses [][]bool
|
|
for _, s := range allSites {
|
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
|
hist, _ := m.engine.GetHistory(s.ID)
|
|
if len(hist.Statuses) > 0 {
|
|
allStatuses = append(allStatuses, hist.Statuses)
|
|
}
|
|
}
|
|
}
|
|
if len(allStatuses) == 0 {
|
|
return subtleStyle.Render("—")
|
|
}
|
|
total, up := 0, 0
|
|
for _, statuses := range allStatuses {
|
|
for _, s := range statuses {
|
|
total++
|
|
if s {
|
|
up++
|
|
}
|
|
}
|
|
}
|
|
return fmtUptime(func() []bool {
|
|
out := make([]bool, total)
|
|
idx := 0
|
|
for _, statuses := range allStatuses {
|
|
copy(out[idx:], statuses)
|
|
idx += len(statuses)
|
|
}
|
|
return out
|
|
}())
|
|
}
|