Files
uptop/internal/tui/tab_maint.go
T
lerko 9d12e3ecf1
CI / test (pull_request) Successful in 4m26s
CI / lint (pull_request) Successful in 1m11s
chore: complete rename from go-upkeep to uptop
- Module path: gitea.lerkolabs.com/lerko/uptop
- Binary: cmd/uptop/
- All imports updated to full module path
- Env vars: UPKEEP_* → UPTOP_*
- Prometheus metrics: upkeep_* → uptop_*
- Default DB: uptop.db
- Docker image: lerko/uptop
- All docs, compose files, CI updated

Only remaining "go-upkeep" reference is the fork attribution in README.
2026-05-24 20:20:35 -04:00

231 lines
5.6 KiB
Go

package tui
import (
"fmt"
"gitea.lerkolabs.com/lerko/uptop/internal/models"
"strconv"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
"github.com/charmbracelet/lipgloss"
)
var maintStyle lipgloss.Style
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(m.theme.HuhTheme())
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
}