fix(tui): visual polish and layout improvements #18
@@ -62,6 +62,8 @@ func (m *mockStore) AddMaintenanceWindow(models.MaintenanceWindow) error { retur
|
|||||||
func (m *mockStore) EndMaintenanceWindow(int) error { return nil }
|
func (m *mockStore) EndMaintenanceWindow(int) error { return nil }
|
||||||
func (m *mockStore) DeleteMaintenanceWindow(int) error { return nil }
|
func (m *mockStore) DeleteMaintenanceWindow(int) error { return nil }
|
||||||
func (m *mockStore) IsMonitorInMaintenance(int) (bool, error) { return false, nil }
|
func (m *mockStore) IsMonitorInMaintenance(int) (bool, error) { return false, nil }
|
||||||
|
func (m *mockStore) GetPreference(string) (string, error) { return "", nil }
|
||||||
|
func (m *mockStore) SetPreference(string, string) error { return nil }
|
||||||
|
|
||||||
func TestMetricsHandler(t *testing.T) {
|
func TestMetricsHandler(t *testing.T) {
|
||||||
ms := &mockStore{
|
ms := &mockStore{
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
|||||||
created_by TEXT DEFAULT '',
|
created_by TEXT DEFAULT '',
|
||||||
created_at TIMESTAMP DEFAULT NOW()
|
created_at TIMESTAMP DEFAULT NOW()
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS preferences (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,10 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
|||||||
created_by TEXT DEFAULT '',
|
created_by TEXT DEFAULT '',
|
||||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
)`,
|
)`,
|
||||||
|
`CREATE TABLE IF NOT EXISTS preferences (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
)`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -441,6 +441,24 @@ func (s *SQLStore) IsMonitorInMaintenance(monitorID int) (bool, error) {
|
|||||||
return count > 0, nil
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) GetPreference(key string) (string, error) {
|
||||||
|
var value string
|
||||||
|
err := s.db.QueryRow(s.q("SELECT value FROM preferences WHERE key = ?"), key).Scan(&value)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SQLStore) SetPreference(key, value string) error {
|
||||||
|
if s.dollar {
|
||||||
|
_, err := s.db.Exec(s.q("INSERT INTO preferences (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = ?"), key, value, value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := s.db.Exec("INSERT OR REPLACE INTO preferences (key, value) VALUES (?, ?)", key, value)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SQLStore) ExportData() (models.Backup, error) {
|
func (s *SQLStore) ExportData() (models.Backup, error) {
|
||||||
sites, err := s.GetSites()
|
sites, err := s.GetSites()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ type Store interface {
|
|||||||
DeleteMaintenanceWindow(id int) error
|
DeleteMaintenanceWindow(id int) error
|
||||||
IsMonitorInMaintenance(monitorID int) (bool, error)
|
IsMonitorInMaintenance(monitorID int) (bool, error)
|
||||||
|
|
||||||
|
// Preferences
|
||||||
|
GetPreference(key string) (string, error)
|
||||||
|
SetPreference(key, value string) error
|
||||||
|
|
||||||
// Backup & Restore
|
// Backup & Restore
|
||||||
ExportData() (models.Backup, error)
|
ExportData() (models.Backup, error)
|
||||||
ImportData(data models.Backup) error
|
ImportData(data models.Backup) error
|
||||||
|
|||||||
+134
-7
@@ -29,9 +29,9 @@ func typeIcon(siteType string, collapsed bool) string {
|
|||||||
return "◆"
|
return "◆"
|
||||||
case "group":
|
case "group":
|
||||||
if collapsed {
|
if collapsed {
|
||||||
return ""
|
return "▶"
|
||||||
}
|
}
|
||||||
return ""
|
return "▼"
|
||||||
default:
|
default:
|
||||||
return "·"
|
return "·"
|
||||||
}
|
}
|
||||||
@@ -132,6 +132,93 @@ func heartbeatSparkline(statuses []bool, width int) string {
|
|||||||
return sb.String()
|
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
|
||||||
|
}())
|
||||||
|
}
|
||||||
|
|
||||||
func fmtLatency(d time.Duration) string {
|
func fmtLatency(d time.Duration) string {
|
||||||
ms := d.Milliseconds()
|
ms := d.Milliseconds()
|
||||||
if ms == 0 {
|
if ms == 0 {
|
||||||
@@ -227,7 +314,7 @@ func fmtStatus(status string, paused bool, inMaint bool) string {
|
|||||||
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
func (m Model) dynamicWidths() (nameW, sparkW int) {
|
||||||
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
fixed := 6 + 10 + 10 + 8 + 8 + 7 + 9 // #, TYPE, STATUS, LATENCY, UPTIME, SSL, RETRY
|
||||||
overhead := 30 // cell padding + borders
|
overhead := 30 // cell padding + borders
|
||||||
avail := m.termWidth - 6 - fixed - overhead
|
avail := m.termWidth - chromePadH - 2 - fixed - overhead
|
||||||
if avail < 30 {
|
if avail < 30 {
|
||||||
avail = 30
|
avail = 30
|
||||||
}
|
}
|
||||||
@@ -285,8 +372,8 @@ func (m Model) viewSitesTab() string {
|
|||||||
"group",
|
"group",
|
||||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||||
subtleStyle.Render("—"),
|
subtleStyle.Render("—"),
|
||||||
subtleStyle.Render("—"),
|
m.groupUptime(site.ID),
|
||||||
subtleStyle.Render(strings.Repeat("·", sparkWidth)),
|
m.groupSparkline(site.ID, sparkWidth),
|
||||||
subtleStyle.Render("-"),
|
subtleStyle.Render("-"),
|
||||||
subtleStyle.Render("—"),
|
subtleStyle.Render("—"),
|
||||||
})
|
})
|
||||||
@@ -619,8 +706,19 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|
||||||
title := titleStyle.Render(fmt.Sprintf(" %s", site.Name))
|
var breadcrumb string
|
||||||
b.WriteString(title + "\n\n")
|
if site.ParentID > 0 {
|
||||||
|
for _, s := range m.sites {
|
||||||
|
if s.ID == site.ParentID {
|
||||||
|
breadcrumb = subtleStyle.Render(" Sites > "+s.Name+" > ") + titleStyle.Render(site.Name)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if breadcrumb == "" {
|
||||||
|
breadcrumb = subtleStyle.Render(" Sites > ") + titleStyle.Render(site.Name)
|
||||||
|
}
|
||||||
|
b.WriteString(breadcrumb + "\n\n")
|
||||||
|
|
||||||
row := func(label, value string) {
|
row := func(label, value string) {
|
||||||
b.WriteString(fmt.Sprintf(" %-16s %s\n", subtleStyle.Render(label), value))
|
b.WriteString(fmt.Sprintf(" %-16s %s\n", subtleStyle.Render(label), value))
|
||||||
@@ -690,8 +788,37 @@ func (m Model) viewDetailPanel() string {
|
|||||||
const sparkWidth = 40
|
const sparkWidth = 40
|
||||||
if site.Type == "push" {
|
if site.Type == "push" {
|
||||||
b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth))
|
b.WriteString(" " + heartbeatSparkline(hist.Statuses, sparkWidth))
|
||||||
|
if len(hist.Statuses) > 0 {
|
||||||
|
up := 0
|
||||||
|
for _, s := range hist.Statuses {
|
||||||
|
if s {
|
||||||
|
up++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(fmt.Sprintf("\n %s %d/%d checks up",
|
||||||
|
subtleStyle.Render("Heartbeats"),
|
||||||
|
up, len(hist.Statuses)))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
b.WriteString(" " + latencySparkline(hist.Latencies, sparkWidth))
|
b.WriteString(" " + latencySparkline(hist.Latencies, sparkWidth))
|
||||||
|
if len(hist.Latencies) > 0 {
|
||||||
|
minL, maxL := hist.Latencies[0], hist.Latencies[0]
|
||||||
|
var total time.Duration
|
||||||
|
for _, l := range hist.Latencies {
|
||||||
|
total += l
|
||||||
|
if l < minL {
|
||||||
|
minL = l
|
||||||
|
}
|
||||||
|
if l > maxL {
|
||||||
|
maxL = l
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avg := total / time.Duration(len(hist.Latencies))
|
||||||
|
b.WriteString(fmt.Sprintf("\n %s %dms %s %dms %s %dms",
|
||||||
|
subtleStyle.Render("Min"), minL.Milliseconds(),
|
||||||
|
subtleStyle.Render("Avg"), avg.Milliseconds(),
|
||||||
|
subtleStyle.Render("Max"), maxL.Milliseconds()))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
b.WriteString("\n\n")
|
b.WriteString("\n\n")
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ var (
|
|||||||
|
|
||||||
tableBorderStyle = lipgloss.NewStyle().
|
tableBorderStyle = lipgloss.NewStyle().
|
||||||
Foreground(lipgloss.Color("#444"))
|
Foreground(lipgloss.Color("#444"))
|
||||||
|
|
||||||
|
tableZebraStyle = lipgloss.NewStyle().
|
||||||
|
Padding(0, 1).
|
||||||
|
Background(lipgloss.Color("#1a1a2e"))
|
||||||
)
|
)
|
||||||
|
|
||||||
type StyleOverride func(row, col int) *lipgloss.Style
|
type StyleOverride func(row, col int) *lipgloss.Style
|
||||||
@@ -38,7 +42,7 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
selectedVisual := m.cursor - m.tableOffset
|
selectedVisual := m.cursor - m.tableOffset
|
||||||
rows := buildRows(m.tableOffset, end)
|
rows := buildRows(m.tableOffset, end)
|
||||||
|
|
||||||
tableWidth := m.termWidth - 6
|
tableWidth := m.termWidth - chromePadH - 2
|
||||||
if tableWidth < 40 {
|
if tableWidth < 40 {
|
||||||
tableWidth = 40
|
tableWidth = 40
|
||||||
}
|
}
|
||||||
@@ -53,16 +57,24 @@ func (m Model) renderTable(headers []string, items int, buildRows func(start, en
|
|||||||
if row == table.HeaderRow {
|
if row == table.HeaderRow {
|
||||||
return tableHeaderStyle
|
return tableHeaderStyle
|
||||||
}
|
}
|
||||||
|
isSelected := row == selectedVisual
|
||||||
if styleOverride != nil {
|
if styleOverride != nil {
|
||||||
if s := styleOverride(row, col); s != nil {
|
if s := styleOverride(row, col); s != nil {
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
style := *s
|
||||||
return s.Width(colWidths[col])
|
if isSelected {
|
||||||
|
style = tableSelectedStyle.Foreground(s.GetForeground())
|
||||||
}
|
}
|
||||||
return *s
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
|
style = style.Width(colWidths[col])
|
||||||
|
}
|
||||||
|
return style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
base := tableCellStyle
|
base := tableCellStyle
|
||||||
if row == selectedVisual {
|
if row%2 == 1 {
|
||||||
|
base = tableZebraStyle
|
||||||
|
}
|
||||||
|
if isSelected {
|
||||||
base = tableSelectedStyle
|
base = tableSelectedStyle
|
||||||
}
|
}
|
||||||
if col < len(colWidths) && colWidths[col] > 0 {
|
if col < len(colWidths) && colWidths[col] > 0 {
|
||||||
|
|||||||
+57
-12
@@ -1,6 +1,7 @@
|
|||||||
package tui
|
package tui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-upkeep/internal/models"
|
"go-upkeep/internal/models"
|
||||||
"go-upkeep/internal/monitor"
|
"go-upkeep/internal/monitor"
|
||||||
@@ -31,6 +32,16 @@ var (
|
|||||||
|
|
||||||
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
var pulseFrames = []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"}
|
||||||
|
|
||||||
|
const (
|
||||||
|
chromePadV = 2 // outer Padding(1,2): 1 top + 1 bottom
|
||||||
|
chromePadH = 4 // outer Padding(1,2): 2 left + 2 right
|
||||||
|
chromeHeader = 1 // tab bar line
|
||||||
|
chromeGaps = 2 // "\n" separators: before content + before footer
|
||||||
|
chromeFooter = 2 // footer: "\n" prefix + text line
|
||||||
|
chromeTable = 3 // renderTable "\n" prefix + top border + header + bottom border (lipgloss collapses two into three rendered lines)
|
||||||
|
chromeBase = chromePadV + chromeHeader + chromeGaps + chromeFooter + chromeTable
|
||||||
|
)
|
||||||
|
|
||||||
type sessionState int
|
type sessionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -95,6 +106,7 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
|||||||
vpLogs.SetContent("Waiting for logs...")
|
vpLogs.SetContent("Waiting for logs...")
|
||||||
z := zone.New()
|
z := zone.New()
|
||||||
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4)
|
spring := harmonica.NewSpring(harmonica.FPS(10), 6.0, 0.4)
|
||||||
|
collapsed := loadCollapsed(s)
|
||||||
return Model{
|
return Model{
|
||||||
state: stateDashboard,
|
state: stateDashboard,
|
||||||
logViewport: vpLogs,
|
logViewport: vpLogs,
|
||||||
@@ -104,10 +116,37 @@ func InitialModel(isAdmin bool, s store.Store, eng *monitor.Engine) Model {
|
|||||||
engine: eng,
|
engine: eng,
|
||||||
zones: z,
|
zones: z,
|
||||||
pulseSpring: spring,
|
pulseSpring: spring,
|
||||||
collapsed: make(map[int]bool),
|
collapsed: collapsed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadCollapsed(s store.Store) map[int]bool {
|
||||||
|
m := make(map[int]bool)
|
||||||
|
raw, err := s.GetPreference("collapsed_groups")
|
||||||
|
if err != nil || raw == "" {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
var ids []int
|
||||||
|
if err := json.Unmarshal([]byte(raw), &ids); err != nil {
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
m[id] = true
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
func saveCollapsed(s store.Store, collapsed map[int]bool) {
|
||||||
|
var ids []int
|
||||||
|
for id, v := range collapsed {
|
||||||
|
if v {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, _ := json.Marshal(ids)
|
||||||
|
_ = s.SetPreference("collapsed_groups", string(data))
|
||||||
|
}
|
||||||
|
|
||||||
func (m Model) Init() tea.Cmd {
|
func (m Model) Init() tea.Cmd {
|
||||||
return tea.Batch(tea.ClearScreen, tea.Tick(time.Second, func(t time.Time) tea.Msg { return t }))
|
return tea.Batch(tea.ClearScreen, tea.Tick(time.Second, func(t time.Time) tea.Msg { return t }))
|
||||||
}
|
}
|
||||||
@@ -198,17 +237,16 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tea.WindowSizeMsg:
|
case tea.WindowSizeMsg:
|
||||||
m.termWidth = msg.Width
|
m.termWidth = msg.Width
|
||||||
m.termHeight = msg.Height
|
m.termHeight = msg.Height
|
||||||
// Chrome: 1 top pad + 1 tabs + 2 newlines + 3 table borders + 1 table header + 1 footer + 1 bottom pad = 10
|
chrome := chromeBase
|
||||||
chrome := 10
|
if m.filterMode || m.filterText != "" {
|
||||||
if m.filterText != "" {
|
|
||||||
chrome++
|
chrome++
|
||||||
}
|
}
|
||||||
m.maxTableRows = msg.Height - chrome
|
m.maxTableRows = msg.Height - chrome
|
||||||
if m.maxTableRows < 1 {
|
if m.maxTableRows < 1 {
|
||||||
m.maxTableRows = 1
|
m.maxTableRows = 1
|
||||||
}
|
}
|
||||||
m.logViewport.Width = msg.Width - 4
|
m.logViewport.Width = msg.Width - chromePadH
|
||||||
m.logViewport.Height = msg.Height - 8
|
m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeGaps + chromeFooter)
|
||||||
return m, tea.ClearScreen
|
return m, tea.ClearScreen
|
||||||
|
|
||||||
case time.Time:
|
case time.Time:
|
||||||
@@ -392,6 +430,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.currentTab == 0 && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
|
if m.currentTab == 0 && len(m.sites) > 0 && m.sites[m.cursor].Type == "group" {
|
||||||
gid := m.sites[m.cursor].ID
|
gid := m.sites[m.cursor].ID
|
||||||
m.collapsed[gid] = !m.collapsed[gid]
|
m.collapsed[gid] = !m.collapsed[gid]
|
||||||
|
saveCollapsed(m.store, m.collapsed)
|
||||||
m.refreshData()
|
m.refreshData()
|
||||||
}
|
}
|
||||||
case "p":
|
case "p":
|
||||||
@@ -725,8 +764,14 @@ func (m Model) View() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewDashboard() string {
|
func (m Model) viewDashboard() string {
|
||||||
|
allSites := m.engine.GetAllSites()
|
||||||
|
totalMonitors := 0
|
||||||
downCount := 0
|
downCount := 0
|
||||||
for _, s := range m.sites {
|
for _, s := range allSites {
|
||||||
|
if s.Type == "group" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
totalMonitors++
|
||||||
if !s.Paused && !m.isMonitorInMaintenance(s.ID) && (s.Status == "DOWN" || s.Status == "SSL EXP") {
|
if !s.Paused && !m.isMonitorInMaintenance(s.ID) && (s.Status == "DOWN" || s.Status == "SSL EXP") {
|
||||||
downCount++
|
downCount++
|
||||||
}
|
}
|
||||||
@@ -741,8 +786,8 @@ func (m Model) viewDashboard() string {
|
|||||||
var sitesLabel string
|
var sitesLabel string
|
||||||
if downCount > 0 {
|
if downCount > 0 {
|
||||||
sitesLabel = fmt.Sprintf("Sites (%d↓)", downCount)
|
sitesLabel = fmt.Sprintf("Sites (%d↓)", downCount)
|
||||||
} else if len(m.sites) > 0 {
|
} else if totalMonitors > 0 {
|
||||||
sitesLabel = fmt.Sprintf("Sites (%d)", len(m.sites))
|
sitesLabel = fmt.Sprintf("Sites (%d)", totalMonitors)
|
||||||
} else {
|
} else {
|
||||||
sitesLabel = "Sites"
|
sitesLabel = "Sites"
|
||||||
}
|
}
|
||||||
@@ -806,12 +851,12 @@ func (m Model) viewDashboard() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
upCount := len(m.sites) - downCount
|
upCount := totalMonitors - downCount
|
||||||
var upStr string
|
var upStr string
|
||||||
if downCount > 0 {
|
if downCount > 0 {
|
||||||
upStr = dangerStyle.Render(fmt.Sprintf("%d/%d UP", upCount, len(m.sites)))
|
upStr = dangerStyle.Render(fmt.Sprintf("%d/%d UP", upCount, totalMonitors))
|
||||||
} else {
|
} else {
|
||||||
upStr = specialStyle.Render(fmt.Sprintf("%d/%d UP", upCount, len(m.sites)))
|
upStr = specialStyle.Render(fmt.Sprintf("%d/%d UP", upCount, totalMonitors))
|
||||||
}
|
}
|
||||||
statusParts := []string{upStr}
|
statusParts := []string{upStr}
|
||||||
if len(m.nodes) > 0 {
|
if len(m.nodes) > 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user