3a089e7c1d
Warn via log when creating a maintenance window that overlaps with an existing one for the same monitor, parent group, or global scope. Handles both timed and indefinite windows.
267 lines
6.6 KiB
Go
267 lines
6.6 KiB
Go
package tui
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/huh"
|
|
)
|
|
|
|
type maintFormData struct {
|
|
Title string
|
|
Description string
|
|
Type string
|
|
MonitorID string
|
|
Duration string
|
|
CustomHours string
|
|
}
|
|
|
|
func (m Model) fmtMaintStatus(mw models.MaintenanceWindow) string {
|
|
now := time.Now()
|
|
if mw.StartTime.After(now) {
|
|
return m.st.warnStyle.Render("SCHEDULED")
|
|
}
|
|
if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
|
return m.st.subtleStyle.Render("ENDED")
|
|
}
|
|
return m.st.specialStyle.Render("ACTIVE")
|
|
}
|
|
|
|
func (m Model) fmtMaintType(t string) string {
|
|
if t == "incident" {
|
|
return m.st.dangerStyle.Render("incident")
|
|
}
|
|
return m.st.maintStyle.Render("maintenance")
|
|
}
|
|
|
|
func fmtMaintMonitorW(monitorID int, sites []models.Site, maxW int) string {
|
|
if monitorID == 0 {
|
|
return "All"
|
|
}
|
|
for _, s := range sites {
|
|
if s.ID == monitorID {
|
|
return limitStr(s.Name, maxW)
|
|
}
|
|
}
|
|
return fmt.Sprintf("#%d", monitorID)
|
|
}
|
|
|
|
func (m Model) fmtMaintTime(t time.Time, colW int) string {
|
|
if t.IsZero() {
|
|
return m.st.subtleStyle.Render("—")
|
|
}
|
|
now := time.Now()
|
|
if t.Year() == now.Year() && t.YearDay() == now.YearDay() {
|
|
return t.Format("15:04")
|
|
}
|
|
if colW >= 14 {
|
|
return t.Format("15:04 Jan 02")
|
|
}
|
|
return t.Format("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 m.emptyState("No maintenance windows or incidents.", "[n] Create one")
|
|
}
|
|
|
|
var headers []string
|
|
var widths []int
|
|
if m.isWide() {
|
|
headers = []string{"#", "TITLE", "TYPE", "MONITORS", "STATUS", "STARTED", "ENDS"}
|
|
widths = []int{4, 24, 14, 22, 12, 16, 16}
|
|
} else {
|
|
headers = []string{"#", "TITLE", "TYPE", "MON", "ST", "START", "ENDS"}
|
|
widths = []int{4, 14, 13, 14, 11, 14, 14}
|
|
}
|
|
titleW := widths[1]
|
|
monW := widths[3]
|
|
timeW := widths[5]
|
|
|
|
tbl := m.renderTable(
|
|
headers,
|
|
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, titleW-2)),
|
|
m.fmtMaintType(mw.Type),
|
|
fmtMaintMonitorW(mw.MonitorID, allSites, monW-2),
|
|
m.fmtMaintStatus(mw),
|
|
m.fmtMaintTime(mw.StartTime, timeW),
|
|
m.fmtMaintTime(mw.EndTime, timeW),
|
|
})
|
|
}
|
|
return rows
|
|
},
|
|
widths,
|
|
nil,
|
|
)
|
|
|
|
now := time.Now()
|
|
var active, scheduled, ended int
|
|
for _, mw := range m.maintenanceWindows {
|
|
if mw.StartTime.After(now) {
|
|
scheduled++
|
|
} else if !mw.EndTime.IsZero() && mw.EndTime.Before(now) {
|
|
ended++
|
|
} else {
|
|
active++
|
|
}
|
|
}
|
|
summary := fmt.Sprintf("%d active · %d scheduled · %d ended", active, scheduled, ended)
|
|
|
|
return tbl + "\n " + m.st.subtleStyle.Render(summary)
|
|
}
|
|
|
|
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(m.theme.HuhTheme())
|
|
|
|
return m.huhForm.Init()
|
|
}
|
|
|
|
func (m *Model) submitMaintForm() tea.Cmd {
|
|
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)
|
|
}
|
|
}
|
|
|
|
st := m.store
|
|
m.state = stateDashboard
|
|
return writeCmd("Add maintenance window", func() error {
|
|
ctx := context.Background()
|
|
overlaps, _ := st.GetOverlappingMaintenanceWindows(ctx, mw.MonitorID, mw.StartTime, mw.EndTime)
|
|
if len(overlaps) > 0 {
|
|
_ = st.SaveLog(ctx, fmt.Sprintf("Overlap: new window %q overlaps with existing %q", mw.Title, overlaps[0].Title))
|
|
}
|
|
return st.AddMaintenanceWindow(ctx, mw)
|
|
})
|
|
}
|