fix(tui): move theme styles onto the Model to end cross-session races
applyTheme mutated ~18 package-global lipgloss styles while every SSH session's tea.Program read them concurrently from its own goroutine. Pressing T or opening a new connection raced other sessions' View and bled themes across users. Styles now live in an immutable per-Model struct built by newStyles; free formatter helpers that consumed the globals became Model methods.
This commit is contained in:
+31
-31
@@ -18,13 +18,13 @@ func (m Model) dividerWidth() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) divider() string {
|
func (m Model) divider() string {
|
||||||
return " " + subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
|
return " " + m.st.subtleStyle.Render(strings.Repeat("─", m.dividerWidth()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) emptyState(message, hint string) string {
|
func (m Model) emptyState(message, hint string) string {
|
||||||
content := message
|
content := message
|
||||||
if hint != "" {
|
if hint != "" {
|
||||||
content += "\n\n" + subtleStyle.Render(hint)
|
content += "\n\n" + m.st.subtleStyle.Render(hint)
|
||||||
}
|
}
|
||||||
return "\n" + lipgloss.NewStyle().
|
return "\n" + lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
@@ -81,10 +81,10 @@ func typeIcon(siteType string, collapsed bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtLatency(d time.Duration) string {
|
func (m Model) fmtLatency(d time.Duration) string {
|
||||||
ms := d.Milliseconds()
|
ms := d.Milliseconds()
|
||||||
if ms == 0 {
|
if ms == 0 {
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
var s string
|
var s string
|
||||||
if ms < 1000 {
|
if ms < 1000 {
|
||||||
@@ -93,17 +93,17 @@ func fmtLatency(d time.Duration) string {
|
|||||||
s = fmt.Sprintf("%.1fs", float64(ms)/1000)
|
s = fmt.Sprintf("%.1fs", float64(ms)/1000)
|
||||||
}
|
}
|
||||||
if ms < 200 {
|
if ms < 200 {
|
||||||
return specialStyle.Render(s)
|
return m.st.specialStyle.Render(s)
|
||||||
}
|
}
|
||||||
if ms < 500 {
|
if ms < 500 {
|
||||||
return warnStyle.Render(s)
|
return m.st.warnStyle.Render(s)
|
||||||
}
|
}
|
||||||
return dangerStyle.Render(s)
|
return m.st.dangerStyle.Render(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtUptime(statuses []bool) string {
|
func (m Model) fmtUptime(statuses []bool) string {
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
up := 0
|
up := 0
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
@@ -114,70 +114,70 @@ func fmtUptime(statuses []bool) string {
|
|||||||
pct := float64(up) / float64(len(statuses)) * 100
|
pct := float64(up) / float64(len(statuses)) * 100
|
||||||
s := fmt.Sprintf("%.1f%%", pct)
|
s := fmt.Sprintf("%.1f%%", pct)
|
||||||
if pct >= 99 {
|
if pct >= 99 {
|
||||||
return specialStyle.Render(s)
|
return m.st.specialStyle.Render(s)
|
||||||
}
|
}
|
||||||
if pct >= 95 {
|
if pct >= 95 {
|
||||||
return warnStyle.Render(s)
|
return m.st.warnStyle.Render(s)
|
||||||
}
|
}
|
||||||
return dangerStyle.Render(s)
|
return m.st.dangerStyle.Render(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtSSL(site models.Site) string {
|
func (m Model) fmtSSL(site models.Site) string {
|
||||||
if site.Type != "http" || !site.CheckSSL || !site.HasSSL {
|
if site.Type != "http" || !site.CheckSSL || !site.HasSSL {
|
||||||
return subtleStyle.Render("-")
|
return m.st.subtleStyle.Render("-")
|
||||||
}
|
}
|
||||||
days := int(time.Until(site.CertExpiry).Hours() / 24)
|
days := int(time.Until(site.CertExpiry).Hours() / 24)
|
||||||
s := fmt.Sprintf("%dd", days)
|
s := fmt.Sprintf("%dd", days)
|
||||||
if days <= 0 {
|
if days <= 0 {
|
||||||
return dangerStyle.Render("EXPIRED")
|
return m.st.dangerStyle.Render("EXPIRED")
|
||||||
}
|
}
|
||||||
if days <= site.ExpiryThreshold {
|
if days <= site.ExpiryThreshold {
|
||||||
return warnStyle.Render(s)
|
return m.st.warnStyle.Render(s)
|
||||||
}
|
}
|
||||||
return specialStyle.Render(s)
|
return m.st.specialStyle.Render(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtRetries(site models.Site) string {
|
func (m Model) fmtRetries(site models.Site) string {
|
||||||
dispCount := site.FailureCount
|
dispCount := site.FailureCount
|
||||||
if dispCount > site.MaxRetries {
|
if dispCount > site.MaxRetries {
|
||||||
dispCount = site.MaxRetries
|
dispCount = site.MaxRetries
|
||||||
}
|
}
|
||||||
s := fmt.Sprintf("%d/%d", dispCount, site.MaxRetries)
|
s := fmt.Sprintf("%d/%d", dispCount, site.MaxRetries)
|
||||||
if site.Status == "DOWN" {
|
if site.Status == "DOWN" {
|
||||||
return dangerStyle.Render(s)
|
return m.st.dangerStyle.Render(s)
|
||||||
}
|
}
|
||||||
if site.Status == "UP" && site.FailureCount > 0 {
|
if site.Status == "UP" && site.FailureCount > 0 {
|
||||||
return warnStyle.Render(s)
|
return m.st.warnStyle.Render(s)
|
||||||
}
|
}
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtStatus(status string, paused bool, inMaint bool) string {
|
func (m Model) fmtStatus(status string, paused bool, inMaint bool) string {
|
||||||
if paused {
|
if paused {
|
||||||
return warnStyle.Render("◇ PAUSED")
|
return m.st.warnStyle.Render("◇ PAUSED")
|
||||||
}
|
}
|
||||||
if inMaint {
|
if inMaint {
|
||||||
return maintStyle.Render("◼ MAINT")
|
return m.st.maintStyle.Render("◼ MAINT")
|
||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case "DOWN":
|
case "DOWN":
|
||||||
return dangerStyle.Render("▼ DOWN")
|
return m.st.dangerStyle.Render("▼ DOWN")
|
||||||
case "SSL EXP":
|
case "SSL EXP":
|
||||||
return dangerStyle.Render("▼ SSL EXP")
|
return m.st.dangerStyle.Render("▼ SSL EXP")
|
||||||
case "LATE":
|
case "LATE":
|
||||||
return warnStyle.Render("◆ LATE")
|
return m.st.warnStyle.Render("◆ LATE")
|
||||||
case "STALE":
|
case "STALE":
|
||||||
return staleStyle.Render("◆ STALE")
|
return m.st.staleStyle.Render("◆ STALE")
|
||||||
case "PENDING":
|
case "PENDING":
|
||||||
return subtleStyle.Render("○ PENDING")
|
return m.st.subtleStyle.Render("○ PENDING")
|
||||||
default:
|
default:
|
||||||
return specialStyle.Render("▲ " + status)
|
return m.st.specialStyle.Render("▲ " + status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtTimeAgo(t time.Time) string {
|
func (m Model) fmtTimeAgo(t time.Time) string {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return subtleStyle.Render("never")
|
return m.st.subtleStyle.Render("never")
|
||||||
}
|
}
|
||||||
d := time.Since(t)
|
d := time.Since(t)
|
||||||
if d < time.Minute {
|
if d < time.Minute {
|
||||||
|
|||||||
@@ -7,9 +7,8 @@ import (
|
|||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// styledModel carries a default-theme styles instance for render-helper tests.
|
||||||
applyTheme(themeFlexokiDark)
|
var styledModel = Model{st: newStyles(themeFlexokiDark)}
|
||||||
}
|
|
||||||
|
|
||||||
func TestLimitStr(t *testing.T) {
|
func TestLimitStr(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -72,7 +71,7 @@ func TestFmtStatus(t *testing.T) {
|
|||||||
{"DOWN", false, true, "◼ MAINT"},
|
{"DOWN", false, true, "◼ MAINT"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := fmtStatus(tt.status, tt.paused, tt.inMaint)
|
got := styledModel.fmtStatus(tt.status, tt.paused, tt.inMaint)
|
||||||
if !containsPlain(got, tt.wantSub) {
|
if !containsPlain(got, tt.wantSub) {
|
||||||
t.Errorf("fmtStatus(%q, paused=%v, maint=%v): %q missing %q",
|
t.Errorf("fmtStatus(%q, paused=%v, maint=%v): %q missing %q",
|
||||||
tt.status, tt.paused, tt.inMaint, got, tt.wantSub)
|
tt.status, tt.paused, tt.inMaint, got, tt.wantSub)
|
||||||
@@ -136,7 +135,7 @@ func TestFmtUptime(t *testing.T) {
|
|||||||
{"all down", []bool{false, false}, "0.0%"},
|
{"all down", []bool{false, false}, "0.0%"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := fmtUptime(tt.statuses)
|
got := styledModel.fmtUptime(tt.statuses)
|
||||||
if !containsPlain(got, tt.wantSub) {
|
if !containsPlain(got, tt.wantSub) {
|
||||||
t.Errorf("fmtUptime(%s): %q missing %q", tt.name, got, tt.wantSub)
|
t.Errorf("fmtUptime(%s): %q missing %q", tt.name, got, tt.wantSub)
|
||||||
}
|
}
|
||||||
@@ -154,7 +153,7 @@ func TestFmtLatency(t *testing.T) {
|
|||||||
{1500 * time.Millisecond, "1.5s"},
|
{1500 * time.Millisecond, "1.5s"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := fmtLatency(tt.d)
|
got := styledModel.fmtLatency(tt.d)
|
||||||
if !containsPlain(got, tt.wantSub) {
|
if !containsPlain(got, tt.wantSub) {
|
||||||
t.Errorf("fmtLatency(%v): %q missing %q", tt.d, got, tt.wantSub)
|
t.Errorf("fmtLatency(%v): %q missing %q", tt.d, got, tt.wantSub)
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-20
@@ -34,18 +34,18 @@ func withBg(s lipgloss.Style, bg lipgloss.Color) lipgloss.Style {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func latencyStyle(ms int64, bg lipgloss.Color) lipgloss.Style {
|
func (m Model) latencyStyle(ms int64, bg lipgloss.Color) lipgloss.Style {
|
||||||
var hex string
|
var hex string
|
||||||
var t float64
|
var t float64
|
||||||
switch {
|
switch {
|
||||||
case ms < 200:
|
case ms < 200:
|
||||||
hex = sparkSuccess
|
hex = m.st.sparkSuccess
|
||||||
t = float64(ms) / 200
|
t = float64(ms) / 200
|
||||||
case ms < 500:
|
case ms < 500:
|
||||||
hex = sparkWarning
|
hex = m.st.sparkWarning
|
||||||
t = float64(ms-200) / 300
|
t = float64(ms-200) / 300
|
||||||
default:
|
default:
|
||||||
hex = sparkDanger
|
hex = m.st.sparkDanger
|
||||||
t = float64(ms-500) / 1500
|
t = float64(ms-500) / 1500
|
||||||
if t > 1 {
|
if t > 1 {
|
||||||
t = 1
|
t = 1
|
||||||
@@ -55,9 +55,9 @@ func latencyStyle(ms int64, bg lipgloss.Color) lipgloss.Style {
|
|||||||
return withBg(s, bg)
|
return withBg(s, bg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func latencySparkline(latencies []time.Duration, statuses []bool, width int, bg lipgloss.Color) string {
|
func (m Model) latencySparkline(latencies []time.Duration, statuses []bool, width int, bg lipgloss.Color) string {
|
||||||
if len(latencies) == 0 {
|
if len(latencies) == 0 {
|
||||||
return withBg(subtleStyle, bg).Render(strings.Repeat("·", width))
|
return withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", width))
|
||||||
}
|
}
|
||||||
|
|
||||||
samples := latencies
|
samples := latencies
|
||||||
@@ -82,7 +82,7 @@ func latencySparkline(latencies []time.Duration, statuses []bool, width int, bg
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
if remaining := width - len(samples); remaining > 0 {
|
if remaining := width - len(samples); remaining > 0 {
|
||||||
sb.WriteString(withBg(subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
sb.WriteString(withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
||||||
}
|
}
|
||||||
for i, l := range samples {
|
for i, l := range samples {
|
||||||
idx := 0
|
idx := 0
|
||||||
@@ -95,17 +95,17 @@ func latencySparkline(latencies []time.Duration, statuses []bool, width int, bg
|
|||||||
ch := string(sparkChars[idx])
|
ch := string(sparkChars[idx])
|
||||||
isDown := i < len(sampledStatuses) && !sampledStatuses[i]
|
isDown := i < len(sampledStatuses) && !sampledStatuses[i]
|
||||||
if isDown {
|
if isDown {
|
||||||
sb.WriteString(withBg(dangerStyle, bg).Render(ch))
|
sb.WriteString(withBg(m.st.dangerStyle, bg).Render(ch))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(latencyStyle(l.Milliseconds(), bg).Render(ch))
|
sb.WriteString(m.latencyStyle(l.Milliseconds(), bg).Render(ch))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func heartbeatSparkline(statuses []bool, width int, bg lipgloss.Color) string {
|
func (m Model) heartbeatSparkline(statuses []bool, width int, bg lipgloss.Color) string {
|
||||||
if len(statuses) == 0 {
|
if len(statuses) == 0 {
|
||||||
return withBg(subtleStyle, bg).Render(strings.Repeat("·", width))
|
return withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", width))
|
||||||
}
|
}
|
||||||
|
|
||||||
samples := statuses
|
samples := statuses
|
||||||
@@ -115,13 +115,13 @@ func heartbeatSparkline(statuses []bool, width int, bg lipgloss.Color) string {
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
if remaining := width - len(samples); remaining > 0 {
|
if remaining := width - len(samples); remaining > 0 {
|
||||||
sb.WriteString(withBg(subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
sb.WriteString(withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
||||||
}
|
}
|
||||||
for _, up := range samples {
|
for _, up := range samples {
|
||||||
if up {
|
if up {
|
||||||
sb.WriteString(withBg(specialStyle, bg).Render("▁"))
|
sb.WriteString(withBg(m.st.specialStyle, bg).Render("▁"))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(withBg(dangerStyle, bg).Render("█"))
|
sb.WriteString(withBg(m.st.dangerStyle, bg).Render("█"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
@@ -156,7 +156,7 @@ func (m Model) groupSparkline(groupID int, width int, bg lipgloss.Color) string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(childStatuses) == 0 {
|
if len(childStatuses) == 0 {
|
||||||
return withBg(subtleStyle, bg).Render(strings.Repeat("·", width))
|
return withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", width))
|
||||||
}
|
}
|
||||||
|
|
||||||
maxLen := 0
|
maxLen := 0
|
||||||
@@ -184,13 +184,13 @@ func (m Model) groupSparkline(groupID int, width int, bg lipgloss.Color) string
|
|||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
if remaining := width - len(aggregated); remaining > 0 {
|
if remaining := width - len(aggregated); remaining > 0 {
|
||||||
sb.WriteString(withBg(subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
sb.WriteString(withBg(m.st.subtleStyle, bg).Render(strings.Repeat("·", remaining)))
|
||||||
}
|
}
|
||||||
for _, up := range aggregated {
|
for _, up := range aggregated {
|
||||||
if up {
|
if up {
|
||||||
sb.WriteString(withBg(subtleStyle, bg).Render("·"))
|
sb.WriteString(withBg(m.st.subtleStyle, bg).Render("·"))
|
||||||
} else {
|
} else {
|
||||||
sb.WriteString(withBg(dangerStyle, bg).Render("•"))
|
sb.WriteString(withBg(m.st.dangerStyle, bg).Render("•"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
@@ -208,7 +208,7 @@ func (m Model) groupUptime(groupID int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(allStatuses) == 0 {
|
if len(allStatuses) == 0 {
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
total, up := 0, 0
|
total, up := 0, 0
|
||||||
for _, statuses := range allStatuses {
|
for _, statuses := range allStatuses {
|
||||||
@@ -219,7 +219,7 @@ func (m Model) groupUptime(groupID int) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fmtUptime(func() []bool {
|
return m.fmtUptime(func() []bool {
|
||||||
out := make([]bool, total)
|
out := make([]bool, total)
|
||||||
idx := 0
|
idx := 0
|
||||||
for _, statuses := range allStatuses {
|
for _, statuses := range allStatuses {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestLatencySparkline_Empty(t *testing.T) {
|
func TestLatencySparkline_Empty(t *testing.T) {
|
||||||
got := latencySparkline(nil, nil, 10, "")
|
got := styledModel.latencySparkline(nil, nil, 10, "")
|
||||||
if !strings.Contains(got, "··········") {
|
if !strings.Contains(got, "··········") {
|
||||||
t.Errorf("empty sparkline should be dots, got %q", got)
|
t.Errorf("empty sparkline should be dots, got %q", got)
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ func TestLatencySparkline_Empty(t *testing.T) {
|
|||||||
func TestLatencySparkline_SingleValue(t *testing.T) {
|
func TestLatencySparkline_SingleValue(t *testing.T) {
|
||||||
latencies := []time.Duration{100 * time.Millisecond}
|
latencies := []time.Duration{100 * time.Millisecond}
|
||||||
statuses := []bool{true}
|
statuses := []bool{true}
|
||||||
got := latencySparkline(latencies, statuses, 5, "")
|
got := styledModel.latencySparkline(latencies, statuses, 5, "")
|
||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
t.Error("sparkline should not be empty")
|
t.Error("sparkline should not be empty")
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func TestLatencySparkline_WidthTruncation(t *testing.T) {
|
|||||||
latencies[i] = time.Duration(i*50) * time.Millisecond
|
latencies[i] = time.Duration(i*50) * time.Millisecond
|
||||||
statuses[i] = true
|
statuses[i] = true
|
||||||
}
|
}
|
||||||
got := latencySparkline(latencies, statuses, 5, "")
|
got := styledModel.latencySparkline(latencies, statuses, 5, "")
|
||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
t.Error("sparkline should not be empty")
|
t.Error("sparkline should not be empty")
|
||||||
}
|
}
|
||||||
@@ -45,7 +45,7 @@ func TestLatencySparkline_WidthTruncation(t *testing.T) {
|
|||||||
func TestLatencySparkline_RelativeHeight(t *testing.T) {
|
func TestLatencySparkline_RelativeHeight(t *testing.T) {
|
||||||
latencies := []time.Duration{10 * time.Millisecond, 50 * time.Millisecond, 10 * time.Millisecond}
|
latencies := []time.Duration{10 * time.Millisecond, 50 * time.Millisecond, 10 * time.Millisecond}
|
||||||
statuses := []bool{true, true, true}
|
statuses := []bool{true, true, true}
|
||||||
out := stripANSI(latencySparkline(latencies, statuses, 3, ""))
|
out := stripANSI(styledModel.latencySparkline(latencies, statuses, 3, ""))
|
||||||
runes := []rune(out)
|
runes := []rune(out)
|
||||||
if len(runes) < 3 {
|
if len(runes) < 3 {
|
||||||
t.Fatalf("expected 3 runes, got %d", len(runes))
|
t.Fatalf("expected 3 runes, got %d", len(runes))
|
||||||
@@ -56,18 +56,15 @@ func TestLatencySparkline_RelativeHeight(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLatencyStyle_BandsProduceDifferentColors(t *testing.T) {
|
func TestLatencyStyle_BandsProduceDifferentColors(t *testing.T) {
|
||||||
sparkSuccess = "#00ff00"
|
st := newStyles(themeFlexokiDark)
|
||||||
sparkWarning = "#ffff00"
|
st.sparkSuccess = "#00ff00"
|
||||||
sparkDanger = "#ff0000"
|
st.sparkWarning = "#ffff00"
|
||||||
defer func() {
|
st.sparkDanger = "#ff0000"
|
||||||
sparkSuccess = ""
|
m := Model{st: st}
|
||||||
sparkWarning = ""
|
|
||||||
sparkDanger = ""
|
|
||||||
}()
|
|
||||||
|
|
||||||
green := latencyStyle(50, "")
|
green := m.latencyStyle(50, "")
|
||||||
yellow := latencyStyle(300, "")
|
yellow := m.latencyStyle(300, "")
|
||||||
red := latencyStyle(800, "")
|
red := m.latencyStyle(800, "")
|
||||||
|
|
||||||
gfg := green.GetForeground()
|
gfg := green.GetForeground()
|
||||||
yfg := yellow.GetForeground()
|
yfg := yellow.GetForeground()
|
||||||
@@ -79,11 +76,12 @@ func TestLatencyStyle_BandsProduceDifferentColors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLatencyStyle_BrightnessVariesWithinBand(t *testing.T) {
|
func TestLatencyStyle_BrightnessVariesWithinBand(t *testing.T) {
|
||||||
sparkSuccess = "#00ff00"
|
st := newStyles(themeFlexokiDark)
|
||||||
defer func() { sparkSuccess = "" }()
|
st.sparkSuccess = "#00ff00"
|
||||||
|
m := Model{st: st}
|
||||||
|
|
||||||
dim := latencyStyle(10, "")
|
dim := m.latencyStyle(10, "")
|
||||||
bright := latencyStyle(190, "")
|
bright := m.latencyStyle(190, "")
|
||||||
|
|
||||||
if dim.GetForeground() == bright.GetForeground() {
|
if dim.GetForeground() == bright.GetForeground() {
|
||||||
t.Error("10ms and 190ms should have different brightness within green band")
|
t.Error("10ms and 190ms should have different brightness within green band")
|
||||||
@@ -93,7 +91,7 @@ func TestLatencyStyle_BrightnessVariesWithinBand(t *testing.T) {
|
|||||||
func TestLatencySparkline_OutputWidth(t *testing.T) {
|
func TestLatencySparkline_OutputWidth(t *testing.T) {
|
||||||
latencies := []time.Duration{100 * time.Millisecond, 200 * time.Millisecond, 300 * time.Millisecond}
|
latencies := []time.Duration{100 * time.Millisecond, 200 * time.Millisecond, 300 * time.Millisecond}
|
||||||
statuses := []bool{true, true, true}
|
statuses := []bool{true, true, true}
|
||||||
got := latencySparkline(latencies, statuses, 5, "")
|
got := styledModel.latencySparkline(latencies, statuses, 5, "")
|
||||||
count := utf8.RuneCountInString(stripANSI(got))
|
count := utf8.RuneCountInString(stripANSI(got))
|
||||||
if count != 5 {
|
if count != 5 {
|
||||||
t.Errorf("expected 5 rune-width output, got %d from %q", count, got)
|
t.Errorf("expected 5 rune-width output, got %d from %q", count, got)
|
||||||
@@ -118,7 +116,7 @@ func stripANSI(s string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHeartbeatSparkline_Empty(t *testing.T) {
|
func TestHeartbeatSparkline_Empty(t *testing.T) {
|
||||||
got := heartbeatSparkline(nil, 10, "")
|
got := styledModel.heartbeatSparkline(nil, 10, "")
|
||||||
if !strings.Contains(got, "··········") {
|
if !strings.Contains(got, "··········") {
|
||||||
t.Errorf("empty heartbeat should be dots, got %q", got)
|
t.Errorf("empty heartbeat should be dots, got %q", got)
|
||||||
}
|
}
|
||||||
@@ -126,7 +124,7 @@ func TestHeartbeatSparkline_Empty(t *testing.T) {
|
|||||||
|
|
||||||
func TestHeartbeatSparkline_Mixed(t *testing.T) {
|
func TestHeartbeatSparkline_Mixed(t *testing.T) {
|
||||||
statuses := []bool{true, false, true, true, false}
|
statuses := []bool{true, false, true, true, false}
|
||||||
got := heartbeatSparkline(statuses, 5, "")
|
got := styledModel.heartbeatSparkline(statuses, 5, "")
|
||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
t.Error("heartbeat sparkline should not be empty")
|
t.Error("heartbeat sparkline should not be empty")
|
||||||
}
|
}
|
||||||
@@ -134,7 +132,7 @@ func TestHeartbeatSparkline_Mixed(t *testing.T) {
|
|||||||
|
|
||||||
func TestHeartbeatSparkline_PaddedWidth(t *testing.T) {
|
func TestHeartbeatSparkline_PaddedWidth(t *testing.T) {
|
||||||
statuses := []bool{true, true}
|
statuses := []bool{true, true}
|
||||||
got := heartbeatSparkline(statuses, 5, "")
|
got := styledModel.heartbeatSparkline(statuses, 5, "")
|
||||||
if !strings.Contains(got, "···") {
|
if !strings.Contains(got, "···") {
|
||||||
t.Errorf("should have dot padding for width > data, got %q", got)
|
t.Errorf("should have dot padding for width > data, got %q", got)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-27
@@ -71,7 +71,7 @@ func fmtAlertType(t string) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtAlertConfig(alert struct {
|
func (m Model) fmtAlertConfig(alert struct {
|
||||||
Type string
|
Type string
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}) string {
|
}) string {
|
||||||
@@ -85,34 +85,34 @@ func fmtAlertConfig(alert struct {
|
|||||||
if host != "" {
|
if host != "" {
|
||||||
return limitStr(host, 34)
|
return limitStr(host, 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "ntfy":
|
case "ntfy":
|
||||||
topic := alert.Settings["topic"]
|
topic := alert.Settings["topic"]
|
||||||
url := alert.Settings["url"]
|
url := alert.Settings["url"]
|
||||||
if url != "" && topic != "" {
|
if url != "" && topic != "" {
|
||||||
return limitStr(fmt.Sprintf("%s/%s", url, topic), 34)
|
return limitStr(fmt.Sprintf("%s/%s", url, topic), 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "telegram":
|
case "telegram":
|
||||||
if id := alert.Settings["chat_id"]; id != "" {
|
if id := alert.Settings["chat_id"]; id != "" {
|
||||||
return limitStr(fmt.Sprintf("chat:%s", id), 34)
|
return limitStr(fmt.Sprintf("chat:%s", id), 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "pagerduty":
|
case "pagerduty":
|
||||||
if key := alert.Settings["routing_key"]; key != "" {
|
if key := alert.Settings["routing_key"]; key != "" {
|
||||||
return limitStr(key, 34)
|
return limitStr(key, 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "pushover":
|
case "pushover":
|
||||||
if user := alert.Settings["user"]; user != "" {
|
if user := alert.Settings["user"]; user != "" {
|
||||||
return limitStr(fmt.Sprintf("user:%s", user), 34)
|
return limitStr(fmt.Sprintf("user:%s", user), 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "gotify":
|
case "gotify":
|
||||||
if url := alert.Settings["url"]; url != "" {
|
if url := alert.Settings["url"]; url != "" {
|
||||||
return limitStr(url, 34)
|
return limitStr(url, 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
case "opsgenie":
|
case "opsgenie":
|
||||||
key := alert.Settings["api_key"]
|
key := alert.Settings["api_key"]
|
||||||
if key != "" {
|
if key != "" {
|
||||||
@@ -125,27 +125,27 @@ func fmtAlertConfig(alert struct {
|
|||||||
}
|
}
|
||||||
return limitStr(masked, 34)
|
return limitStr(masked, 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
default:
|
default:
|
||||||
if val, ok := alert.Settings["url"]; ok {
|
if val, ok := alert.Settings["url"]; ok {
|
||||||
return limitStr(val, 34)
|
return limitStr(val, 34)
|
||||||
}
|
}
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtAlertHealth(h monitor.AlertHealth) string {
|
func (m Model) fmtAlertHealth(h monitor.AlertHealth) string {
|
||||||
if h.LastSendAt.IsZero() {
|
if h.LastSendAt.IsZero() {
|
||||||
return subtleStyle.Render("●")
|
return m.st.subtleStyle.Render("●")
|
||||||
}
|
}
|
||||||
if h.LastSendOK {
|
if h.LastSendOK {
|
||||||
return specialStyle.Render("●")
|
return m.st.specialStyle.Render("●")
|
||||||
}
|
}
|
||||||
return dangerStyle.Render("●")
|
return m.st.dangerStyle.Render("●")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtAlertLastSent(h monitor.AlertHealth) string {
|
func (m Model) fmtAlertLastSent(h monitor.AlertHealth) string {
|
||||||
return fmtTimeAgo(h.LastSendAt)
|
return m.fmtTimeAgo(h.LastSendAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewAlertsTab() string {
|
func (m Model) viewAlertsTab() string {
|
||||||
@@ -175,14 +175,14 @@ func (m Model) viewAlertsTab() string {
|
|||||||
h := m.engine.GetAlertHealth(a.ID)
|
h := m.engine.GetAlertHealth(a.ID)
|
||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
fmt.Sprintf("%d", i+1),
|
fmt.Sprintf("%d", i+1),
|
||||||
fmtAlertHealth(h),
|
m.fmtAlertHealth(h),
|
||||||
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
m.zones.Mark(fmt.Sprintf("alert-%d", i), limitStr(a.Name, nameW-2)),
|
||||||
fmtAlertType(a.Type),
|
fmtAlertType(a.Type),
|
||||||
limitStr(fmtAlertConfig(struct {
|
limitStr(m.fmtAlertConfig(struct {
|
||||||
Type string
|
Type string
|
||||||
Settings map[string]string
|
Settings map[string]string
|
||||||
}{a.Type, a.Settings}), cfgW-2),
|
}{a.Type, a.Settings}), cfgW-2),
|
||||||
fmtAlertLastSent(h),
|
m.fmtAlertLastSent(h),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
@@ -200,41 +200,41 @@ func (m Model) viewAlertDetailPanel() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
b.WriteString(subtleStyle.Render(" Alerts > ") + titleStyle.Render(a.Name) + "\n")
|
b.WriteString(m.st.subtleStyle.Render(" Alerts > ") + m.st.titleStyle.Render(a.Name) + "\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
|
|
||||||
row := func(label, value string) {
|
row := func(label, value string) {
|
||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render(label), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
row("Type", fmtAlertType(a.Type))
|
row("Type", fmtAlertType(a.Type))
|
||||||
|
|
||||||
if h.LastSendAt.IsZero() {
|
if h.LastSendAt.IsZero() {
|
||||||
row("Health", subtleStyle.Render("never sent"))
|
row("Health", m.st.subtleStyle.Render("never sent"))
|
||||||
} else if h.LastSendOK {
|
} else if h.LastSendOK {
|
||||||
row("Health", specialStyle.Render("OK"))
|
row("Health", m.st.specialStyle.Render("OK"))
|
||||||
} else {
|
} else {
|
||||||
row("Health", dangerStyle.Render("FAILED"))
|
row("Health", m.st.dangerStyle.Render("FAILED"))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !h.LastSendAt.IsZero() {
|
if !h.LastSendAt.IsZero() {
|
||||||
row("Last Sent", h.LastSendAt.Format("2006-01-02 15:04:05")+" ("+fmtAlertLastSent(h)+")")
|
row("Last Sent", h.LastSendAt.Format("2006-01-02 15:04:05")+" ("+m.fmtAlertLastSent(h)+")")
|
||||||
}
|
}
|
||||||
if h.SendCount > 0 {
|
if h.SendCount > 0 {
|
||||||
row("Sends", fmt.Sprintf("%d sent, %d failed", h.SendCount, h.FailCount))
|
row("Sends", fmt.Sprintf("%d sent, %d failed", h.SendCount, h.FailCount))
|
||||||
}
|
}
|
||||||
if h.LastError != "" {
|
if h.LastError != "" {
|
||||||
row("Last Error", dangerStyle.Render(limitStr(h.LastError, 60)))
|
row("Last Error", m.st.dangerStyle.Render(limitStr(h.LastError, 60)))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
b.WriteString(subtleStyle.Render(" CONFIGURATION") + "\n")
|
b.WriteString(m.st.subtleStyle.Render(" CONFIGURATION") + "\n")
|
||||||
for k, v := range a.Settings {
|
for k, v := range a.Settings {
|
||||||
row(k, v)
|
row(k, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
|
b.WriteString(m.st.subtleStyle.Render(" [i/Esc] Back [e] Edit [t] Test [q] Quit"))
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-12
@@ -48,30 +48,30 @@ func isImportantLog(sev logSeverity) bool {
|
|||||||
return sev == severityDown || sev == severityUp || sev == severitySystem
|
return sev == severityDown || sev == severityUp || sev == severitySystem
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderLogTag(sev logSeverity) string {
|
func (m Model) renderLogTag(sev logSeverity) string {
|
||||||
switch sev {
|
switch sev {
|
||||||
case severityDown:
|
case severityDown:
|
||||||
return dangerStyle.Render(" DOWN ")
|
return m.st.dangerStyle.Render(" DOWN ")
|
||||||
case severityUp:
|
case severityUp:
|
||||||
return specialStyle.Render(" UP ")
|
return m.st.specialStyle.Render(" UP ")
|
||||||
case severityWarn:
|
case severityWarn:
|
||||||
return warnStyle.Render(" WARN ")
|
return m.st.warnStyle.Render(" WARN ")
|
||||||
case severitySystem:
|
case severitySystem:
|
||||||
return titleStyle.Render(" SYS ")
|
return m.st.titleStyle.Render(" SYS ")
|
||||||
default:
|
default:
|
||||||
return subtleStyle.Render(" info ")
|
return m.st.subtleStyle.Render(" info ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderLogLine(line string) string {
|
func (m Model) renderLogLine(line string) string {
|
||||||
sev := classifyLog(line)
|
sev := classifyLog(line)
|
||||||
tag := renderLogTag(sev)
|
tag := m.renderLogTag(sev)
|
||||||
|
|
||||||
ts := ""
|
ts := ""
|
||||||
msg := line
|
msg := line
|
||||||
if len(line) > 10 && line[0] == '[' {
|
if len(line) > 10 && line[0] == '[' {
|
||||||
if idx := strings.Index(line, "]"); idx > 0 && idx < 12 {
|
if idx := strings.Index(line, "]"); idx > 0 && idx < 12 {
|
||||||
ts = subtleStyle.Render(line[1:idx])
|
ts = m.st.subtleStyle.Render(line[1:idx])
|
||||||
msg = strings.TrimSpace(line[idx+1:])
|
msg = strings.TrimSpace(line[idx+1:])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,7 +103,7 @@ func (m Model) viewLogsTab() string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
shown++
|
shown++
|
||||||
rendered = append(rendered, renderLogLine(line))
|
rendered = append(rendered, m.renderLogLine(line))
|
||||||
}
|
}
|
||||||
|
|
||||||
filterLabel := "All"
|
filterLabel := "All"
|
||||||
@@ -111,11 +111,11 @@ func (m Model) viewLogsTab() string {
|
|||||||
filterLabel = "Important"
|
filterLabel = "Important"
|
||||||
}
|
}
|
||||||
|
|
||||||
header := subtleStyle.Render(fmt.Sprintf(
|
header := m.st.subtleStyle.Render(fmt.Sprintf(
|
||||||
" %d entries Filter: %s", shown, filterLabel))
|
" %d entries Filter: %s", shown, filterLabel))
|
||||||
|
|
||||||
if m.logFilterImportant && shown < total {
|
if m.logFilterImportant && shown < total {
|
||||||
header += subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
|
header += m.st.subtleStyle.Render(fmt.Sprintf(" (%d hidden)", total-shown))
|
||||||
}
|
}
|
||||||
|
|
||||||
m.logViewport.SetContent(strings.Join(rendered, "\n"))
|
m.logViewport.SetContent(strings.Join(rendered, "\n"))
|
||||||
|
|||||||
+13
-16
@@ -9,11 +9,8 @@ import (
|
|||||||
|
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
"github.com/charmbracelet/huh"
|
"github.com/charmbracelet/huh"
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var maintStyle lipgloss.Style
|
|
||||||
|
|
||||||
type maintFormData struct {
|
type maintFormData struct {
|
||||||
Title string
|
Title string
|
||||||
Description string
|
Description string
|
||||||
@@ -23,22 +20,22 @@ type maintFormData struct {
|
|||||||
CustomHours string
|
CustomHours string
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintStatus(mw models.MaintenanceWindow) string {
|
func (m Model) fmtMaintStatus(mw models.MaintenanceWindow) string {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if mw.StartTime.After(now) {
|
if mw.StartTime.After(now) {
|
||||||
return warnStyle.Render("SCHEDULED")
|
return m.st.warnStyle.Render("SCHEDULED")
|
||||||
}
|
}
|
||||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||||
return subtleStyle.Render("ENDED")
|
return m.st.subtleStyle.Render("ENDED")
|
||||||
}
|
}
|
||||||
return specialStyle.Render("ACTIVE")
|
return m.st.specialStyle.Render("ACTIVE")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintType(t string) string {
|
func (m Model) fmtMaintType(t string) string {
|
||||||
if t == "incident" {
|
if t == "incident" {
|
||||||
return dangerStyle.Render("incident")
|
return m.st.dangerStyle.Render("incident")
|
||||||
}
|
}
|
||||||
return maintStyle.Render("maintenance")
|
return m.st.maintStyle.Render("maintenance")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintMonitorW(monitorID int, sites []models.Site, maxW int) string {
|
func fmtMaintMonitorW(monitorID int, sites []models.Site, maxW int) string {
|
||||||
@@ -53,9 +50,9 @@ func fmtMaintMonitorW(monitorID int, sites []models.Site, maxW int) string {
|
|||||||
return fmt.Sprintf("#%d", monitorID)
|
return fmt.Sprintf("#%d", monitorID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtMaintTime(t time.Time, colW int) string {
|
func (m Model) fmtMaintTime(t time.Time, colW int) string {
|
||||||
if t.IsZero() {
|
if t.IsZero() {
|
||||||
return subtleStyle.Render("—")
|
return m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
||||||
@@ -120,11 +117,11 @@ func (m Model) viewMaintTab() string {
|
|||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
strconv.Itoa(i + 1),
|
strconv.Itoa(i + 1),
|
||||||
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, titleW-2)),
|
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, titleW-2)),
|
||||||
fmtMaintType(mw.Type),
|
m.fmtMaintType(mw.Type),
|
||||||
fmtMaintMonitorW(mw.MonitorID, allSites, monW-2),
|
fmtMaintMonitorW(mw.MonitorID, allSites, monW-2),
|
||||||
fmtMaintStatus(mw),
|
m.fmtMaintStatus(mw),
|
||||||
fmtMaintTime(mw.StartTime, timeW),
|
m.fmtMaintTime(mw.StartTime, timeW),
|
||||||
fmtMaintTime(mw.EndTime, timeW),
|
m.fmtMaintTime(mw.EndTime, timeW),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
|
|||||||
+11
-11
@@ -33,14 +33,14 @@ func (m Model) viewNodesTab() string {
|
|||||||
}
|
}
|
||||||
region := node.Region
|
region := node.Region
|
||||||
if region == "" {
|
if region == "" {
|
||||||
region = subtleStyle.Render("—")
|
region = m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
lastSeen := fmtNodeLastSeen(node.LastSeen)
|
lastSeen := m.fmtNodeLastSeen(node.LastSeen)
|
||||||
version := node.Version
|
version := node.Version
|
||||||
if version == "" {
|
if version == "" {
|
||||||
version = subtleStyle.Render("—")
|
version = m.st.subtleStyle.Render("—")
|
||||||
}
|
}
|
||||||
status := fmtNodeStatus(node.LastSeen)
|
status := m.fmtNodeStatus(node.LastSeen)
|
||||||
rows = append(rows, []string{name, region, lastSeen, version, status})
|
rows = append(rows, []string{name, region, lastSeen, version, status})
|
||||||
}
|
}
|
||||||
return rows
|
return rows
|
||||||
@@ -50,20 +50,20 @@ func (m Model) viewNodesTab() string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtNodeStatus(lastSeen time.Time) string {
|
func (m Model) fmtNodeStatus(lastSeen time.Time) string {
|
||||||
if lastSeen.IsZero() {
|
if lastSeen.IsZero() {
|
||||||
return subtleStyle.Render("UNKNOWN")
|
return m.st.subtleStyle.Render("UNKNOWN")
|
||||||
}
|
}
|
||||||
ago := time.Since(lastSeen)
|
ago := time.Since(lastSeen)
|
||||||
if ago < 60*time.Second {
|
if ago < 60*time.Second {
|
||||||
return specialStyle.Render("ONLINE")
|
return m.st.specialStyle.Render("ONLINE")
|
||||||
}
|
}
|
||||||
if ago < 5*time.Minute {
|
if ago < 5*time.Minute {
|
||||||
return warnStyle.Render("STALE")
|
return m.st.warnStyle.Render("STALE")
|
||||||
}
|
}
|
||||||
return dangerStyle.Render("OFFLINE")
|
return m.st.dangerStyle.Render("OFFLINE")
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtNodeLastSeen(t time.Time) string {
|
func (m Model) fmtNodeLastSeen(t time.Time) string {
|
||||||
return fmtTimeAgo(t)
|
return m.fmtTimeAgo(t)
|
||||||
}
|
}
|
||||||
|
|||||||
+14
-16
@@ -11,8 +11,6 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
var siteGroupStyle lipgloss.Style
|
|
||||||
|
|
||||||
type siteFormData struct {
|
type siteFormData struct {
|
||||||
Name string
|
Name string
|
||||||
SiteType string
|
SiteType string
|
||||||
@@ -182,7 +180,7 @@ func pickCols(active []colKey, allCells map[colKey]string) []string {
|
|||||||
func (m Model) viewSitesTab() string {
|
func (m Model) viewSitesTab() string {
|
||||||
|
|
||||||
if len(m.sites) == 0 {
|
if len(m.sites) == 0 {
|
||||||
return m.emptyState(titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor")
|
return m.emptyState(m.st.titleStyle.Render("uptop")+"\n\nNo monitors configured yet.", "[n] Add your first monitor")
|
||||||
}
|
}
|
||||||
|
|
||||||
layout := m.computeLayout()
|
layout := m.computeLayout()
|
||||||
@@ -219,12 +217,12 @@ func (m Model) viewSitesTab() string {
|
|||||||
colNum: strconv.Itoa(i + 1),
|
colNum: strconv.Itoa(i + 1),
|
||||||
colName: m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-4)),
|
colName: m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-4)),
|
||||||
colType: "group",
|
colType: "group",
|
||||||
colStatus: fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
colStatus: m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||||
colLatency: subtleStyle.Render("—"),
|
colLatency: m.st.subtleStyle.Render("—"),
|
||||||
colUptime: m.groupUptime(site.ID),
|
colUptime: m.groupUptime(site.ID),
|
||||||
colHistory: m.groupSparkline(site.ID, sparkWidth, rowBg),
|
colHistory: m.groupSparkline(site.ID, sparkWidth, rowBg),
|
||||||
colSSL: subtleStyle.Render("-"),
|
colSSL: m.st.subtleStyle.Render("-"),
|
||||||
colRetries: subtleStyle.Render("—"),
|
colRetries: m.st.subtleStyle.Render("—"),
|
||||||
}
|
}
|
||||||
rows = append(rows, pickCols(layout.active, cells))
|
rows = append(rows, pickCols(layout.active, cells))
|
||||||
continue
|
continue
|
||||||
@@ -251,28 +249,28 @@ func (m Model) viewSitesTab() string {
|
|||||||
if tag != "" {
|
if tag != "" {
|
||||||
errText = tag + " " + errText
|
errText = tag + " " + errText
|
||||||
}
|
}
|
||||||
name = name + " " + subtleStyle.Render(limitStr(errText, errSpace))
|
name = name + " " + m.st.subtleStyle.Render(limitStr(errText, errSpace))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hist, _ := m.engine.GetHistory(site.ID)
|
hist, _ := m.engine.GetHistory(site.ID)
|
||||||
var spark string
|
var spark string
|
||||||
if site.Type == "push" {
|
if site.Type == "push" {
|
||||||
spark = heartbeatSparkline(hist.Statuses, sparkWidth, rowBg)
|
spark = m.heartbeatSparkline(hist.Statuses, sparkWidth, rowBg)
|
||||||
} else {
|
} else {
|
||||||
spark = latencySparkline(hist.Latencies, hist.Statuses, sparkWidth, rowBg)
|
spark = m.latencySparkline(hist.Latencies, hist.Statuses, sparkWidth, rowBg)
|
||||||
}
|
}
|
||||||
|
|
||||||
cells := map[colKey]string{
|
cells := map[colKey]string{
|
||||||
colNum: strconv.Itoa(i + 1),
|
colNum: strconv.Itoa(i + 1),
|
||||||
colName: m.zones.Mark(fmt.Sprintf("site-%d", i), name),
|
colName: m.zones.Mark(fmt.Sprintf("site-%d", i), name),
|
||||||
colType: typeIcon(site.Type, false) + " " + site.Type,
|
colType: typeIcon(site.Type, false) + " " + site.Type,
|
||||||
colStatus: fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
colStatus: m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||||
colLatency: fmtLatency(site.Latency),
|
colLatency: m.fmtLatency(site.Latency),
|
||||||
colUptime: fmtUptime(hist.Statuses),
|
colUptime: m.fmtUptime(hist.Statuses),
|
||||||
colHistory: spark,
|
colHistory: spark,
|
||||||
colSSL: fmtSSL(site),
|
colSSL: m.fmtSSL(site),
|
||||||
colRetries: fmtRetries(site),
|
colRetries: m.fmtRetries(site),
|
||||||
}
|
}
|
||||||
rows = append(rows, pickCols(layout.active, cells))
|
rows = append(rows, pickCols(layout.active, cells))
|
||||||
}
|
}
|
||||||
@@ -281,7 +279,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
layout.colWidths,
|
layout.colWidths,
|
||||||
func(row, col int) *lipgloss.Style {
|
func(row, col int) *lipgloss.Style {
|
||||||
if groupRows[row] {
|
if groupRows[row] {
|
||||||
s := siteGroupStyle
|
s := m.st.siteGroupStyle
|
||||||
return &s
|
return &s
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ type userFormData struct {
|
|||||||
Role string
|
Role string
|
||||||
}
|
}
|
||||||
|
|
||||||
func fmtRole(role string) string {
|
func (m Model) fmtRole(role string) string {
|
||||||
if role == "admin" {
|
if role == "admin" {
|
||||||
return specialStyle.Render(role)
|
return m.st.specialStyle.Render(role)
|
||||||
}
|
}
|
||||||
return role
|
return role
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,7 @@ func (m Model) viewUsersTab() string {
|
|||||||
rows = append(rows, []string{
|
rows = append(rows, []string{
|
||||||
fmt.Sprintf("%d", i+1),
|
fmt.Sprintf("%d", i+1),
|
||||||
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
|
m.zones.Mark(fmt.Sprintf("user-%d", i), limitStr(u.Username, userW-2)),
|
||||||
fmtRole(u.Role),
|
m.fmtRole(u.Role),
|
||||||
fmtKey(u.PublicKey),
|
fmtKey(u.PublicKey),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,6 @@ import (
|
|||||||
"github.com/charmbracelet/lipgloss/table"
|
"github.com/charmbracelet/lipgloss/table"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
tableHeaderStyle lipgloss.Style
|
|
||||||
tableCellStyle lipgloss.Style
|
|
||||||
tableSelectedStyle lipgloss.Style
|
|
||||||
tableBorderStyle lipgloss.Style
|
|
||||||
tableZebraStyle lipgloss.Style
|
|
||||||
)
|
|
||||||
|
|
||||||
type StyleOverride func(row, col int) *lipgloss.Style
|
type StyleOverride func(row, col int) *lipgloss.Style
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -53,13 +45,13 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
|
|
||||||
t := table.New().
|
t := table.New().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderStyle(tableBorderStyle).
|
BorderStyle(m.st.tableBorderStyle).
|
||||||
Width(tableWidth).
|
Width(tableWidth).
|
||||||
Headers(headers...).
|
Headers(headers...).
|
||||||
Rows(rows...).
|
Rows(rows...).
|
||||||
StyleFunc(func(row, col int) lipgloss.Style {
|
StyleFunc(func(row, col int) lipgloss.Style {
|
||||||
if row == table.HeaderRow {
|
if row == table.HeaderRow {
|
||||||
h := tableHeaderStyle
|
h := m.st.tableHeaderStyle
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
h = h.Width(colWidths[col]).MaxWidth(colWidths[col])
|
h = h.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
}
|
}
|
||||||
@@ -70,10 +62,10 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
if s := styleOverride(row, col); s != nil {
|
if s := styleOverride(row, col); s != nil {
|
||||||
style := *s
|
style := *s
|
||||||
if row%2 == 1 {
|
if row%2 == 1 {
|
||||||
style = style.Background(tableZebraStyle.GetBackground())
|
style = style.Background(m.st.tableZebraStyle.GetBackground())
|
||||||
}
|
}
|
||||||
if isSelected {
|
if isSelected {
|
||||||
style = tableSelectedStyle.Foreground(s.GetForeground())
|
style = m.st.tableSelectedStyle.Foreground(s.GetForeground())
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
style = style.Width(colWidths[col]).MaxWidth(colWidths[col])
|
style = style.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
@@ -81,12 +73,12 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
return style
|
return style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
base := tableCellStyle
|
base := m.st.tableCellStyle
|
||||||
if row%2 == 1 {
|
if row%2 == 1 {
|
||||||
base = tableZebraStyle
|
base = m.st.tableZebraStyle
|
||||||
}
|
}
|
||||||
if isSelected {
|
if isSelected {
|
||||||
base = tableSelectedStyle
|
base = m.st.tableSelectedStyle
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
base = base.Width(colWidths[col]).MaxWidth(colWidths[col])
|
base = base.Width(colWidths[col]).MaxWidth(colWidths[col])
|
||||||
|
|||||||
+36
-23
@@ -16,7 +16,10 @@ import (
|
|||||||
zone "github.com/lrstanley/bubblezone"
|
zone "github.com/lrstanley/bubblezone"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// styles holds every theme-derived lipgloss style. Each Model owns its own
|
||||||
|
// instance (built by newStyles), so concurrent SSH sessions can run different
|
||||||
|
// themes without racing on shared package state. Never mutate after creation.
|
||||||
|
type styles struct {
|
||||||
subtleStyle lipgloss.Style
|
subtleStyle lipgloss.Style
|
||||||
specialStyle lipgloss.Style
|
specialStyle lipgloss.Style
|
||||||
warnStyle lipgloss.Style
|
warnStyle lipgloss.Style
|
||||||
@@ -29,31 +32,41 @@ var (
|
|||||||
sparkSuccess string
|
sparkSuccess string
|
||||||
sparkWarning string
|
sparkWarning string
|
||||||
sparkDanger string
|
sparkDanger string
|
||||||
)
|
|
||||||
|
|
||||||
func applyTheme(t Theme) {
|
tableHeaderStyle lipgloss.Style
|
||||||
subtleStyle = lipgloss.NewStyle().Foreground(t.Subtle)
|
tableCellStyle lipgloss.Style
|
||||||
specialStyle = lipgloss.NewStyle().Foreground(t.Success)
|
tableSelectedStyle lipgloss.Style
|
||||||
warnStyle = lipgloss.NewStyle().Foreground(t.Warning)
|
tableBorderStyle lipgloss.Style
|
||||||
staleStyle = lipgloss.NewStyle().Foreground(t.Stale)
|
tableZebraStyle lipgloss.Style
|
||||||
dangerStyle = lipgloss.NewStyle().Foreground(t.Danger)
|
|
||||||
|
|
||||||
sparkSuccess = string(t.Success)
|
siteGroupStyle lipgloss.Style
|
||||||
sparkWarning = string(t.Warning)
|
maintStyle lipgloss.Style
|
||||||
sparkDanger = string(t.Danger)
|
}
|
||||||
|
|
||||||
titleStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true)
|
func newStyles(t Theme) *styles {
|
||||||
activeTab = lipgloss.NewStyle().Background(t.Surface).Foreground(t.Accent).Bold(true).Padding(0, 1)
|
return &styles{
|
||||||
inactiveTab = lipgloss.NewStyle().Padding(0, 1).Foreground(t.Muted)
|
subtleStyle: lipgloss.NewStyle().Foreground(t.Subtle),
|
||||||
|
specialStyle: lipgloss.NewStyle().Foreground(t.Success),
|
||||||
|
warnStyle: lipgloss.NewStyle().Foreground(t.Warning),
|
||||||
|
staleStyle: lipgloss.NewStyle().Foreground(t.Stale),
|
||||||
|
dangerStyle: lipgloss.NewStyle().Foreground(t.Danger),
|
||||||
|
titleStyle: lipgloss.NewStyle().Foreground(t.Accent).Bold(true),
|
||||||
|
activeTab: lipgloss.NewStyle().Background(t.Surface).Foreground(t.Accent).Bold(true).Padding(0, 1),
|
||||||
|
inactiveTab: lipgloss.NewStyle().Padding(0, 1).Foreground(t.Muted),
|
||||||
|
|
||||||
tableHeaderStyle = lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Padding(0, 1)
|
sparkSuccess: string(t.Success),
|
||||||
tableCellStyle = lipgloss.NewStyle().Padding(0, 1)
|
sparkWarning: string(t.Warning),
|
||||||
tableSelectedStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.SelectedFg).Background(t.SelectedBg)
|
sparkDanger: string(t.Danger),
|
||||||
tableBorderStyle = lipgloss.NewStyle().Foreground(t.Border)
|
|
||||||
tableZebraStyle = lipgloss.NewStyle().Padding(0, 1).Background(t.ZebraBg)
|
|
||||||
|
|
||||||
siteGroupStyle = lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.Accent)
|
tableHeaderStyle: lipgloss.NewStyle().Foreground(t.Accent).Bold(true).Padding(0, 1),
|
||||||
maintStyle = lipgloss.NewStyle().Foreground(t.Purple)
|
tableCellStyle: lipgloss.NewStyle().Padding(0, 1),
|
||||||
|
tableSelectedStyle: lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.SelectedFg).Background(t.SelectedBg),
|
||||||
|
tableBorderStyle: lipgloss.NewStyle().Foreground(t.Border),
|
||||||
|
tableZebraStyle: lipgloss.NewStyle().Padding(0, 1).Background(t.ZebraBg),
|
||||||
|
|
||||||
|
siteGroupStyle: lipgloss.NewStyle().Padding(0, 1).Bold(true).Foreground(t.Accent),
|
||||||
|
maintStyle: lipgloss.NewStyle().Foreground(t.Purple),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||||
@@ -128,6 +141,7 @@ type Model struct {
|
|||||||
engine *monitor.Engine
|
engine *monitor.Engine
|
||||||
theme Theme
|
theme Theme
|
||||||
themeIndex int
|
themeIndex int
|
||||||
|
st *styles
|
||||||
|
|
||||||
// harmonica animation state
|
// harmonica animation state
|
||||||
pulseSpring harmonica.Spring
|
pulseSpring harmonica.Spring
|
||||||
@@ -174,8 +188,6 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine, version stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTheme(theme)
|
|
||||||
|
|
||||||
return Model{
|
return Model{
|
||||||
state: stateDashboard,
|
state: stateDashboard,
|
||||||
logViewport: vpLogs,
|
logViewport: vpLogs,
|
||||||
@@ -188,6 +200,7 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine, version stri
|
|||||||
collapsed: collapsed,
|
collapsed: collapsed,
|
||||||
theme: theme,
|
theme: theme,
|
||||||
themeIndex: themeIdx,
|
themeIndex: themeIdx,
|
||||||
|
st: newStyles(theme),
|
||||||
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
demoMode: os.Getenv("UPTOP_DEMO") == "1",
|
||||||
version: version,
|
version: version,
|
||||||
sparkTooltipIdx: -1,
|
sparkTooltipIdx: -1,
|
||||||
|
|||||||
@@ -542,7 +542,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
case "T":
|
case "T":
|
||||||
m.themeIndex = (m.themeIndex + 1) % len(themes)
|
m.themeIndex = (m.themeIndex + 1) % len(themes)
|
||||||
m.theme = themes[m.themeIndex]
|
m.theme = themes[m.themeIndex]
|
||||||
applyTheme(m.theme)
|
m.st = newStyles(m.theme)
|
||||||
_ = m.store.SetPreference("theme", m.theme.Name)
|
_ = m.store.SetPreference("theme", m.theme.Name)
|
||||||
case "d", "backspace":
|
case "d", "backspace":
|
||||||
return m.handleDeleteItem()
|
return m.handleDeleteItem()
|
||||||
|
|||||||
@@ -99,6 +99,8 @@ func newTestModel(ms *tuiMockStore) Model {
|
|||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
zones: zone.New(),
|
zones: zone.New(),
|
||||||
detailChangesSiteID: -1,
|
detailChangesSiteID: -1,
|
||||||
|
theme: themeFlexokiDark,
|
||||||
|
st: newStyles(themeFlexokiDark),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ func (m Model) View() string {
|
|||||||
case 5:
|
case 5:
|
||||||
kind = "user"
|
kind = "user"
|
||||||
}
|
}
|
||||||
msg := dangerStyle.Render(fmt.Sprintf("Delete %s \"%s\"?", kind, m.deleteName))
|
msg := m.st.dangerStyle.Render(fmt.Sprintf("Delete %s \"%s\"?", kind, m.deleteName))
|
||||||
hint := subtleStyle.Render("[y] Confirm [n] Cancel")
|
hint := m.st.subtleStyle.Render("[y] Confirm [n] Cancel")
|
||||||
box := lipgloss.NewStyle().
|
box := lipgloss.NewStyle().
|
||||||
Border(lipgloss.RoundedBorder()).
|
Border(lipgloss.RoundedBorder()).
|
||||||
BorderForeground(m.theme.Danger).
|
BorderForeground(m.theme.Danger).
|
||||||
@@ -89,8 +89,8 @@ func (m Model) View() string {
|
|||||||
formHeight = 5
|
formHeight = 5
|
||||||
}
|
}
|
||||||
m.huhForm.WithHeight(formHeight)
|
m.huhForm.WithHeight(formHeight)
|
||||||
header := titleStyle.Render(title)
|
header := m.st.titleStyle.Render(title)
|
||||||
footer := subtleStyle.Render("\n[Esc] Cancel")
|
footer := m.st.subtleStyle.Render("\n[Esc] Cancel")
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(header + "\n\n" + m.huhForm.View() + "\n" + footer)
|
return lipgloss.NewStyle().Padding(1, 2).Render(header + "\n\n" + m.huhForm.View() + "\n" + footer)
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
@@ -216,16 +216,16 @@ func (m Model) renderTabBar(stats dashboardStats) string {
|
|||||||
if t.count > 0 {
|
if t.count > 0 {
|
||||||
badge := countStyle.Render(fmt.Sprintf(" %d", t.count))
|
badge := countStyle.Render(fmt.Sprintf(" %d", t.count))
|
||||||
if t.warn > 0 {
|
if t.warn > 0 {
|
||||||
badge = dangerStyle.Render(fmt.Sprintf(" %d", t.warn))
|
badge = m.st.dangerStyle.Render(fmt.Sprintf(" %d", t.warn))
|
||||||
}
|
}
|
||||||
label += badge
|
label += badge
|
||||||
}
|
}
|
||||||
|
|
||||||
var rendered string
|
var rendered string
|
||||||
if i == m.currentTab {
|
if i == m.currentTab {
|
||||||
rendered = activeTab.Render(label)
|
rendered = m.st.activeTab.Render(label)
|
||||||
} else {
|
} else {
|
||||||
rendered = inactiveTab.Render(label)
|
rendered = m.st.inactiveTab.Render(label)
|
||||||
}
|
}
|
||||||
renderedTabs = append(renderedTabs, m.zones.Mark(fmt.Sprintf("tab-%d", i), rendered))
|
renderedTabs = append(renderedTabs, m.zones.Mark(fmt.Sprintf("tab-%d", i), rendered))
|
||||||
}
|
}
|
||||||
@@ -235,21 +235,21 @@ func (m Model) renderTabBar(stats dashboardStats) string {
|
|||||||
func (m Model) renderFooter(stats dashboardStats) string {
|
func (m Model) renderFooter(stats dashboardStats) string {
|
||||||
if m.filterMode {
|
if m.filterMode {
|
||||||
cursor := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("│")
|
cursor := lipgloss.NewStyle().Foreground(m.theme.Accent).Render("│")
|
||||||
return "\n" + titleStyle.Render("/") + " " + m.filterText + cursor + " " + subtleStyle.Render("[Enter]Apply [Esc]Clear")
|
return "\n" + m.st.titleStyle.Render("/") + " " + m.filterText + cursor + " " + m.st.subtleStyle.Render("[Enter]Apply [Esc]Clear")
|
||||||
}
|
}
|
||||||
|
|
||||||
upCount := stats.totalMonitors - stats.downCount - stats.lateCount
|
upCount := stats.totalMonitors - stats.downCount - stats.lateCount
|
||||||
var upStr string
|
var upStr string
|
||||||
if stats.downCount > 0 {
|
if stats.downCount > 0 {
|
||||||
upStr = dangerStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
upStr = m.st.dangerStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
||||||
} else if stats.lateCount > 0 {
|
} else if stats.lateCount > 0 {
|
||||||
upStr = warnStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
upStr = m.st.warnStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
||||||
} else {
|
} else {
|
||||||
upStr = specialStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
upStr = m.st.specialStyle.Render(fmt.Sprintf("%d/%d UP", upCount, stats.totalMonitors))
|
||||||
}
|
}
|
||||||
statusParts := []string{upStr}
|
statusParts := []string{upStr}
|
||||||
if stats.lateCount > 0 {
|
if stats.lateCount > 0 {
|
||||||
statusParts = append(statusParts, warnStyle.Render(fmt.Sprintf("%d LATE", stats.lateCount)))
|
statusParts = append(statusParts, m.st.warnStyle.Render(fmt.Sprintf("%d LATE", stats.lateCount)))
|
||||||
}
|
}
|
||||||
if len(m.nodes) > 0 {
|
if len(m.nodes) > 0 {
|
||||||
online := 0
|
online := 0
|
||||||
@@ -264,7 +264,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
|||||||
}
|
}
|
||||||
statusParts = append(statusParts, fmt.Sprintf("%d %s", online, probeLabel))
|
statusParts = append(statusParts, fmt.Sprintf("%d %s", online, probeLabel))
|
||||||
}
|
}
|
||||||
statusLine := strings.Join(statusParts, subtleStyle.Render(" · "))
|
statusLine := strings.Join(statusParts, m.st.subtleStyle.Render(" · "))
|
||||||
|
|
||||||
var keys string
|
var keys string
|
||||||
switch m.currentTab {
|
switch m.currentTab {
|
||||||
@@ -282,10 +282,10 @@ func (m Model) renderFooter(stats dashboardStats) string {
|
|||||||
keys = "[T]Theme [Tab]Switch [q]Quit"
|
keys = "[T]Theme [Tab]Switch [q]Quit"
|
||||||
}
|
}
|
||||||
|
|
||||||
ver := subtleStyle.Render("v" + m.version)
|
ver := m.st.subtleStyle.Render("v" + m.version)
|
||||||
footer := statusLine + " " + subtleStyle.Render(keys) + " " + ver
|
footer := statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
||||||
if m.filterText != "" && m.currentTab == 0 {
|
if m.filterText != "" && m.currentTab == 0 {
|
||||||
footer = subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + subtleStyle.Render(keys) + " " + ver
|
footer = m.st.subtleStyle.Render(fmt.Sprintf("filter: %s", m.filterText)) + " " + statusLine + " " + m.st.subtleStyle.Render(keys) + " " + ver
|
||||||
}
|
}
|
||||||
return footer
|
return footer
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-42
@@ -24,26 +24,26 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if site.ParentID > 0 {
|
if site.ParentID > 0 {
|
||||||
for _, s := range m.sites {
|
for _, s := range m.sites {
|
||||||
if s.ID == site.ParentID {
|
if s.ID == site.ParentID {
|
||||||
breadcrumb = subtleStyle.Render(" Sites > "+s.Name+" > ") + titleStyle.Render(site.Name)
|
breadcrumb = m.st.subtleStyle.Render(" Sites > "+s.Name+" > ") + m.st.titleStyle.Render(site.Name)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if breadcrumb == "" {
|
if breadcrumb == "" {
|
||||||
breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name)
|
breadcrumb = m.st.subtleStyle.Render(" Sites > ") + m.st.titleStyle.Render(site.Name)
|
||||||
}
|
}
|
||||||
b.WriteString(breadcrumb + "\n")
|
b.WriteString(breadcrumb + "\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
|
|
||||||
row := func(label, value string) {
|
row := func(label, value string) {
|
||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render(label), value)
|
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render(label), value)
|
||||||
}
|
}
|
||||||
|
|
||||||
section := func(label string) {
|
section := func(label string) {
|
||||||
b.WriteString("\n" + subtleStyle.Render(" "+label) + "\n")
|
b.WriteString("\n" + m.st.subtleStyle.Render(" "+label) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
row("Status", m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
||||||
|
|
||||||
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" {
|
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" {
|
||||||
errWidth := m.termWidth - chromePadH - 19
|
errWidth := m.termWidth - chromePadH - 19
|
||||||
@@ -51,7 +51,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
errWidth = 30
|
errWidth = 30
|
||||||
}
|
}
|
||||||
wrapped := lipgloss.NewStyle().Width(errWidth).Render(site.LastError)
|
wrapped := lipgloss.NewStyle().Width(errWidth).Render(site.LastError)
|
||||||
row("Error", dangerStyle.Render(wrapped))
|
row("Error", m.st.dangerStyle.Render(wrapped))
|
||||||
}
|
}
|
||||||
|
|
||||||
if site.Type == "http" && site.StatusCode > 0 {
|
if site.Type == "http" && site.StatusCode > 0 {
|
||||||
@@ -66,19 +66,19 @@ func (m Model) viewDetailPanel() string {
|
|||||||
var icon string
|
var icon string
|
||||||
switch step.Status {
|
switch step.Status {
|
||||||
case stepPassed:
|
case stepPassed:
|
||||||
icon = specialStyle.Render("✓")
|
icon = m.st.specialStyle.Render("✓")
|
||||||
case stepFailed:
|
case stepFailed:
|
||||||
icon = dangerStyle.Render("✗")
|
icon = m.st.dangerStyle.Render("✗")
|
||||||
case stepSkipped:
|
case stepSkipped:
|
||||||
icon = subtleStyle.Render("·")
|
icon = m.st.subtleStyle.Render("·")
|
||||||
}
|
}
|
||||||
line := fmt.Sprintf(" %s %-16s", icon, step.Name)
|
line := fmt.Sprintf(" %s %-16s", icon, step.Name)
|
||||||
if step.Detail != "" {
|
if step.Detail != "" {
|
||||||
switch step.Status {
|
switch step.Status {
|
||||||
case stepFailed:
|
case stepFailed:
|
||||||
line += " " + dangerStyle.Render(step.Detail)
|
line += " " + m.st.dangerStyle.Render(step.Detail)
|
||||||
case stepSkipped:
|
case stepSkipped:
|
||||||
line += " " + subtleStyle.Render(step.Detail)
|
line += " " + m.st.subtleStyle.Render(step.Detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(line + "\n")
|
b.WriteString(line + "\n")
|
||||||
@@ -99,7 +99,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if m.isMonitorInMaintenance(site.ID) {
|
if m.isMonitorInMaintenance(site.ID) {
|
||||||
for _, mw := range m.maintenanceWindows {
|
for _, mw := range m.maintenanceWindows {
|
||||||
if mw.Type == "maintenance" && (mw.MonitorID == 0 || mw.MonitorID == site.ID || mw.MonitorID == site.ParentID) {
|
if mw.Type == "maintenance" && (mw.MonitorID == 0 || mw.MonitorID == site.ID || mw.MonitorID == site.ParentID) {
|
||||||
row("Maintenance", maintStyle.Render(mw.Title))
|
row("Maintenance", m.st.maintStyle.Render(mw.Title))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,10 +126,10 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if site.Timeout > 0 {
|
if site.Timeout > 0 {
|
||||||
row("Timeout", fmt.Sprintf("%ds", site.Timeout))
|
row("Timeout", fmt.Sprintf("%ds", site.Timeout))
|
||||||
}
|
}
|
||||||
row("Latency", fmtLatency(site.Latency))
|
row("Latency", m.fmtLatency(site.Latency))
|
||||||
row("Uptime", fmtUptime(hist.Statuses))
|
row("Uptime", m.fmtUptime(hist.Statuses))
|
||||||
if !site.LastCheck.IsZero() {
|
if !site.LastCheck.IsZero() {
|
||||||
row("Last Check", fmtTimeAgo(site.LastCheck))
|
row("Last Check", m.fmtTimeAgo(site.LastCheck))
|
||||||
}
|
}
|
||||||
|
|
||||||
if site.Type == "http" {
|
if site.Type == "http" {
|
||||||
@@ -142,16 +142,16 @@ func (m Model) viewDetailPanel() string {
|
|||||||
codes = "200-299"
|
codes = "200-299"
|
||||||
}
|
}
|
||||||
row("Codes", codes)
|
row("Codes", codes)
|
||||||
row("SSL", fmtSSL(site))
|
row("SSL", m.fmtSSL(site))
|
||||||
if site.IgnoreTLS {
|
if site.IgnoreTLS {
|
||||||
row("TLS Verify", dangerStyle.Render("disabled"))
|
row("TLS Verify", m.st.dangerStyle.Render("disabled"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if site.MaxRetries > 0 || site.Regions != "" || site.Description != "" {
|
if site.MaxRetries > 0 || site.Regions != "" || site.Description != "" {
|
||||||
section("CONFIG")
|
section("CONFIG")
|
||||||
if site.MaxRetries > 0 {
|
if site.MaxRetries > 0 {
|
||||||
row("Retries", fmtRetries(site))
|
row("Retries", m.fmtRetries(site))
|
||||||
}
|
}
|
||||||
if site.Regions != "" {
|
if site.Regions != "" {
|
||||||
row("Regions", site.Regions)
|
row("Regions", site.Regions)
|
||||||
@@ -163,17 +163,17 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
probeResults := m.engine.GetProbeResults(site.ID)
|
probeResults := m.engine.GetProbeResults(site.ID)
|
||||||
if len(probeResults) > 0 {
|
if len(probeResults) > 0 {
|
||||||
b.WriteString("\n" + subtleStyle.Render(" PROBE RESULTS") + "\n")
|
b.WriteString("\n" + m.st.subtleStyle.Render(" PROBE RESULTS") + "\n")
|
||||||
for nodeID, result := range probeResults {
|
for nodeID, result := range probeResults {
|
||||||
status := specialStyle.Render("UP")
|
status := m.st.specialStyle.Render("UP")
|
||||||
if !result.IsUp {
|
if !result.IsUp {
|
||||||
status = dangerStyle.Render("DN")
|
status = m.st.dangerStyle.Render("DN")
|
||||||
}
|
}
|
||||||
latency := time.Duration(result.LatencyNs).Milliseconds()
|
latency := time.Duration(result.LatencyNs).Milliseconds()
|
||||||
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
||||||
line := fmt.Sprintf(" %-14s %s %dms %s ago", nodeID, status, latency, ago)
|
line := fmt.Sprintf(" %-14s %s %dms %s ago", nodeID, status, latency, ago)
|
||||||
if !result.IsUp && result.ErrorReason != "" {
|
if !result.IsUp && result.ErrorReason != "" {
|
||||||
line += " " + dangerStyle.Render(result.ErrorReason)
|
line += " " + m.st.dangerStyle.Render(result.ErrorReason)
|
||||||
}
|
}
|
||||||
b.WriteString(line + "\n")
|
b.WriteString(line + "\n")
|
||||||
}
|
}
|
||||||
@@ -185,31 +185,31 @@ func (m Model) viewDetailPanel() string {
|
|||||||
stateChanges = m.detailChanges
|
stateChanges = m.detailChanges
|
||||||
}
|
}
|
||||||
if len(stateChanges) > 0 {
|
if len(stateChanges) > 0 {
|
||||||
b.WriteString("\n" + subtleStyle.Render(" STATE CHANGES") + "\n")
|
b.WriteString("\n" + m.st.subtleStyle.Render(" STATE CHANGES") + "\n")
|
||||||
for i, sc := range stateChanges {
|
for i, sc := range stateChanges {
|
||||||
ago := fmtDuration(time.Since(sc.ChangedAt))
|
ago := fmtDuration(time.Since(sc.ChangedAt))
|
||||||
arrow := subtleStyle.Render(sc.FromStatus) + " → "
|
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||||
if sc.ToStatus == "UP" {
|
if sc.ToStatus == "UP" {
|
||||||
arrow += specialStyle.Render(sc.ToStatus)
|
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||||
} else {
|
} else {
|
||||||
arrow += dangerStyle.Render(sc.ToStatus)
|
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||||
}
|
}
|
||||||
line := fmt.Sprintf(" %s %s", arrow, subtleStyle.Render(ago+" ago"))
|
line := fmt.Sprintf(" %s %s", arrow, m.st.subtleStyle.Render(ago+" ago"))
|
||||||
if dur := computeOutageDuration(stateChanges, i); dur > 0 {
|
if dur := computeOutageDuration(stateChanges, i); dur > 0 {
|
||||||
line += " " + warnStyle.Render("outage "+fmtDuration(dur))
|
line += " " + m.st.warnStyle.Render("outage "+fmtDuration(dur))
|
||||||
}
|
}
|
||||||
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
||||||
line += " " + dangerStyle.Render(sc.ErrorReason)
|
line += " " + m.st.dangerStyle.Render(sc.ErrorReason)
|
||||||
}
|
}
|
||||||
b.WriteString(line + "\n")
|
b.WriteString(line + "\n")
|
||||||
}
|
}
|
||||||
b.WriteString(" " + subtleStyle.Render("[h] History") + "\n")
|
b.WriteString(" " + m.st.subtleStyle.Render("[h] History") + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
const sparkWidth = 40
|
const sparkWidth = 40
|
||||||
if site.Type == "push" {
|
if site.Type == "push" {
|
||||||
b.WriteString(" " + m.zones.Mark("spark-heartbeat", heartbeatSparkline(hist.Statuses, sparkWidth, "")))
|
b.WriteString(" " + m.zones.Mark("spark-heartbeat", m.heartbeatSparkline(hist.Statuses, sparkWidth, "")))
|
||||||
if len(hist.Statuses) > 0 {
|
if len(hist.Statuses) > 0 {
|
||||||
up := 0
|
up := 0
|
||||||
for _, s := range hist.Statuses {
|
for _, s := range hist.Statuses {
|
||||||
@@ -218,11 +218,11 @@ func (m Model) viewDetailPanel() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "\n %s %d/%d checks up",
|
fmt.Fprintf(&b, "\n %s %d/%d checks up",
|
||||||
subtleStyle.Render("Heartbeats"),
|
m.st.subtleStyle.Render("Heartbeats"),
|
||||||
up, len(hist.Statuses))
|
up, len(hist.Statuses))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(" " + m.zones.Mark("spark-latency", latencySparkline(hist.Latencies, hist.Statuses, sparkWidth, "")))
|
b.WriteString(" " + m.zones.Mark("spark-latency", m.latencySparkline(hist.Latencies, hist.Statuses, sparkWidth, "")))
|
||||||
var minL, maxL, total time.Duration
|
var minL, maxL, total time.Duration
|
||||||
count := 0
|
count := 0
|
||||||
for i, l := range hist.Latencies {
|
for i, l := range hist.Latencies {
|
||||||
@@ -242,9 +242,9 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if count > 0 {
|
if count > 0 {
|
||||||
avg := total / time.Duration(count)
|
avg := total / time.Duration(count)
|
||||||
fmt.Fprintf(&b, "\n %s %dms %s %dms %s %dms",
|
fmt.Fprintf(&b, "\n %s %dms %s %dms %s %dms",
|
||||||
subtleStyle.Render("Min"), minL.Milliseconds(),
|
m.st.subtleStyle.Render("Min"), minL.Milliseconds(),
|
||||||
subtleStyle.Render("Avg"), avg.Milliseconds(),
|
m.st.subtleStyle.Render("Avg"), avg.Milliseconds(),
|
||||||
subtleStyle.Render("Max"), maxL.Milliseconds())
|
m.st.subtleStyle.Render("Max"), maxL.Milliseconds())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
b.WriteString(subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect [q] Quit"))
|
b.WriteString(m.st.subtleStyle.Render(" [i/Esc] Back [e] Edit [h] History [s] SLA [click] Inspect [q] Quit"))
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
}
|
}
|
||||||
@@ -283,18 +283,18 @@ func (m Model) renderSparkTooltip(site models.Site, hist monitor.SiteHistory, sp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if site.Type != "push" && idx < len(hist.Latencies) {
|
if site.Type != "push" && idx < len(hist.Latencies) {
|
||||||
parts = append(parts, fmtLatency(hist.Latencies[idx]))
|
parts = append(parts, m.fmtLatency(hist.Latencies[idx]))
|
||||||
}
|
}
|
||||||
|
|
||||||
if idx < len(hist.Statuses) {
|
if idx < len(hist.Statuses) {
|
||||||
if hist.Statuses[idx] {
|
if hist.Statuses[idx] {
|
||||||
parts = append(parts, specialStyle.Render("UP"))
|
parts = append(parts, m.st.specialStyle.Render("UP"))
|
||||||
} else {
|
} else {
|
||||||
parts = append(parts, dangerStyle.Render("DOWN"))
|
parts = append(parts, m.st.dangerStyle.Render("DOWN"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sep := subtleStyle.Render(" | ")
|
sep := m.st.subtleStyle.Render(" | ")
|
||||||
pos := subtleStyle.Render(fmt.Sprintf("[%d/%d]", idx+1, dataLen))
|
pos := m.st.subtleStyle.Render(fmt.Sprintf("[%d/%d]", idx+1, dataLen))
|
||||||
return " " + strings.Join(parts, sep) + " " + pos
|
return " " + strings.Join(parts, sep) + " " + pos
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func computeHistoryStats(changes []models.StateChange) historyStats {
|
|||||||
|
|
||||||
var stateChangeChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
var stateChangeChars = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}
|
||||||
|
|
||||||
func stateChangeSparkline(changes []models.StateChange, width int) string {
|
func (m Model) stateChangeSparkline(changes []models.StateChange, width int) string {
|
||||||
if len(changes) < 2 || width < 4 {
|
if len(changes) < 2 || width < 4 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -96,11 +96,11 @@ func stateChangeSparkline(changes []models.StateChange, width int) string {
|
|||||||
ch := string(stateChangeChars[idx])
|
ch := string(stateChangeChars[idx])
|
||||||
switch {
|
switch {
|
||||||
case v >= 3:
|
case v >= 3:
|
||||||
sb.WriteString(dangerStyle.Render(ch))
|
sb.WriteString(m.st.dangerStyle.Render(ch))
|
||||||
case v >= 2:
|
case v >= 2:
|
||||||
sb.WriteString(warnStyle.Render(ch))
|
sb.WriteString(m.st.warnStyle.Render(ch))
|
||||||
default:
|
default:
|
||||||
sb.WriteString(subtleStyle.Render(ch))
|
sb.WriteString(m.st.subtleStyle.Render(ch))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sb.String()
|
return sb.String()
|
||||||
@@ -120,26 +120,26 @@ func (m Model) buildHistoryContent() string {
|
|||||||
for i, sc := range m.historyChanges {
|
for i, sc := range m.historyChanges {
|
||||||
ts := sc.ChangedAt.Format("2006-01-02 15:04")
|
ts := sc.ChangedAt.Format("2006-01-02 15:04")
|
||||||
|
|
||||||
arrow := subtleStyle.Render(sc.FromStatus) + " → "
|
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||||
switch sc.ToStatus {
|
switch sc.ToStatus {
|
||||||
case "UP":
|
case "UP":
|
||||||
arrow += specialStyle.Render(sc.ToStatus)
|
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||||
case "LATE":
|
case "LATE":
|
||||||
arrow += warnStyle.Render(sc.ToStatus)
|
arrow += m.st.warnStyle.Render(sc.ToStatus)
|
||||||
case "STALE":
|
case "STALE":
|
||||||
arrow += staleStyle.Render(sc.ToStatus)
|
arrow += m.st.staleStyle.Render(sc.ToStatus)
|
||||||
default:
|
default:
|
||||||
arrow += dangerStyle.Render(sc.ToStatus)
|
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
durStr := ""
|
durStr := ""
|
||||||
if dur := computeOutageDuration(m.historyChanges, i); dur > 0 {
|
if dur := computeOutageDuration(m.historyChanges, i); dur > 0 {
|
||||||
durStr = warnStyle.Render("outage " + fmtDuration(dur))
|
durStr = m.st.warnStyle.Render("outage " + fmtDuration(dur))
|
||||||
}
|
}
|
||||||
|
|
||||||
reason := ""
|
reason := ""
|
||||||
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
||||||
reason = dangerStyle.Render(limitStr(sc.ErrorReason, reasonWidth))
|
reason = m.st.dangerStyle.Render(limitStr(sc.ErrorReason, reasonWidth))
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&b, " %-18s %s %-12s %s\n", ts, arrow, durStr, reason)
|
fmt.Fprintf(&b, " %-18s %s %-12s %s\n", ts, arrow, durStr, reason)
|
||||||
@@ -151,27 +151,27 @@ func (m Model) buildHistoryContent() string {
|
|||||||
func (m Model) viewHistoryPanel() string {
|
func (m Model) viewHistoryPanel() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
header := " " + titleStyle.Render("STATE HISTORY: "+m.historySiteName)
|
header := " " + m.st.titleStyle.Render("STATE HISTORY: "+m.historySiteName)
|
||||||
header += " " + subtleStyle.Render("[q] Back")
|
header += " " + m.st.subtleStyle.Render("[q] Back")
|
||||||
b.WriteString(header + "\n")
|
b.WriteString(header + "\n")
|
||||||
|
|
||||||
divWidth := m.dividerWidth()
|
divWidth := m.dividerWidth()
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
|
|
||||||
sparkline := stateChangeSparkline(m.historyChanges, divWidth)
|
sparkline := m.stateChangeSparkline(m.historyChanges, divWidth)
|
||||||
if sparkline != "" {
|
if sparkline != "" {
|
||||||
b.WriteString(" " + sparkline + "\n")
|
b.WriteString(" " + sparkline + "\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n",
|
fmt.Fprintf(&b, " %-18s %-17s %-12s %s\n",
|
||||||
subtleStyle.Render("TIME"),
|
m.st.subtleStyle.Render("TIME"),
|
||||||
subtleStyle.Render("TRANSITION"),
|
m.st.subtleStyle.Render("TRANSITION"),
|
||||||
subtleStyle.Render("DURATION"),
|
m.st.subtleStyle.Render("DURATION"),
|
||||||
subtleStyle.Render("REASON"))
|
m.st.subtleStyle.Render("REASON"))
|
||||||
|
|
||||||
if len(m.historyChanges) == 0 {
|
if len(m.historyChanges) == 0 {
|
||||||
b.WriteString("\n " + subtleStyle.Render("No state changes recorded") + "\n")
|
b.WriteString("\n " + m.st.subtleStyle.Render("No state changes recorded") + "\n")
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(m.historyViewport.View())
|
b.WriteString(m.historyViewport.View())
|
||||||
}
|
}
|
||||||
@@ -185,8 +185,8 @@ func (m Model) viewHistoryPanel() string {
|
|||||||
avg := stats.totalDowntime / time.Duration(stats.outageCount)
|
avg := stats.totalDowntime / time.Duration(stats.outageCount)
|
||||||
parts = append(parts, "avg outage "+fmtDuration(avg))
|
parts = append(parts, "avg outage "+fmtDuration(avg))
|
||||||
}
|
}
|
||||||
b.WriteString(" " + subtleStyle.Render(strings.Join(parts, " │ ")) + "\n")
|
b.WriteString(" " + m.st.subtleStyle.Render(strings.Join(parts, " │ ")) + "\n")
|
||||||
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
|
b.WriteString(" " + m.st.subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,14 +134,14 @@ func TestComputeHistoryStats_Empty(t *testing.T) {
|
|||||||
|
|
||||||
func TestStateChangeSparkline(t *testing.T) {
|
func TestStateChangeSparkline(t *testing.T) {
|
||||||
t.Run("empty", func(t *testing.T) {
|
t.Run("empty", func(t *testing.T) {
|
||||||
if got := stateChangeSparkline(nil, 20); got != "" {
|
if got := styledModel.stateChangeSparkline(nil, 20); got != "" {
|
||||||
t.Errorf("expected empty for nil, got %q", got)
|
t.Errorf("expected empty for nil, got %q", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("single event", func(t *testing.T) {
|
t.Run("single event", func(t *testing.T) {
|
||||||
changes := []models.StateChange{{ChangedAt: time.Now()}}
|
changes := []models.StateChange{{ChangedAt: time.Now()}}
|
||||||
if got := stateChangeSparkline(changes, 20); got != "" {
|
if got := styledModel.stateChangeSparkline(changes, 20); got != "" {
|
||||||
t.Errorf("expected empty for single event, got %q", got)
|
t.Errorf("expected empty for single event, got %q", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -152,7 +152,7 @@ func TestStateChangeSparkline(t *testing.T) {
|
|||||||
{ChangedAt: now},
|
{ChangedAt: now},
|
||||||
{ChangedAt: now.Add(-1 * time.Hour)},
|
{ChangedAt: now.Add(-1 * time.Hour)},
|
||||||
}
|
}
|
||||||
got := stateChangeSparkline(changes, 20)
|
got := styledModel.stateChangeSparkline(changes, 20)
|
||||||
if got == "" {
|
if got == "" {
|
||||||
t.Error("expected non-empty sparkline for two events")
|
t.Error("expected non-empty sparkline for two events")
|
||||||
}
|
}
|
||||||
@@ -164,7 +164,7 @@ func TestStateChangeSparkline(t *testing.T) {
|
|||||||
{ChangedAt: now},
|
{ChangedAt: now},
|
||||||
{ChangedAt: now.Add(-1 * time.Hour)},
|
{ChangedAt: now.Add(-1 * time.Hour)},
|
||||||
}
|
}
|
||||||
if got := stateChangeSparkline(changes, 3); got != "" {
|
if got := styledModel.stateChangeSparkline(changes, 3); got != "" {
|
||||||
t.Errorf("expected empty for width 3, got %q", got)
|
t.Errorf("expected empty for width 3, got %q", got)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
+25
-25
@@ -24,13 +24,13 @@ var slaPeriods = []struct {
|
|||||||
func (m Model) viewSLAPanel() string {
|
func (m Model) viewSLAPanel() string {
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
header := " " + titleStyle.Render("SLA REPORT: "+m.slaSiteName)
|
header := " " + m.st.titleStyle.Render("SLA REPORT: "+m.slaSiteName)
|
||||||
header += " " + subtleStyle.Render("[q] Back")
|
header += " " + m.st.subtleStyle.Render("[q] Back")
|
||||||
b.WriteString(header + "\n")
|
b.WriteString(header + "\n")
|
||||||
b.WriteString(m.divider() + "\n")
|
b.WriteString(m.divider() + "\n")
|
||||||
|
|
||||||
period := slaPeriods[m.slaPeriodIdx]
|
period := slaPeriods[m.slaPeriodIdx]
|
||||||
b.WriteString(" " + subtleStyle.Render("Period: Last "+period.label) + "\n\n")
|
b.WriteString(" " + m.st.subtleStyle.Render("Period: Last "+period.label) + "\n\n")
|
||||||
|
|
||||||
r := m.slaReport
|
r := m.slaReport
|
||||||
|
|
||||||
@@ -38,22 +38,22 @@ func (m Model) viewSLAPanel() string {
|
|||||||
if barWidth < 10 {
|
if barWidth < 10 {
|
||||||
barWidth = 10
|
barWidth = 10
|
||||||
}
|
}
|
||||||
bar := uptimeBar(r.UptimePct, barWidth)
|
bar := m.uptimeBar(r.UptimePct, barWidth)
|
||||||
uptimeColor := specialStyle
|
uptimeColor := m.st.specialStyle
|
||||||
if r.UptimePct < 99.9 {
|
if r.UptimePct < 99.9 {
|
||||||
uptimeColor = warnStyle
|
uptimeColor = m.st.warnStyle
|
||||||
}
|
}
|
||||||
if r.UptimePct < 99.0 {
|
if r.UptimePct < 99.0 {
|
||||||
uptimeColor = dangerStyle
|
uptimeColor = m.st.dangerStyle
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, " %-16s %s %s\n", subtleStyle.Render("Uptime"), uptimeColor.Render(fmt.Sprintf("%s%%", fmtPct(r.UptimePct))), bar)
|
fmt.Fprintf(&b, " %-16s %s %s\n", m.st.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 %s\n", m.st.subtleStyle.Render("Downtime"), fmtDuration(r.Downtime))
|
||||||
fmt.Fprintf(&b, " %-16s %d\n", subtleStyle.Render("Outages"), r.OutageCount)
|
fmt.Fprintf(&b, " %-16s %d\n", m.st.subtleStyle.Render("Outages"), r.OutageCount)
|
||||||
|
|
||||||
if r.OutageCount > 0 {
|
if r.OutageCount > 0 {
|
||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
|
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render("Longest"), fmtDuration(r.LongestOut))
|
||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
|
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render("MTTR"), fmtDuration(r.MTTR))
|
||||||
fmt.Fprintf(&b, " %-16s %s\n", subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
|
fmt.Fprintf(&b, " %-16s %s\n", m.st.subtleStyle.Render("MTBF"), fmtDuration(r.MTBF))
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n" + m.divider() + "\n")
|
b.WriteString("\n" + m.divider() + "\n")
|
||||||
@@ -68,13 +68,13 @@ func (m Model) viewSLAPanel() string {
|
|||||||
for i, p := range slaPeriods {
|
for i, p := range slaPeriods {
|
||||||
label := fmt.Sprintf("[%s] %s", p.key, p.label)
|
label := fmt.Sprintf("[%s] %s", p.key, p.label)
|
||||||
if i == m.slaPeriodIdx {
|
if i == m.slaPeriodIdx {
|
||||||
keys = append(keys, titleStyle.Render(label))
|
keys = append(keys, m.st.titleStyle.Render(label))
|
||||||
} else {
|
} else {
|
||||||
keys = append(keys, subtleStyle.Render(label))
|
keys = append(keys, m.st.subtleStyle.Render(label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(" " + strings.Join(keys, " "))
|
b.WriteString(" " + strings.Join(keys, " "))
|
||||||
b.WriteString(" " + subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
|
b.WriteString(" " + m.st.subtleStyle.Render("[j/k/↑/↓] Scroll [q/Esc] Back"))
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
return lipgloss.NewStyle().Padding(1, 2).Render(b.String())
|
||||||
}
|
}
|
||||||
@@ -87,27 +87,27 @@ func (m Model) buildSLADailyContent() string {
|
|||||||
barWidth = 10
|
barWidth = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString(" " + subtleStyle.Render("DAILY BREAKDOWN") + "\n")
|
b.WriteString(" " + m.st.subtleStyle.Render("DAILY BREAKDOWN") + "\n")
|
||||||
for _, day := range m.slaDailyBreakdown {
|
for _, day := range m.slaDailyBreakdown {
|
||||||
dateStr := day.Date.Format("Jan 02")
|
dateStr := day.Date.Format("Jan 02")
|
||||||
bar := uptimeBar(day.UptimePct, barWidth)
|
bar := m.uptimeBar(day.UptimePct, barWidth)
|
||||||
pctStr := fmtPct(day.UptimePct) + "%"
|
pctStr := fmtPct(day.UptimePct) + "%"
|
||||||
|
|
||||||
color := specialStyle
|
color := m.st.specialStyle
|
||||||
if day.UptimePct < 99.9 {
|
if day.UptimePct < 99.9 {
|
||||||
color = warnStyle
|
color = m.st.warnStyle
|
||||||
}
|
}
|
||||||
if day.UptimePct < 99.0 {
|
if day.UptimePct < 99.0 {
|
||||||
color = dangerStyle
|
color = m.st.dangerStyle
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(&b, " %-8s %s %s\n", subtleStyle.Render(dateStr), bar, color.Render(pctStr))
|
fmt.Fprintf(&b, " %-8s %s %s\n", m.st.subtleStyle.Render(dateStr), bar, color.Render(pctStr))
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func uptimeBar(pct float64, width int) string {
|
func (m Model) uptimeBar(pct float64, width int) string {
|
||||||
filled := int(math.Round(pct / 100 * float64(width)))
|
filled := int(math.Round(pct / 100 * float64(width)))
|
||||||
if filled > width {
|
if filled > width {
|
||||||
filled = width
|
filled = width
|
||||||
@@ -117,9 +117,9 @@ func uptimeBar(pct float64, width int) string {
|
|||||||
}
|
}
|
||||||
empty := width - filled
|
empty := width - filled
|
||||||
|
|
||||||
bar := specialStyle.Render(strings.Repeat("█", filled))
|
bar := m.st.specialStyle.Render(strings.Repeat("█", filled))
|
||||||
if empty > 0 {
|
if empty > 0 {
|
||||||
bar += subtleStyle.Render(strings.Repeat("░", empty))
|
bar += m.st.subtleStyle.Render(strings.Repeat("░", empty))
|
||||||
}
|
}
|
||||||
return bar
|
return bar
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user