feat: add incident management and maintenance windows
Maintenance windows suppress alerts during planned downtime while checks continue running. Incidents provide informational tracking. Supports targeting all monitors, single monitor, or group (applies to children). New Maint tab in TUI with create/end/delete. Status page, JSON API, and Prometheus metrics all reflect maintenance state.
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/models"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
var maintStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#bb9af7"))
|
||||
|
||||
type maintFormData struct {
|
||||
Title string
|
||||
Description string
|
||||
Type string
|
||||
MonitorID string
|
||||
Duration string
|
||||
CustomHours string
|
||||
}
|
||||
|
||||
func fmtMaintStatus(mw models.MaintenanceWindow) string {
|
||||
now := time.Now()
|
||||
if mw.StartTime.After(now) {
|
||||
return warnStyle.Render("SCHEDULED")
|
||||
}
|
||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||
return subtleStyle.Render("ENDED")
|
||||
}
|
||||
return specialStyle.Render("ACTIVE")
|
||||
}
|
||||
|
||||
func fmtMaintType(t string) string {
|
||||
if t == "incident" {
|
||||
return dangerStyle.Render("incident")
|
||||
}
|
||||
return maintStyle.Render("maintenance")
|
||||
}
|
||||
|
||||
func fmtMaintMonitor(monitorID int, sites []models.Site) string {
|
||||
if monitorID == 0 {
|
||||
return "All"
|
||||
}
|
||||
for _, s := range sites {
|
||||
if s.ID == monitorID {
|
||||
return limitStr(s.Name, 18)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("#%d", monitorID)
|
||||
}
|
||||
|
||||
func fmtMaintTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return subtleStyle.Render("—")
|
||||
}
|
||||
now := time.Now()
|
||||
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
||||
return t.Format("15:04")
|
||||
}
|
||||
return t.Format("15:04 Jan 02")
|
||||
}
|
||||
|
||||
func (m Model) isMonitorInMaintenance(monitorID int) bool {
|
||||
for _, mw := range m.maintenanceWindows {
|
||||
if mw.Type != "maintenance" {
|
||||
continue
|
||||
}
|
||||
now := time.Now()
|
||||
if mw.StartTime.After(now) {
|
||||
continue
|
||||
}
|
||||
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
||||
continue
|
||||
}
|
||||
if mw.MonitorID == 0 || mw.MonitorID == monitorID {
|
||||
return true
|
||||
}
|
||||
for _, s := range m.sites {
|
||||
if s.ID == monitorID && s.ParentID > 0 && mw.MonitorID == s.ParentID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m Model) viewMaintTab() string {
|
||||
if len(m.maintenanceWindows) == 0 {
|
||||
return "\n No maintenance windows or incidents. Press [n] to create one."
|
||||
}
|
||||
|
||||
return m.renderTable(
|
||||
[]string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"},
|
||||
len(m.maintenanceWindows),
|
||||
func(start, end int) [][]string {
|
||||
var rows [][]string
|
||||
allSites := m.engine.GetAllSites()
|
||||
for i := start; i < end; i++ {
|
||||
mw := m.maintenanceWindows[i]
|
||||
rows = append(rows, []string{
|
||||
strconv.Itoa(i + 1),
|
||||
m.zones.Mark(fmt.Sprintf("maint-%d", i), limitStr(mw.Title, 24)),
|
||||
fmtMaintType(mw.Type),
|
||||
fmtMaintMonitor(mw.MonitorID, allSites),
|
||||
fmtMaintStatus(mw),
|
||||
fmtMaintTime(mw.StartTime),
|
||||
fmtMaintTime(mw.EndTime),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
},
|
||||
[]int{6, 0, 14, 20, 12, 16, 16},
|
||||
nil,
|
||||
)
|
||||
}
|
||||
|
||||
func (m *Model) initMaintHuhForm() tea.Cmd {
|
||||
m.maintFormData = &maintFormData{
|
||||
Type: "maintenance",
|
||||
MonitorID: "0",
|
||||
Duration: "1h",
|
||||
CustomHours: "12",
|
||||
}
|
||||
|
||||
monitorOpts := []huh.Option[string]{huh.NewOption("All Monitors", "0")}
|
||||
allSites := m.engine.GetAllSites()
|
||||
for _, s := range allSites {
|
||||
label := s.Name
|
||||
if s.Type == "group" {
|
||||
label = s.Name + " (group)"
|
||||
}
|
||||
monitorOpts = append(monitorOpts, huh.NewOption(label, strconv.Itoa(s.ID)))
|
||||
}
|
||||
|
||||
m.huhForm = huh.NewForm(
|
||||
huh.NewGroup(
|
||||
huh.NewInput().Title("Title").
|
||||
Placeholder("DB Migration").
|
||||
Value(&m.maintFormData.Title).
|
||||
Validate(func(s string) error {
|
||||
if s == "" {
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
huh.NewSelect[string]().Title("Type").
|
||||
Options(
|
||||
huh.NewOption("Maintenance (suppress alerts)", "maintenance"),
|
||||
huh.NewOption("Incident (informational)", "incident"),
|
||||
).Value(&m.maintFormData.Type),
|
||||
huh.NewSelect[string]().Title("Affected Monitors").
|
||||
Options(monitorOpts...).
|
||||
Value(&m.maintFormData.MonitorID),
|
||||
huh.NewInput().Title("Description").
|
||||
Placeholder("Optional notes").
|
||||
Value(&m.maintFormData.Description),
|
||||
).Title("Maintenance Window"),
|
||||
huh.NewGroup(
|
||||
huh.NewSelect[string]().Title("Duration").
|
||||
Options(
|
||||
huh.NewOption("1 hour", "1h"),
|
||||
huh.NewOption("2 hours", "2h"),
|
||||
huh.NewOption("4 hours", "4h"),
|
||||
huh.NewOption("8 hours", "8h"),
|
||||
huh.NewOption("Indefinite (end manually)", "indefinite"),
|
||||
huh.NewOption("Custom", "custom"),
|
||||
).Value(&m.maintFormData.Duration),
|
||||
huh.NewInput().Title("Custom Duration (hours)").
|
||||
Placeholder("12").
|
||||
Value(&m.maintFormData.CustomHours).
|
||||
Validate(func(s string) error {
|
||||
if m.maintFormData.Duration != "custom" {
|
||||
return nil
|
||||
}
|
||||
v, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("must be a number")
|
||||
}
|
||||
if v < 1 {
|
||||
return fmt.Errorf("must be at least 1 hour")
|
||||
}
|
||||
return nil
|
||||
}),
|
||||
).Title("Duration").WithHideFunc(func() bool {
|
||||
return m.maintFormData.Type == "incident"
|
||||
}),
|
||||
).WithTheme(huh.ThemeDracula())
|
||||
|
||||
return m.huhForm.Init()
|
||||
}
|
||||
|
||||
func (m *Model) submitMaintForm() {
|
||||
d := m.maintFormData
|
||||
monitorID, _ := strconv.Atoi(d.MonitorID)
|
||||
|
||||
mw := models.MaintenanceWindow{
|
||||
MonitorID: monitorID,
|
||||
Title: d.Title,
|
||||
Description: d.Description,
|
||||
Type: d.Type,
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
|
||||
if d.Type == "maintenance" {
|
||||
switch d.Duration {
|
||||
case "1h":
|
||||
mw.EndTime = mw.StartTime.Add(1 * time.Hour)
|
||||
case "2h":
|
||||
mw.EndTime = mw.StartTime.Add(2 * time.Hour)
|
||||
case "4h":
|
||||
mw.EndTime = mw.StartTime.Add(4 * time.Hour)
|
||||
case "8h":
|
||||
mw.EndTime = mw.StartTime.Add(8 * time.Hour)
|
||||
case "custom":
|
||||
hours, _ := strconv.Atoi(d.CustomHours)
|
||||
if hours < 1 {
|
||||
hours = 1
|
||||
}
|
||||
mw.EndTime = mw.StartTime.Add(time.Duration(hours) * time.Hour)
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.store.AddMaintenanceWindow(mw); err != nil {
|
||||
m.engine.AddLog("Add maintenance window failed: " + err.Error())
|
||||
}
|
||||
m.state = stateDashboard
|
||||
}
|
||||
@@ -207,10 +207,13 @@ func fmtRetries(site models.Site) string {
|
||||
return s
|
||||
}
|
||||
|
||||
func fmtStatus(status string, paused bool) string {
|
||||
func fmtStatus(status string, paused bool, inMaint bool) string {
|
||||
if paused {
|
||||
return warnStyle.Render("PAUSED")
|
||||
}
|
||||
if inMaint {
|
||||
return maintStyle.Render("MAINT")
|
||||
}
|
||||
switch {
|
||||
case status == "DOWN" || status == "SSL EXP":
|
||||
return dangerStyle.Render(status)
|
||||
@@ -280,7 +283,7 @@ func (m Model) viewSitesTab() string {
|
||||
strconv.Itoa(i + 1),
|
||||
m.zones.Mark(fmt.Sprintf("site-%d", i), icon+" "+limitStr(site.Name, nameW-2)),
|
||||
"group",
|
||||
fmtStatus(site.Status, site.Paused),
|
||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||
subtleStyle.Render("—"),
|
||||
subtleStyle.Render("—"),
|
||||
subtleStyle.Render(strings.Repeat("·", sparkWidth)),
|
||||
@@ -313,7 +316,7 @@ func (m Model) viewSitesTab() string {
|
||||
strconv.Itoa(i + 1),
|
||||
m.zones.Mark(fmt.Sprintf("site-%d", i), name),
|
||||
typeIcon(site.Type, false) + " " + site.Type,
|
||||
fmtStatus(site.Status, site.Paused),
|
||||
fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)),
|
||||
fmtLatency(site.Latency),
|
||||
fmtUptime(hist.Statuses),
|
||||
spark,
|
||||
@@ -623,7 +626,15 @@ func (m Model) viewDetailPanel() string {
|
||||
b.WriteString(fmt.Sprintf(" %-16s %s\n", subtleStyle.Render(label), value))
|
||||
}
|
||||
|
||||
row("Status", fmtStatus(site.Status, site.Paused))
|
||||
row("Status", fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
||||
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) {
|
||||
row("Maintenance", maintStyle.Render(mw.Title))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
row("Type", site.Type)
|
||||
if site.URL != "" {
|
||||
row("URL", site.URL)
|
||||
|
||||
+97
-20
@@ -42,6 +42,7 @@ const (
|
||||
stateFormAlert
|
||||
stateFormUser
|
||||
stateConfirmDelete
|
||||
stateFormMaint
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
@@ -59,6 +60,7 @@ type Model struct {
|
||||
siteFormData *siteFormData
|
||||
alertFormData *alertFormData
|
||||
userFormData *userFormData
|
||||
maintFormData *maintFormData
|
||||
|
||||
logViewport viewport.Model
|
||||
isAdmin bool
|
||||
@@ -78,10 +80,11 @@ type Model struct {
|
||||
pulseVel float64
|
||||
tickCount int
|
||||
|
||||
sites []models.Site
|
||||
alerts []models.AlertConfig
|
||||
users []models.User
|
||||
nodes []models.ProbeNode
|
||||
sites []models.Site
|
||||
alerts []models.AlertConfig
|
||||
users []models.User
|
||||
nodes []models.ProbeNode
|
||||
maintenanceWindows []models.MaintenanceWindow
|
||||
|
||||
filterMode bool
|
||||
filterText string
|
||||
@@ -128,7 +131,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.engine.AddLog("Delete alert failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.alerts) - 1)
|
||||
case 3:
|
||||
case 4:
|
||||
if err := m.store.DeleteMaintenanceWindow(m.deleteID); err != nil {
|
||||
m.engine.AddLog("Delete maintenance window failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.maintenanceWindows) - 1)
|
||||
case 5:
|
||||
if err := m.store.DeleteUser(m.deleteID); err != nil {
|
||||
m.engine.AddLog("Delete user failed: " + err.Error())
|
||||
}
|
||||
@@ -136,12 +144,12 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
m.refreshData()
|
||||
m.state = stateDashboard
|
||||
if m.deleteTab == 4 {
|
||||
if m.deleteTab == 5 {
|
||||
m.state = stateUsers
|
||||
}
|
||||
case "n", "N", "esc":
|
||||
m.state = stateDashboard
|
||||
if m.deleteTab == 4 {
|
||||
if m.deleteTab == 5 {
|
||||
m.state = stateUsers
|
||||
}
|
||||
case "ctrl+c":
|
||||
@@ -152,7 +160,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
// Form state: forward ALL messages to huh (keys, timers, resize, etc.)
|
||||
if m.state == stateFormSite || m.state == stateFormAlert || m.state == stateFormUser {
|
||||
if m.state == stateFormSite || m.state == stateFormAlert || m.state == stateFormUser || m.state == stateFormMaint {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
if keyMsg.String() == "ctrl+c" {
|
||||
return m, tea.Quit
|
||||
@@ -160,7 +168,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg.String() == "esc" {
|
||||
m.huhForm = nil
|
||||
m.state = stateDashboard
|
||||
if m.currentTab == 4 {
|
||||
if m.currentTab == 5 {
|
||||
m.state = stateUsers
|
||||
}
|
||||
return m, nil
|
||||
@@ -226,6 +234,8 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
} else if m.currentTab == 3 {
|
||||
listLen = len(m.nodes)
|
||||
} else if m.currentTab == 4 {
|
||||
listLen = len(m.maintenanceWindows)
|
||||
} else if m.currentTab == 5 {
|
||||
listLen = len(m.users)
|
||||
}
|
||||
if msg.Button == tea.MouseButtonWheelUp {
|
||||
@@ -331,6 +341,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
max = len(m.nodes) - 1
|
||||
}
|
||||
if m.currentTab == 4 {
|
||||
max = len(m.maintenanceWindows) - 1
|
||||
}
|
||||
if m.currentTab == 5 {
|
||||
max = len(m.users) - 1
|
||||
}
|
||||
if m.cursor < max {
|
||||
@@ -349,7 +362,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
} else if m.currentTab == 1 {
|
||||
m.state = stateFormAlert
|
||||
return m, m.initAlertHuhForm()
|
||||
} else if m.currentTab == 4 && m.isAdmin {
|
||||
} else if m.currentTab == 4 {
|
||||
m.state = stateFormMaint
|
||||
return m, m.initMaintHuhForm()
|
||||
} else if m.currentTab == 5 && m.isAdmin {
|
||||
m.state = stateFormUser
|
||||
return m, m.initUserHuhForm()
|
||||
}
|
||||
@@ -363,7 +379,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.editID = m.alerts[m.cursor].ID
|
||||
m.state = stateFormAlert
|
||||
return m, m.initAlertHuhForm()
|
||||
} else if m.currentTab == 4 && m.isAdmin && len(m.users) > 0 {
|
||||
} else if m.currentTab == 5 && m.isAdmin && len(m.users) > 0 {
|
||||
m.editID = m.users[m.cursor].ID
|
||||
m.state = stateFormUser
|
||||
return m, m.initUserHuhForm()
|
||||
@@ -386,6 +402,18 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
||||
m.state = stateDetail
|
||||
}
|
||||
case "x":
|
||||
if m.currentTab == 4 && len(m.maintenanceWindows) > 0 {
|
||||
mw := m.maintenanceWindows[m.cursor]
|
||||
now := time.Now()
|
||||
isActive := !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now))
|
||||
if isActive {
|
||||
if err := m.store.EndMaintenanceWindow(mw.ID); err != nil {
|
||||
m.engine.AddLog("End maintenance failed: " + err.Error())
|
||||
}
|
||||
m.refreshData()
|
||||
}
|
||||
}
|
||||
case "d", "backspace":
|
||||
if m.currentTab == 0 && len(m.sites) > 0 {
|
||||
m.deleteID = m.sites[m.cursor].ID
|
||||
@@ -397,10 +425,15 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
m.deleteName = m.alerts[m.cursor].Name
|
||||
m.deleteTab = 1
|
||||
m.state = stateConfirmDelete
|
||||
} else if m.currentTab == 4 && m.isAdmin && len(m.users) > 0 {
|
||||
} else if m.currentTab == 4 && len(m.maintenanceWindows) > 0 {
|
||||
m.deleteID = m.maintenanceWindows[m.cursor].ID
|
||||
m.deleteName = m.maintenanceWindows[m.cursor].Title
|
||||
m.deleteTab = 4
|
||||
m.state = stateConfirmDelete
|
||||
} else if m.currentTab == 5 && m.isAdmin && len(m.users) > 0 {
|
||||
m.deleteID = m.users[m.cursor].ID
|
||||
m.deleteName = m.users[m.cursor].Username
|
||||
m.deleteTab = 4
|
||||
m.deleteTab = 5
|
||||
m.state = stateConfirmDelete
|
||||
}
|
||||
}
|
||||
@@ -410,9 +443,9 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
tabCount := 4
|
||||
tabCount := 5
|
||||
if m.isAdmin {
|
||||
tabCount = 5
|
||||
tabCount = 6
|
||||
}
|
||||
for i := 0; i < tabCount; i++ {
|
||||
if m.zones.Get(fmt.Sprintf("tab-%d", i)).InBounds(msg) {
|
||||
@@ -448,6 +481,19 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
if m.currentTab == 4 {
|
||||
end := m.tableOffset + m.maxTableRows
|
||||
if end > len(m.maintenanceWindows) {
|
||||
end = len(m.maintenanceWindows)
|
||||
}
|
||||
for i := m.tableOffset; i < end; i++ {
|
||||
if m.zones.Get(fmt.Sprintf("maint-%d", i)).InBounds(msg) {
|
||||
m.cursor = i
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if m.currentTab == 5 {
|
||||
end := m.tableOffset + m.maxTableRows
|
||||
if end > len(m.users) {
|
||||
end = len(m.users)
|
||||
@@ -464,9 +510,9 @@ func (m *Model) handleClick(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func (m *Model) switchTab(idx int) {
|
||||
maxTabs := 3
|
||||
maxTabs := 4
|
||||
if m.isAdmin {
|
||||
maxTabs = 4
|
||||
maxTabs = 5
|
||||
}
|
||||
if idx > maxTabs {
|
||||
idx = 0
|
||||
@@ -477,7 +523,7 @@ func (m *Model) switchTab(idx int) {
|
||||
switch idx {
|
||||
case 2:
|
||||
m.state = stateLogs
|
||||
case 4:
|
||||
case 5:
|
||||
m.state = stateUsers
|
||||
default:
|
||||
m.state = stateDashboard
|
||||
@@ -550,6 +596,9 @@ func (m *Model) refreshData() {
|
||||
if nodes, err := m.store.GetAllNodes(); err == nil {
|
||||
m.nodes = nodes
|
||||
}
|
||||
if windows, err := m.store.GetAllMaintenanceWindows(100); err == nil {
|
||||
m.maintenanceWindows = windows
|
||||
}
|
||||
m.logViewport.SetContent(strings.Join(m.engine.GetLogs(), "\n"))
|
||||
|
||||
listLen := len(m.sites)
|
||||
@@ -558,6 +607,8 @@ func (m *Model) refreshData() {
|
||||
} else if m.currentTab == 3 {
|
||||
listLen = len(m.nodes)
|
||||
} else if m.currentTab == 4 {
|
||||
listLen = len(m.maintenanceWindows)
|
||||
} else if m.currentTab == 5 {
|
||||
listLen = len(m.users)
|
||||
}
|
||||
if listLen > 0 && m.cursor >= listLen {
|
||||
@@ -582,6 +633,10 @@ func (m *Model) submitForm() {
|
||||
if m.userFormData != nil {
|
||||
m.submitUserForm()
|
||||
}
|
||||
case stateFormMaint:
|
||||
if m.maintFormData != nil {
|
||||
m.submitMaintForm()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,6 +669,8 @@ func (m Model) View() string {
|
||||
if m.deleteTab == 1 {
|
||||
kind = "alert"
|
||||
} else if m.deleteTab == 4 {
|
||||
kind = "maintenance window"
|
||||
} else if m.deleteTab == 5 {
|
||||
kind = "user"
|
||||
}
|
||||
msg := dangerStyle.Render(fmt.Sprintf("Delete %s \"%s\"?", kind, m.deleteName))
|
||||
@@ -624,7 +681,7 @@ func (m Model) View() string {
|
||||
Padding(1, 3).
|
||||
Render(msg + "\n\n" + hint)
|
||||
return lipgloss.NewStyle().Padding(2, 4).Render(box)
|
||||
case stateFormSite, stateFormAlert, stateFormUser:
|
||||
case stateFormSite, stateFormAlert, stateFormUser, stateFormMaint:
|
||||
if m.huhForm != nil {
|
||||
title := ""
|
||||
switch m.state {
|
||||
@@ -643,6 +700,8 @@ func (m Model) View() string {
|
||||
if m.editID > 0 {
|
||||
title = fmt.Sprintf("Edit User #%d", m.editID)
|
||||
}
|
||||
case stateFormMaint:
|
||||
title = "New Maintenance Window"
|
||||
}
|
||||
header := titleStyle.Render(title)
|
||||
footer := subtleStyle.Render("\n[Esc] Cancel")
|
||||
@@ -687,7 +746,21 @@ func (m Model) viewDashboard() string {
|
||||
nodesLabel = "Nodes"
|
||||
}
|
||||
|
||||
tabs := []string{sitesLabel, "Alerts", "Logs", nodesLabel}
|
||||
activeMaint := 0
|
||||
for _, mw := range m.maintenanceWindows {
|
||||
now := time.Now()
|
||||
if !mw.StartTime.After(now) && (mw.EndTime.IsZero() || mw.EndTime.After(now)) {
|
||||
activeMaint++
|
||||
}
|
||||
}
|
||||
var maintLabel string
|
||||
if activeMaint > 0 {
|
||||
maintLabel = fmt.Sprintf("Maint (%d)", activeMaint)
|
||||
} else {
|
||||
maintLabel = "Maint"
|
||||
}
|
||||
|
||||
tabs := []string{sitesLabel, "Alerts", "Logs", nodesLabel, maintLabel}
|
||||
if m.isAdmin {
|
||||
tabs = append(tabs, "Users")
|
||||
}
|
||||
@@ -717,6 +790,8 @@ func (m Model) viewDashboard() string {
|
||||
case 3:
|
||||
content = m.viewNodesTab()
|
||||
case 4:
|
||||
content = m.viewMaintTab()
|
||||
case 5:
|
||||
if m.isAdmin {
|
||||
content = m.viewUsersTab()
|
||||
}
|
||||
@@ -751,6 +826,8 @@ func (m Model) viewDashboard() string {
|
||||
case 0:
|
||||
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Tab]Switch [q]Quit"
|
||||
case 4:
|
||||
keys = "[n]New [x]End [d]Del [Tab]Switch [q]Quit"
|
||||
case 5:
|
||||
keys = "[n]Add [d]Revoke [Tab]Switch [q]Quit"
|
||||
default:
|
||||
keys = "[Tab]Switch [q]Quit"
|
||||
|
||||
Reference in New Issue
Block a user