Compare commits
1 Commits
main
..
24b0cb05ba
| Author | SHA1 | Date | |
|---|---|---|---|
|
24b0cb05ba
|
@@ -123,7 +123,6 @@ func (m *Model) refreshLive() {
|
|||||||
ordered = filterSites(ordered, m.filterText)
|
ordered = filterSites(ordered, m.filterText)
|
||||||
}
|
}
|
||||||
m.sites = ordered
|
m.sites = ordered
|
||||||
m.buildMaintSet()
|
|
||||||
m.refreshLogContent()
|
m.refreshLogContent()
|
||||||
|
|
||||||
if m.selectedID != 0 {
|
if m.selectedID != 0 {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
package tui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
|
||||||
)
|
|
||||||
|
|
||||||
type latencyBucket struct {
|
|
||||||
label string
|
|
||||||
min int64
|
|
||||||
max int64
|
|
||||||
count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) latencyHistogram(latencies []time.Duration, statuses []bool, width int) string {
|
|
||||||
if len(latencies) == 0 || width < 30 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
buckets := []latencyBucket{
|
|
||||||
{"0-50ms", 0, 50, 0},
|
|
||||||
{"50-100ms", 50, 100, 0},
|
|
||||||
{"100-200ms", 100, 200, 0},
|
|
||||||
{"200-500ms", 200, 500, 0},
|
|
||||||
{"500ms+", 500, 999999, 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, l := range latencies {
|
|
||||||
if i < len(statuses) && !statuses[i] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ms := l.Milliseconds()
|
|
||||||
for j := range buckets {
|
|
||||||
if ms >= buckets[j].min && ms < buckets[j].max {
|
|
||||||
buckets[j].count++
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
maxCount := 0
|
|
||||||
for _, b := range buckets {
|
|
||||||
if b.count > maxCount {
|
|
||||||
maxCount = b.count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if maxCount == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
labelW := 10
|
|
||||||
countW := len(fmt.Sprintf("%d", maxCount))
|
|
||||||
barW := width - labelW - countW - 4
|
|
||||||
if barW < 5 {
|
|
||||||
barW = 5
|
|
||||||
}
|
|
||||||
|
|
||||||
var sb strings.Builder
|
|
||||||
for _, b := range buckets {
|
|
||||||
fill := 0
|
|
||||||
if maxCount > 0 {
|
|
||||||
fill = b.count * barW / maxCount
|
|
||||||
}
|
|
||||||
|
|
||||||
var barColor lipgloss.Style
|
|
||||||
switch {
|
|
||||||
case b.min < 100:
|
|
||||||
barColor = m.st.specialStyle
|
|
||||||
case b.min < 500:
|
|
||||||
barColor = m.st.warnStyle
|
|
||||||
default:
|
|
||||||
barColor = m.st.dangerStyle
|
|
||||||
}
|
|
||||||
|
|
||||||
bar := barColor.Render(strings.Repeat("█", fill))
|
|
||||||
if fill < barW {
|
|
||||||
bar += m.st.subtleStyle.Render(strings.Repeat("░", barW-fill))
|
|
||||||
}
|
|
||||||
|
|
||||||
label := fmt.Sprintf("%*s", labelW, b.label)
|
|
||||||
count := fmt.Sprintf("%*d", countW, b.count)
|
|
||||||
sb.WriteString(" " + m.st.subtleStyle.Render(label) + " " + bar + " " + count + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
@@ -156,8 +156,9 @@ func resolveSparklineIndex(x, sparkWidth, dataLen int) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor) string {
|
func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor) string {
|
||||||
|
allSites := m.engine.GetAllSites()
|
||||||
var childStatuses [][]bool
|
var childStatuses [][]bool
|
||||||
for _, s := range m.sites {
|
for _, s := range allSites {
|
||||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||||
hist, _ := m.engine.GetHistory(s.ID)
|
hist, _ := m.engine.GetHistory(s.ID)
|
||||||
if len(hist.Statuses) > 0 {
|
if len(hist.Statuses) > 0 {
|
||||||
@@ -208,8 +209,9 @@ func (m Model) groupSparkline(groupID int, width int, bg lipgloss.TerminalColor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) groupUptime(groupID int) string {
|
func (m Model) groupUptime(groupID int) string {
|
||||||
|
allSites := m.engine.GetAllSites()
|
||||||
var allStatuses [][]bool
|
var allStatuses [][]bool
|
||||||
for _, s := range m.sites {
|
for _, s := range allSites {
|
||||||
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
if s.ParentID == groupID && !s.Paused && !m.isMonitorInMaintenance(s.ID) {
|
||||||
hist, _ := m.engine.GetHistory(s.ID)
|
hist, _ := m.engine.GetHistory(s.ID)
|
||||||
if len(hist.Statuses) > 0 {
|
if len(hist.Statuses) > 0 {
|
||||||
|
|||||||
@@ -21,36 +21,27 @@ type maintFormData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
||||||
return m.maintSet[monitorID]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) buildMaintSet() {
|
|
||||||
set := make(map[int]bool)
|
|
||||||
now := time.Now()
|
|
||||||
for _, mw := range m.maintenanceWindows {
|
for _, mw := range m.maintenanceWindows {
|
||||||
if mw.Type != "maintenance" {
|
if mw.Type != "maintenance" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
now := time.Now()
|
||||||
if mw.StartTime.After(now) {
|
if mw.StartTime.After(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if mw.MonitorID == 0 {
|
if mw.MonitorID == 0 || mw.MonitorID == monitorID {
|
||||||
for _, s := range m.sites {
|
return true
|
||||||
set[s.ID] = true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
set[mw.MonitorID] = true
|
|
||||||
for _, s := range m.sites {
|
for _, s := range m.sites {
|
||||||
if s.ParentID == mw.MonitorID {
|
if s.ID == monitorID && s.ParentID > 0 && mw.MonitorID == s.ParentID {
|
||||||
set[s.ID] = true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.maintSet = set
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) initMaintHuhForm() tea.Cmd {
|
func (m *Model) initMaintHuhForm() tea.Cmd {
|
||||||
|
|||||||
+17
-49
@@ -37,6 +37,7 @@ const (
|
|||||||
colDot colKey = iota
|
colDot colKey = iota
|
||||||
colName
|
colName
|
||||||
colType
|
colType
|
||||||
|
colStatus
|
||||||
colLatency
|
colLatency
|
||||||
colUptime
|
colUptime
|
||||||
colHistory
|
colHistory
|
||||||
@@ -56,16 +57,15 @@ type columnDef struct {
|
|||||||
var siteColumns = []columnDef{
|
var siteColumns = []columnDef{
|
||||||
{colDot, "", "", 3, 3, 0},
|
{colDot, "", "", 3, 3, 0},
|
||||||
{colName, "NAME", "NAME", 0, 0, 0},
|
{colName, "NAME", "NAME", 0, 0, 0},
|
||||||
{colType, "TYPE", "TYPE", 10, 8, 0},
|
{colType, "TYPE", "TYPE", 10, 8, mediumBreakpoint},
|
||||||
|
{colStatus, "STATUS", "STATUS", 10, 10, 0},
|
||||||
{colLatency, "LATENCY", "LAT", 10, 7, 0},
|
{colLatency, "LATENCY", "LAT", 10, 7, 0},
|
||||||
{colUptime, "UPTIME", "UP%", 8, 8, 0},
|
{colUptime, "UPTIME", "UP%", 8, 8, mediumBreakpoint},
|
||||||
{colHistory, "HISTORY", "HISTORY", 0, 0, 0},
|
{colHistory, "HISTORY", "HISTORY", 0, 0, mediumBreakpoint},
|
||||||
{colSSL, "SSL", "SSL", 7, 5, 0},
|
{colSSL, "SSL", "SSL", 7, 5, wideBreakpoint},
|
||||||
{colRetries, "RETRIES", "RT", 9, 5, 0},
|
{colRetries, "RETRIES", "RT", 9, 5, wideBreakpoint},
|
||||||
}
|
}
|
||||||
|
|
||||||
var columnDropOrder = []colKey{colHistory, colRetries, colSSL, colUptime, colType}
|
|
||||||
|
|
||||||
type tableLayout struct {
|
type tableLayout struct {
|
||||||
nameW, sparkW int
|
nameW, sparkW int
|
||||||
headers []string
|
headers []string
|
||||||
@@ -76,51 +76,17 @@ type tableLayout struct {
|
|||||||
func (m Model) computeLayout() tableLayout {
|
func (m Model) computeLayout() tableLayout {
|
||||||
wide := m.isWide()
|
wide := m.isWide()
|
||||||
|
|
||||||
cw := m.contentWidth
|
|
||||||
if cw == 0 {
|
|
||||||
cw = m.termWidth
|
|
||||||
}
|
|
||||||
|
|
||||||
dropped := make(map[colKey]bool)
|
|
||||||
minNameW := 20
|
|
||||||
minSparkW := 12
|
|
||||||
|
|
||||||
for attempt := 0; attempt <= len(columnDropOrder); attempt++ {
|
|
||||||
var fixed int
|
|
||||||
var flexCount int
|
|
||||||
for _, c := range siteColumns {
|
|
||||||
if dropped[c.key] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
w := c.narrowW
|
|
||||||
if wide {
|
|
||||||
w = c.wideW
|
|
||||||
}
|
|
||||||
if w > 0 {
|
|
||||||
fixed += w
|
|
||||||
} else {
|
|
||||||
flexCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
numCols := len(siteColumns) - len(dropped)
|
|
||||||
borderOverhead := 2 + (numCols - 1)
|
|
||||||
avail := cw - chromePadH - 2 - borderOverhead - fixed
|
|
||||||
minFlex := minNameW
|
|
||||||
if flexCount > 1 {
|
|
||||||
minFlex += minSparkW
|
|
||||||
}
|
|
||||||
if avail >= minFlex || attempt >= len(columnDropOrder) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
dropped[columnDropOrder[attempt]] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var active []colKey
|
var active []colKey
|
||||||
var headers []string
|
var headers []string
|
||||||
var widths []int
|
var widths []int
|
||||||
var fixed int
|
var fixed int
|
||||||
|
|
||||||
|
cw := m.contentWidth
|
||||||
|
if cw == 0 {
|
||||||
|
cw = m.termWidth
|
||||||
|
}
|
||||||
for _, c := range siteColumns {
|
for _, c := range siteColumns {
|
||||||
if dropped[c.key] {
|
if c.minTerm > 0 && cw < c.minTerm {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
active = append(active, c.key)
|
active = append(active, c.key)
|
||||||
@@ -140,12 +106,12 @@ func (m Model) computeLayout() tableLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sortColMap := map[int]colKey{
|
sortColMap := map[int]colKey{
|
||||||
sortStatus: colDot,
|
sortStatus: colStatus,
|
||||||
sortName: colName,
|
sortName: colName,
|
||||||
sortLatency: colLatency,
|
sortLatency: colLatency,
|
||||||
}
|
}
|
||||||
sortableKeys := map[colKey]string{
|
sortableKeys := map[colKey]string{
|
||||||
colDot: "sort-status",
|
colStatus: "sort-status",
|
||||||
colName: "sort-name",
|
colName: "sort-name",
|
||||||
colLatency: "sort-latency",
|
colLatency: "sort-latency",
|
||||||
}
|
}
|
||||||
@@ -284,6 +250,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
colDot: m.fmtStatusDot(site.Status, site.Paused, inMaint),
|
colDot: m.fmtStatusDot(site.Status, site.Paused, inMaint),
|
||||||
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: m.fmtStatus(site.Status, site.Paused, inMaint),
|
||||||
colLatency: m.st.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),
|
||||||
@@ -332,6 +299,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
colDot: m.fmtStatusDot(site.Status, site.Paused, inMaint),
|
colDot: m.fmtStatusDot(site.Status, site.Paused, inMaint),
|
||||||
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: m.fmtStatus(site.Status, site.Paused, inMaint),
|
||||||
colLatency: m.fmtLatency(site.Latency),
|
colLatency: m.fmtLatency(site.Latency),
|
||||||
colUptime: m.fmtUptimeMaint(hist.Statuses, site.ID),
|
colUptime: m.fmtUptimeMaint(hist.Statuses, site.ID),
|
||||||
colHistory: spark,
|
colHistory: spark,
|
||||||
|
|||||||
+10
-12
@@ -119,17 +119,16 @@ const (
|
|||||||
type sessionState int
|
type sessionState int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
stateDashboard sessionState = 0
|
stateDashboard sessionState = 0
|
||||||
stateLogs sessionState = 1
|
stateLogs sessionState = 1
|
||||||
stateDetailFullscreen sessionState = 2
|
stateAlertDetail sessionState = 3
|
||||||
stateAlertDetail sessionState = 3
|
stateFormSite sessionState = 4
|
||||||
stateFormSite sessionState = 4
|
stateFormAlert sessionState = 5
|
||||||
stateFormAlert sessionState = 5
|
stateFormUser sessionState = 6
|
||||||
stateFormUser sessionState = 6
|
stateConfirmDelete sessionState = 7
|
||||||
stateConfirmDelete sessionState = 7
|
stateFormMaint sessionState = 8
|
||||||
stateFormMaint sessionState = 8
|
stateSettings sessionState = 11
|
||||||
stateSettings sessionState = 11
|
stateMaintDetail sessionState = 12
|
||||||
stateMaintDetail sessionState = 12
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type Model struct {
|
||||||
@@ -207,7 +206,6 @@ type Model struct {
|
|||||||
lastTabLoad time.Time // last dispatch of loadTabDataCmd (throttle)
|
lastTabLoad time.Time // last dispatch of loadTabDataCmd (throttle)
|
||||||
tabSeq int // seq of the newest issued tab-data load
|
tabSeq int // seq of the newest issued tab-data load
|
||||||
|
|
||||||
maintSet map[int]bool
|
|
||||||
bottomPanel bottomPanel
|
bottomPanel bottomPanel
|
||||||
detailOpen bool
|
detailOpen bool
|
||||||
maintCursor int
|
maintCursor int
|
||||||
|
|||||||
+2
-83
@@ -19,8 +19,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
case tabDataMsg:
|
case tabDataMsg:
|
||||||
return m.handleTabData(msg)
|
return m.handleTabData(msg)
|
||||||
case detailDataMsg:
|
case detailDataMsg:
|
||||||
detailVisible := m.detailOpen || m.state == stateDetailFullscreen
|
if m.detailOpen && m.cursor < len(m.sites) && m.sites[m.cursor].ID != msg.siteID {
|
||||||
if detailVisible && m.cursor < len(m.sites) && m.sites[m.cursor].ID != msg.siteID {
|
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
m.detailChanges = msg.changes
|
m.detailChanges = msg.changes
|
||||||
@@ -52,9 +51,6 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
if m.state == stateLogs {
|
if m.state == stateLogs {
|
||||||
return m.handleLogsFullscreen(msg)
|
return m.handleLogsFullscreen(msg)
|
||||||
}
|
}
|
||||||
if m.state == stateDetailFullscreen {
|
|
||||||
return m.handleDetailFullscreen(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
switch msg := msg.(type) {
|
||||||
case tea.MouseMsg:
|
case tea.MouseMsg:
|
||||||
@@ -207,7 +203,7 @@ func (m *Model) handleTick(t time.Time) (tea.Model, tea.Cmd) {
|
|||||||
// tab-data cadence, so a flap that happens while the panel is on screen shows
|
// tab-data cadence, so a flap that happens while the panel is on screen shows
|
||||||
// up without leaving and re-entering. Nil when no detail panel is open.
|
// up without leaving and re-entering. Nil when no detail panel is open.
|
||||||
func (m *Model) detailRefreshCmd() tea.Cmd {
|
func (m *Model) detailRefreshCmd() tea.Cmd {
|
||||||
if (!m.detailOpen && m.state != stateDetailFullscreen) || m.cursor >= len(m.sites) {
|
if !m.detailOpen || m.cursor >= len(m.sites) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return m.loadDetailCmd(m.sites[m.cursor].ID)
|
return m.loadDetailCmd(m.sites[m.cursor].ID)
|
||||||
@@ -231,7 +227,6 @@ func (m *Model) handleTabData(msg tabDataMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
m.nodes = msg.nodes
|
m.nodes = msg.nodes
|
||||||
m.maintenanceWindows = msg.maint
|
m.maintenanceWindows = msg.maint
|
||||||
m.buildMaintSet()
|
|
||||||
m.clampCursor()
|
m.clampCursor()
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
@@ -283,75 +278,6 @@ func (m *Model) handleLogsFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) handleDetailFullscreen(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case tea.KeyMsg:
|
|
||||||
switch msg.String() {
|
|
||||||
case "esc", "q":
|
|
||||||
m.state = stateDashboard
|
|
||||||
m.focusedPanel = panelMonitors
|
|
||||||
case "ctrl+c":
|
|
||||||
return m, tea.Quit
|
|
||||||
case "e":
|
|
||||||
return m.handleEditItem()
|
|
||||||
case "h":
|
|
||||||
if m.cursor < len(m.sites) {
|
|
||||||
site := m.sites[m.cursor]
|
|
||||||
m.historySiteName = site.Name
|
|
||||||
m.historySiteID = site.ID
|
|
||||||
m.historyChanges = nil
|
|
||||||
m.detailMode = detailHistory
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
return m, m.loadHistoryCmd(site.ID)
|
|
||||||
}
|
|
||||||
case "s":
|
|
||||||
if m.cursor < len(m.sites) {
|
|
||||||
site := m.sites[m.cursor]
|
|
||||||
m.slaSiteName = site.Name
|
|
||||||
m.slaSiteID = site.ID
|
|
||||||
m.slaPeriodIdx = 2
|
|
||||||
m.detailMode = detailSLA
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
return m, m.loadSLACmd(site.ID, m.slaPeriodIdx)
|
|
||||||
}
|
|
||||||
case "1", "2", "3", "4":
|
|
||||||
if m.detailMode == detailSLA {
|
|
||||||
idx := int(msg.String()[0]-'0') - 1
|
|
||||||
if idx >= 0 && idx < len(slaPeriods) {
|
|
||||||
m.slaPeriodIdx = idx
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
return m, m.loadSLACmd(m.slaSiteID, idx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "up", "k":
|
|
||||||
m.detailScrollOffset--
|
|
||||||
if m.detailScrollOffset < 0 {
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
}
|
|
||||||
case "down", "j":
|
|
||||||
m.detailScrollOffset++
|
|
||||||
case "pgup":
|
|
||||||
m.detailScrollOffset -= 10
|
|
||||||
if m.detailScrollOffset < 0 {
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
}
|
|
||||||
case "pgdown":
|
|
||||||
m.detailScrollOffset += 10
|
|
||||||
}
|
|
||||||
case tea.MouseMsg:
|
|
||||||
switch msg.Button {
|
|
||||||
case tea.MouseButtonWheelUp:
|
|
||||||
m.detailScrollOffset -= 3
|
|
||||||
if m.detailScrollOffset < 0 {
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
}
|
|
||||||
case tea.MouseButtonWheelDown:
|
|
||||||
m.detailScrollOffset += 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||||
if m.state != stateDashboard {
|
if m.state != stateDashboard {
|
||||||
return m, nil
|
return m, nil
|
||||||
@@ -747,13 +673,6 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
if len(m.sites) > 0 {
|
if len(m.sites) > 0 {
|
||||||
narrow := m.termWidth < wideBreakpoint
|
|
||||||
if narrow {
|
|
||||||
m.detailMode = detailDefault
|
|
||||||
m.detailScrollOffset = 0
|
|
||||||
m.state = stateDetailFullscreen
|
|
||||||
return m, m.loadDetailCmd(m.sites[m.cursor].ID)
|
|
||||||
}
|
|
||||||
m.detailOpen = !m.detailOpen
|
m.detailOpen = !m.detailOpen
|
||||||
m.detailMode = detailDefault
|
m.detailMode = detailDefault
|
||||||
m.detailScrollOffset = 0
|
m.detailScrollOffset = 0
|
||||||
|
|||||||
@@ -300,70 +300,6 @@ func TestWriteDoneMsg_LogsErrorAndReloads(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEnterNarrowTerminal_OpensFullscreen(t *testing.T) {
|
|
||||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
|
||||||
m := newTestModel(ms)
|
|
||||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "UP"}}}
|
|
||||||
m.termWidth = 80 // narrow, below wideBreakpoint (120)
|
|
||||||
m.termHeight = 40
|
|
||||||
m.focusedPanel = panelMonitors
|
|
||||||
|
|
||||||
updated, cmd := (&m).handleDashboardKey(keyMsg("enter"))
|
|
||||||
got := updated.(*Model)
|
|
||||||
if got.state != stateDetailFullscreen {
|
|
||||||
t.Fatalf("expected stateDetailFullscreen, got %d", got.state)
|
|
||||||
}
|
|
||||||
if cmd == nil {
|
|
||||||
t.Fatal("expected a detail load Cmd")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEnterWideTerminal_TogglesSidebar(t *testing.T) {
|
|
||||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
|
||||||
m := newTestModel(ms)
|
|
||||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}, SiteState: models.SiteState{Status: "UP"}}}
|
|
||||||
m.termWidth = 140 // wide, above wideBreakpoint (120)
|
|
||||||
m.termHeight = 40
|
|
||||||
m.focusedPanel = panelMonitors
|
|
||||||
|
|
||||||
updated, _ := (&m).handleDashboardKey(keyMsg("enter"))
|
|
||||||
got := updated.(*Model)
|
|
||||||
if got.state != stateDashboard {
|
|
||||||
t.Fatalf("expected stateDashboard, got %d", got.state)
|
|
||||||
}
|
|
||||||
if !got.detailOpen {
|
|
||||||
t.Fatal("expected detailOpen to be true")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetailFullscreen_EscReturnsToDashboard(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.state = stateDetailFullscreen
|
|
||||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 1, Name: "site"}}}
|
|
||||||
|
|
||||||
updated, _ := (&m).handleDetailFullscreen(tea.KeyMsg{Type: tea.KeyEsc})
|
|
||||||
got := updated.(*Model)
|
|
||||||
if got.state != stateDashboard {
|
|
||||||
t.Fatalf("expected stateDashboard after Esc, got %d", got.state)
|
|
||||||
}
|
|
||||||
if got.focusedPanel != panelMonitors {
|
|
||||||
t.Fatalf("expected panelMonitors focus, got %d", got.focusedPanel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetailRefreshCmd_FiresForFullscreen(t *testing.T) {
|
|
||||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
|
||||||
m := newTestModel(ms)
|
|
||||||
m.sites = []models.Site{{SiteConfig: models.SiteConfig{ID: 5, Name: "site"}}}
|
|
||||||
m.state = stateDetailFullscreen
|
|
||||||
m.detailOpen = false
|
|
||||||
|
|
||||||
cmd := (&m).detailRefreshCmd()
|
|
||||||
if cmd == nil {
|
|
||||||
t.Fatal("detailRefreshCmd should fire for stateDetailFullscreen")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
||||||
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
ms := &tuiMockStore{stateChanges: []models.StateChange{{FromStatus: "UP", ToStatus: "DOWN"}}}
|
||||||
m := newTestModel(ms)
|
m := newTestModel(ms)
|
||||||
@@ -389,86 +325,3 @@ func TestDetailRefreshCmd_OnlyWhileDetailOpen(t *testing.T) {
|
|||||||
t.Error("refresh Cmd issued for an out-of-range cursor")
|
t.Error("refresh Cmd issued for an out-of-range cursor")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildMaintSet_GlobalWindow(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.sites = []models.Site{
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 2, Name: "b"}},
|
|
||||||
}
|
|
||||||
m.maintenanceWindows = []models.MaintenanceWindow{
|
|
||||||
{ID: 1, MonitorID: 0, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
|
||||||
}
|
|
||||||
m.buildMaintSet()
|
|
||||||
if !m.maintSet[1] || !m.maintSet[2] {
|
|
||||||
t.Error("global maint window should mark all monitors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMaintSet_TargetedWindow(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.sites = []models.Site{
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 2, Name: "b"}},
|
|
||||||
}
|
|
||||||
m.maintenanceWindows = []models.MaintenanceWindow{
|
|
||||||
{ID: 1, MonitorID: 1, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
|
||||||
}
|
|
||||||
m.buildMaintSet()
|
|
||||||
if !m.maintSet[1] {
|
|
||||||
t.Error("targeted window should mark monitor 1")
|
|
||||||
}
|
|
||||||
if m.maintSet[2] {
|
|
||||||
t.Error("targeted window should NOT mark monitor 2")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMaintSet_GroupPropagates(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.sites = []models.Site{
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 10, Name: "group", Type: "group"}},
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 11, Name: "child1", ParentID: 10}},
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 12, Name: "child2", ParentID: 10}},
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 20, Name: "other"}},
|
|
||||||
}
|
|
||||||
m.maintenanceWindows = []models.MaintenanceWindow{
|
|
||||||
{ID: 1, MonitorID: 10, Type: "maintenance", StartTime: time.Now().Add(-time.Hour)},
|
|
||||||
}
|
|
||||||
m.buildMaintSet()
|
|
||||||
if !m.maintSet[10] || !m.maintSet[11] || !m.maintSet[12] {
|
|
||||||
t.Error("group maint window should mark group + children")
|
|
||||||
}
|
|
||||||
if m.maintSet[20] {
|
|
||||||
t.Error("unrelated monitor should NOT be marked")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMaintSet_ExpiredIgnored(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.sites = []models.Site{
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
|
||||||
}
|
|
||||||
m.maintenanceWindows = []models.MaintenanceWindow{
|
|
||||||
{ID: 1, MonitorID: 1, Type: "maintenance",
|
|
||||||
StartTime: time.Now().Add(-2 * time.Hour),
|
|
||||||
EndTime: time.Now().Add(-1 * time.Hour)},
|
|
||||||
}
|
|
||||||
m.buildMaintSet()
|
|
||||||
if m.maintSet[1] {
|
|
||||||
t.Error("expired maint window should not mark monitor")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildMaintSet_IncidentIgnored(t *testing.T) {
|
|
||||||
m := newTestModel(&tuiMockStore{})
|
|
||||||
m.sites = []models.Site{
|
|
||||||
{SiteConfig: models.SiteConfig{ID: 1, Name: "a"}},
|
|
||||||
}
|
|
||||||
m.maintenanceWindows = []models.MaintenanceWindow{
|
|
||||||
{ID: 1, MonitorID: 0, Type: "incident", StartTime: time.Now().Add(-time.Hour)},
|
|
||||||
}
|
|
||||||
m.buildMaintSet()
|
|
||||||
if m.maintSet[1] {
|
|
||||||
t.Error("incident windows should not mark monitors as in maintenance")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -92,8 +92,6 @@ func (m Model) View() string {
|
|||||||
return ""
|
return ""
|
||||||
case stateLogs:
|
case stateLogs:
|
||||||
return m.viewLogsFullscreen()
|
return m.viewLogsFullscreen()
|
||||||
case stateDetailFullscreen:
|
|
||||||
return m.viewDetailFullscreen()
|
|
||||||
case stateAlertDetail:
|
case stateAlertDetail:
|
||||||
return m.viewAlertDetailPanel()
|
return m.viewAlertDetailPanel()
|
||||||
case stateSettings:
|
case stateSettings:
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package tui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -23,72 +22,20 @@ func (m Model) viewDetailInline(width, height int) string {
|
|||||||
default:
|
default:
|
||||||
site := m.sites[m.cursor]
|
site := m.sites[m.cursor]
|
||||||
hist, _ := m.engine.GetHistory(site.ID)
|
hist, _ := m.engine.GetHistory(site.ID)
|
||||||
return m.buildDetailContent(site, hist, width, false)
|
return m.viewDetailSidebar(site, hist, width, height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) viewDetailFullscreen() string {
|
func (m Model) viewDetailSidebar(site models.Site, hist monitor.SiteHistory, width, _ int) string {
|
||||||
if m.cursor >= len(m.sites) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
availW := m.termWidth - chromePadH
|
|
||||||
site := m.sites[m.cursor]
|
|
||||||
|
|
||||||
var title string
|
|
||||||
switch m.detailMode {
|
|
||||||
case detailSLA:
|
|
||||||
title = "SLA · " + site.Name
|
|
||||||
case detailHistory:
|
|
||||||
title = "History · " + site.Name
|
|
||||||
default:
|
|
||||||
title = site.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if site.ParentID > 0 {
|
|
||||||
for _, s := range m.sites {
|
|
||||||
if s.ID == site.ParentID {
|
|
||||||
title = s.Name + " > " + title
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
innerW := availW - 2
|
|
||||||
|
|
||||||
var content string
|
|
||||||
switch m.detailMode {
|
|
||||||
case detailSLA:
|
|
||||||
content = m.viewSLASidebar(innerW, 0)
|
|
||||||
case detailHistory:
|
|
||||||
content = m.viewHistorySidebar(innerW, 0)
|
|
||||||
default:
|
|
||||||
hist, _ := m.engine.GetHistory(site.ID)
|
|
||||||
content = m.buildDetailContent(site, hist, innerW, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
footer := m.detailFooter(innerW)
|
|
||||||
|
|
||||||
panelH := m.termHeight - chromePadV
|
|
||||||
if panelH < 10 {
|
|
||||||
panelH = 10
|
|
||||||
}
|
|
||||||
|
|
||||||
return lipgloss.NewStyle().Padding(1, 2).Render(
|
|
||||||
m.titledPanelH(title, content, footer, availW, panelH, m.detailScrollOffset, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, width int, fullscreen bool) string {
|
|
||||||
dot := m.st.subtleStyle.Render(" · ")
|
dot := m.st.subtleStyle.Render(" · ")
|
||||||
label := m.st.subtleStyle
|
label := m.st.subtleStyle
|
||||||
|
var b strings.Builder
|
||||||
innerW := width - 4
|
innerW := width - 4
|
||||||
if innerW < 20 {
|
if innerW < 20 {
|
||||||
innerW = 20
|
innerW = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
var b strings.Builder
|
// Status + latency + last check
|
||||||
|
|
||||||
// Status + latency + last check + state since
|
|
||||||
status := m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))
|
status := m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID))
|
||||||
statusParts := []string{status}
|
statusParts := []string{status}
|
||||||
if site.Latency > 0 {
|
if site.Latency > 0 {
|
||||||
@@ -97,10 +44,6 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
if !site.LastCheck.IsZero() {
|
if !site.LastCheck.IsZero() {
|
||||||
statusParts = append(statusParts, m.fmtTimeAgo(site.LastCheck))
|
statusParts = append(statusParts, m.fmtTimeAgo(site.LastCheck))
|
||||||
}
|
}
|
||||||
if !site.StatusChangedAt.IsZero() {
|
|
||||||
dur := time.Since(site.StatusChangedAt)
|
|
||||||
statusParts = append(statusParts, label.Render("for")+" "+fmtDuration(dur))
|
|
||||||
}
|
|
||||||
b.WriteString(" " + strings.Join(statusParts, dot) + "\n")
|
b.WriteString(" " + strings.Join(statusParts, dot) + "\n")
|
||||||
|
|
||||||
// Type-specific details
|
// Type-specific details
|
||||||
@@ -109,10 +52,7 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
b.WriteString(" " + strings.Join(typeParts, dot) + "\n")
|
b.WriteString(" " + strings.Join(typeParts, dot) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extended endpoint fields
|
// Uptime + retries
|
||||||
m.writeEndpointFields(&b, site, label, innerW, fullscreen)
|
|
||||||
|
|
||||||
// Uptime + retries + last success
|
|
||||||
uptimeStr := m.fmtUptime(hist.Statuses)
|
uptimeStr := m.fmtUptime(hist.Statuses)
|
||||||
if m.isMonitorInMaintenance(site.ID) {
|
if m.isMonitorInMaintenance(site.ID) {
|
||||||
uptimeStr = m.st.subtleStyle.Render("—")
|
uptimeStr = m.st.subtleStyle.Render("—")
|
||||||
@@ -121,21 +61,8 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
if site.Type != "group" && site.MaxRetries > 0 {
|
if site.Type != "group" && site.MaxRetries > 0 {
|
||||||
uptimeParts = append(uptimeParts, label.Render("Retries")+" "+m.fmtRetries(site))
|
uptimeParts = append(uptimeParts, label.Render("Retries")+" "+m.fmtRetries(site))
|
||||||
}
|
}
|
||||||
if site.Type != "push" && !site.LastSuccessAt.IsZero() {
|
|
||||||
uptimeParts = append(uptimeParts, label.Render("Last OK")+" "+m.fmtTimeAgo(site.LastSuccessAt))
|
|
||||||
}
|
|
||||||
b.WriteString(" " + strings.Join(uptimeParts, dot) + "\n")
|
b.WriteString(" " + strings.Join(uptimeParts, dot) + "\n")
|
||||||
|
|
||||||
// Maintenance window name
|
|
||||||
if m.isMonitorInMaintenance(site.ID) {
|
|
||||||
for _, mw := range m.maintenanceWindows {
|
|
||||||
if mw.Type == "maintenance" && (mw.MonitorID == 0 || mw.MonitorID == site.ID || mw.MonitorID == site.ParentID) {
|
|
||||||
b.WriteString(" " + label.Render("Maint") + " " + m.st.maintStyle.Render(mw.Title) + "\n")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error line
|
// Error line
|
||||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp ||
|
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp ||
|
||||||
site.Status == models.StatusLate || site.Status == models.StatusStale) && site.LastError != "" {
|
site.Status == models.StatusLate || site.Status == models.StatusStale) && site.LastError != "" {
|
||||||
@@ -146,62 +73,8 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
b.WriteString(" " + label.Render("Error") + " " + m.st.dangerStyle.Render(limitStr(site.LastError, errW)) + "\n")
|
b.WriteString(" " + label.Render("Error") + " " + m.st.dangerStyle.Render(limitStr(site.LastError, errW)) + "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connection chain
|
|
||||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp) && site.LastError != "" {
|
|
||||||
chain := connectionChain(site.LastError, site.Type, site.StatusCode, strings.HasPrefix(site.URL, "https"))
|
|
||||||
if len(chain) > 0 {
|
|
||||||
b.WriteString("\n")
|
|
||||||
for _, step := range chain {
|
|
||||||
var icon string
|
|
||||||
switch step.Status {
|
|
||||||
case stepPassed:
|
|
||||||
icon = m.st.specialStyle.Render("✓")
|
|
||||||
case stepFailed:
|
|
||||||
icon = m.st.dangerStyle.Render("✗")
|
|
||||||
case stepSkipped:
|
|
||||||
icon = m.st.subtleStyle.Render("·")
|
|
||||||
}
|
|
||||||
line := fmt.Sprintf(" %s %-16s", icon, step.Name)
|
|
||||||
if step.Detail != "" {
|
|
||||||
switch step.Status {
|
|
||||||
case stepFailed:
|
|
||||||
line += " " + m.st.dangerStyle.Render(step.Detail)
|
|
||||||
case stepSkipped:
|
|
||||||
line += " " + m.st.subtleStyle.Render(step.Detail)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
b.WriteString(line + "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
// Probe results
|
|
||||||
probeResults := m.engine.GetProbeResults(site.ID)
|
|
||||||
if len(probeResults) > 0 {
|
|
||||||
nodeIDs := make([]string, 0, len(probeResults))
|
|
||||||
for id := range probeResults {
|
|
||||||
nodeIDs = append(nodeIDs, id)
|
|
||||||
}
|
|
||||||
sort.Strings(nodeIDs)
|
|
||||||
for _, nodeID := range nodeIDs {
|
|
||||||
result := probeResults[nodeID]
|
|
||||||
probeStatus := m.st.specialStyle.Render("UP")
|
|
||||||
if !result.IsUp {
|
|
||||||
probeStatus = m.st.dangerStyle.Render("DN")
|
|
||||||
}
|
|
||||||
latency := time.Duration(result.LatencyNs).Milliseconds()
|
|
||||||
ago := time.Since(result.CheckedAt).Truncate(time.Second)
|
|
||||||
line := fmt.Sprintf(" %-14s %s %dms %s ago", nodeID, probeStatus, latency, ago)
|
|
||||||
if !result.IsUp && result.ErrorReason != "" {
|
|
||||||
line += " " + m.st.dangerStyle.Render(result.ErrorReason)
|
|
||||||
}
|
|
||||||
b.WriteString(line + "\n")
|
|
||||||
}
|
|
||||||
b.WriteString("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Latency chart
|
// Latency chart
|
||||||
if len(hist.Latencies) > 0 {
|
if len(hist.Latencies) > 0 {
|
||||||
chart := m.latencyChart(hist.Latencies, hist.Statuses, innerW, 3)
|
chart := m.latencyChart(hist.Latencies, hist.Statuses, innerW, 3)
|
||||||
@@ -243,15 +116,6 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Latency histogram
|
|
||||||
if site.Type != "push" && len(hist.Latencies) > 5 {
|
|
||||||
histContent := m.latencyHistogram(hist.Latencies, hist.Statuses, innerW)
|
|
||||||
if histContent != "" {
|
|
||||||
b.WriteString("\n")
|
|
||||||
b.WriteString(histContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
|
|
||||||
// State changes
|
// State changes
|
||||||
@@ -286,47 +150,6 @@ func (m Model) buildDetailContent(site models.Site, hist monitor.SiteHistory, wi
|
|||||||
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
return lipgloss.NewStyle().Width(width).MaxWidth(width).Render(b.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m Model) writeEndpointFields(b *strings.Builder, site models.Site, label lipgloss.Style, innerW int, fullscreen bool) {
|
|
||||||
dot := m.st.subtleStyle.Render(" · ")
|
|
||||||
var fields []string
|
|
||||||
|
|
||||||
if site.Interval > 0 {
|
|
||||||
fields = append(fields, label.Render("Every")+" "+fmt.Sprintf("%ds", site.Interval))
|
|
||||||
}
|
|
||||||
if site.Timeout > 0 {
|
|
||||||
fields = append(fields, label.Render("Timeout")+" "+fmt.Sprintf("%ds", site.Timeout))
|
|
||||||
}
|
|
||||||
if site.Type == "http" && site.Method != "" && site.Method != "GET" {
|
|
||||||
fields = append(fields, label.Render("Method")+" "+site.Method)
|
|
||||||
}
|
|
||||||
if site.Type == "http" {
|
|
||||||
codes := site.AcceptedCodes
|
|
||||||
if codes == "" {
|
|
||||||
codes = "200-299"
|
|
||||||
}
|
|
||||||
fields = append(fields, label.Render("Codes")+" "+codes)
|
|
||||||
}
|
|
||||||
if site.Regions != "" {
|
|
||||||
fields = append(fields, label.Render("Regions")+" "+site.Regions)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(fields) > 0 {
|
|
||||||
b.WriteString(" " + strings.Join(fields, dot) + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if site.Description != "" {
|
|
||||||
maxDescW := innerW
|
|
||||||
if !fullscreen && maxDescW > 60 {
|
|
||||||
maxDescW = 60
|
|
||||||
}
|
|
||||||
b.WriteString(" " + label.Render(limitStr(site.Description, maxDescW)) + "\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
if site.Type == "push" && site.Token != "" {
|
|
||||||
b.WriteString(" " + label.Render("Token") + " " + site.Token + "\n")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m Model) detailTypeLine(site models.Site) []string {
|
func (m Model) detailTypeLine(site models.Site) []string {
|
||||||
label := m.st.subtleStyle
|
label := m.st.subtleStyle
|
||||||
var parts []string
|
var parts []string
|
||||||
|
|||||||
Reference in New Issue
Block a user