release: 2026.05.1 — distributed probing, config-as-code, TUI polish #15
+10
-12
@@ -97,8 +97,6 @@ func main() {
|
||||
fmt.Printf("Database init error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
store.SetGlobal(s)
|
||||
|
||||
if *demo {
|
||||
seedDemoData(s)
|
||||
}
|
||||
@@ -117,15 +115,15 @@ func main() {
|
||||
fmt.Printf("Imported %d monitors and %d alerts from Uptime Kuma v%s\n", len(backup.Sites), len(backup.Alerts), kb.Version)
|
||||
}
|
||||
|
||||
monitor.InitHistoryFromStore()
|
||||
monitor.StartEngine()
|
||||
monitor.InitHistoryFromStore(s)
|
||||
monitor.StartEngine(s)
|
||||
|
||||
server.Start(server.ServerConfig{
|
||||
Port: httpPort,
|
||||
EnableStatus: enableStatus,
|
||||
Title: statusTitle,
|
||||
ClusterKey: clusterKey,
|
||||
})
|
||||
}, s)
|
||||
|
||||
cluster.Start(cluster.Config{
|
||||
Mode: clusterMode,
|
||||
@@ -133,10 +131,10 @@ func main() {
|
||||
SharedKey: clusterKey,
|
||||
})
|
||||
|
||||
startSSHServer(*port)
|
||||
startSSHServer(*port, s)
|
||||
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
p := tea.NewProgram(tui.InitialModel(true), tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
p := tea.NewProgram(tui.InitialModel(true, s), tea.WithAltScreen(), tea.WithMouseCellMotion())
|
||||
if _, err := p.Run(); err != nil {
|
||||
fmt.Printf("Error: %v\n", err)
|
||||
}
|
||||
@@ -149,16 +147,16 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func startSSHServer(port int) {
|
||||
func startSSHServer(port int, db store.Store) {
|
||||
s, err := wish.NewServer(
|
||||
wish.WithAddress(fmt.Sprintf(":%d", port)),
|
||||
wish.WithHostKeyPath(".ssh/id_ed25519"),
|
||||
wish.WithPublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
|
||||
return isKeyAllowed(key)
|
||||
return isKeyAllowed(db, key)
|
||||
}),
|
||||
wish.WithMiddleware(
|
||||
bm.Middleware(func(s ssh.Session) (tea.Model, []tea.ProgramOption) {
|
||||
return tui.InitialModel(false), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
|
||||
return tui.InitialModel(false, db), []tea.ProgramOption{tea.WithAltScreen(), tea.WithMouseCellMotion()}
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -206,8 +204,8 @@ func seedDemoData(s store.Store) {
|
||||
s.AddSite(models.Site{Name: "SSH Server", Type: "port", Interval: 60, AlertID: alertID, Hostname: "10.0.0.1", Port: 22, Timeout: 5, ExpiryThreshold: 7})
|
||||
}
|
||||
|
||||
func isKeyAllowed(incomingKey ssh.PublicKey) bool {
|
||||
users, err := store.Get().GetAllUsers()
|
||||
func isKeyAllowed(db store.Store, incomingKey ssh.PublicKey) bool {
|
||||
users, err := db.GetAllUsers()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -20,11 +20,7 @@ var (
|
||||
historyMu sync.RWMutex
|
||||
)
|
||||
|
||||
func InitHistoryFromStore() {
|
||||
s := store.Get()
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
func InitHistoryFromStore(s store.Store) {
|
||||
all, err := s.LoadAllHistory(maxHistoryLen)
|
||||
if err != nil {
|
||||
AddLog("Failed to load check history: " + err.Error())
|
||||
@@ -74,8 +70,8 @@ func RecordCheck(siteID int, latency time.Duration, isUp bool) {
|
||||
h.Statuses = h.Statuses[len(h.Statuses)-maxHistoryLen:]
|
||||
}
|
||||
|
||||
if s := store.Get(); s != nil {
|
||||
go func() { _ = s.SaveCheck(siteID, latency.Nanoseconds(), isUp) }()
|
||||
if db != nil {
|
||||
go func() { _ = db.SaveCheck(siteID, latency.Nanoseconds(), isUp) }()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,8 @@ var (
|
||||
|
||||
insecureSkipVerify bool
|
||||
|
||||
db store.Store
|
||||
|
||||
strictClient = &http.Client{
|
||||
Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: false}},
|
||||
}
|
||||
@@ -119,16 +121,11 @@ func RecordHeartbeat(token string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func StartEngine() {
|
||||
func StartEngine(s store.Store) {
|
||||
db = s
|
||||
go func() {
|
||||
for {
|
||||
s_instance := store.Get()
|
||||
if s_instance == nil {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
sites, err := s_instance.GetSites()
|
||||
sites, err := db.GetSites()
|
||||
if err != nil {
|
||||
AddLog(fmt.Sprintf("Failed to load sites: %v", err))
|
||||
time.Sleep(5 * time.Second)
|
||||
@@ -407,11 +404,10 @@ func handleStatusChange(site models.Site, rawStatus string, code int, latency ti
|
||||
}
|
||||
|
||||
func triggerAlert(alertID int, title, message string) {
|
||||
s_instance := store.Get()
|
||||
if s_instance == nil {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
cfg, err := s_instance.GetAlert(alertID)
|
||||
cfg, err := db.GetAlert(alertID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ type ServerConfig struct {
|
||||
ClusterKey string // Shared Secret for Security
|
||||
}
|
||||
|
||||
func Start(cfg ServerConfig) {
|
||||
func Start(cfg ServerConfig, s store.Store) {
|
||||
if cfg.ClusterKey == "" {
|
||||
fmt.Println("WARNING: No UPKEEP_CLUSTER_SECRET set. Cluster API endpoints are unauthenticated.")
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func Start(cfg ServerConfig) {
|
||||
http.Error(w, "Unauthorized: UPKEEP_CLUSTER_SECRET required", 401)
|
||||
return
|
||||
}
|
||||
data, err := store.Get().ExportData()
|
||||
data, err := s.ExportData()
|
||||
if err != nil {
|
||||
log.Printf("Export failed: %v", err)
|
||||
http.Error(w, "Export failed", 500)
|
||||
@@ -209,7 +209,7 @@ func Start(cfg ServerConfig) {
|
||||
http.Error(w, "Invalid JSON", 400)
|
||||
return
|
||||
}
|
||||
if err := store.Get().ImportData(data); err != nil {
|
||||
if err := s.ImportData(data); err != nil {
|
||||
log.Printf("Import failed: %v", err)
|
||||
http.Error(w, "Import failed", 500)
|
||||
return
|
||||
@@ -234,7 +234,7 @@ func Start(cfg ServerConfig) {
|
||||
return
|
||||
}
|
||||
backup := importer.ConvertKuma(&kb)
|
||||
if err := store.Get().ImportData(backup); err != nil {
|
||||
if err := s.ImportData(backup); err != nil {
|
||||
log.Printf("Kuma import failed: %v", err)
|
||||
http.Error(w, "Import failed", 500)
|
||||
return
|
||||
|
||||
@@ -35,13 +35,3 @@ type Store interface {
|
||||
ExportData() (models.Backup, error)
|
||||
ImportData(data models.Backup) error
|
||||
}
|
||||
|
||||
var Current Store
|
||||
|
||||
func SetGlobal(s Store) {
|
||||
Current = s
|
||||
}
|
||||
|
||||
func Get() Store {
|
||||
return Current
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package tui
|
||||
import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/monitor"
|
||||
"go-upkeep/internal/store"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -278,11 +277,11 @@ func (m *Model) submitAlertForm() {
|
||||
}
|
||||
|
||||
if m.editID > 0 {
|
||||
if err := store.Get().UpdateAlert(m.editID, d.Name, d.AlertType, settings); err != nil {
|
||||
if err := m.store.UpdateAlert(m.editID, d.Name, d.AlertType, settings); err != nil {
|
||||
monitor.AddLog("Update alert failed: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := store.Get().AddAlert(d.Name, d.AlertType, settings); err != nil {
|
||||
if err := m.store.AddAlert(d.Name, d.AlertType, settings); err != nil {
|
||||
monitor.AddLog("Add alert failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/models"
|
||||
"go-upkeep/internal/monitor"
|
||||
"go-upkeep/internal/store"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -361,14 +360,12 @@ func (m *Model) initSiteHuhForm() tea.Cmd {
|
||||
}
|
||||
|
||||
alertOpts := []huh.Option[string]{huh.NewOption("None", "0")}
|
||||
if s := store.Get(); s != nil {
|
||||
if alerts, err := s.GetAllAlerts(); err == nil {
|
||||
for _, a := range alerts {
|
||||
alertOpts = append(alertOpts, huh.NewOption(
|
||||
fmt.Sprintf("%s (%s)", a.Name, a.Type),
|
||||
strconv.Itoa(a.ID),
|
||||
))
|
||||
}
|
||||
if alerts, err := m.store.GetAllAlerts(); err == nil {
|
||||
for _, a := range alerts {
|
||||
alertOpts = append(alertOpts, huh.NewOption(
|
||||
fmt.Sprintf("%s (%s)", a.Name, a.Type),
|
||||
strconv.Itoa(a.ID),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,12 +557,12 @@ func (m *Model) submitSiteForm() {
|
||||
}
|
||||
|
||||
if m.editID > 0 {
|
||||
if err := store.Get().UpdateSite(site); err != nil {
|
||||
if err := m.store.UpdateSite(site); err != nil {
|
||||
monitor.AddLog("Update site failed: " + err.Error())
|
||||
}
|
||||
monitor.UpdateSiteConfig(site)
|
||||
} else {
|
||||
if err := store.Get().AddSite(site); err != nil {
|
||||
if err := m.store.AddSite(site); err != nil {
|
||||
monitor.AddLog("Add site failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package tui
|
||||
import (
|
||||
"fmt"
|
||||
"go-upkeep/internal/monitor"
|
||||
"go-upkeep/internal/store"
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/huh"
|
||||
@@ -146,11 +145,11 @@ func (m *Model) initUserHuhForm() tea.Cmd {
|
||||
func (m *Model) submitUserForm() {
|
||||
d := m.userFormData
|
||||
if m.editID > 0 {
|
||||
if err := store.Get().UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
|
||||
if err := m.store.UpdateUser(m.editID, d.Username, d.PublicKey, d.Role); err != nil {
|
||||
monitor.AddLog("Update user failed: " + err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := store.Get().AddUser(d.Username, d.PublicKey, d.Role); err != nil {
|
||||
if err := m.store.AddUser(d.Username, d.PublicKey, d.Role); err != nil {
|
||||
monitor.AddLog("Add user failed: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
+26
-33
@@ -68,6 +68,7 @@ type Model struct {
|
||||
deleteTab int
|
||||
|
||||
collapsed map[int]bool
|
||||
store store.Store
|
||||
|
||||
// harmonica animation state
|
||||
pulseSpring harmonica.Spring
|
||||
@@ -80,7 +81,7 @@ type Model struct {
|
||||
users []models.User
|
||||
}
|
||||
|
||||
func InitialModel(isAdmin bool) Model {
|
||||
func InitialModel(isAdmin bool, s store.Store) Model {
|
||||
vpLogs := viewport.New(100, 20)
|
||||
vpLogs.SetContent("Waiting for logs...")
|
||||
z := zone.New()
|
||||
@@ -90,6 +91,7 @@ func InitialModel(isAdmin bool) Model {
|
||||
logViewport: vpLogs,
|
||||
maxTableRows: 5,
|
||||
isAdmin: isAdmin,
|
||||
store: s,
|
||||
zones: z,
|
||||
pulseSpring: spring,
|
||||
collapsed: make(map[int]bool),
|
||||
@@ -107,25 +109,23 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if keyMsg, ok := msg.(tea.KeyMsg); ok {
|
||||
switch keyMsg.String() {
|
||||
case "y", "Y":
|
||||
if s := store.Get(); s != nil {
|
||||
switch m.deleteTab {
|
||||
case 0:
|
||||
if err := s.DeleteSite(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete site failed: " + err.Error())
|
||||
}
|
||||
monitor.RemoveSite(m.deleteID)
|
||||
m.adjustCursor(len(m.sites) - 1)
|
||||
case 1:
|
||||
if err := s.DeleteAlert(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete alert failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.alerts) - 1)
|
||||
case 3:
|
||||
if err := s.DeleteUser(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete user failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.users) - 1)
|
||||
switch m.deleteTab {
|
||||
case 0:
|
||||
if err := m.store.DeleteSite(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete site failed: " + err.Error())
|
||||
}
|
||||
monitor.RemoveSite(m.deleteID)
|
||||
m.adjustCursor(len(m.sites) - 1)
|
||||
case 1:
|
||||
if err := m.store.DeleteAlert(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete alert failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.alerts) - 1)
|
||||
case 3:
|
||||
if err := m.store.DeleteUser(m.deleteID); err != nil {
|
||||
monitor.AddLog("Delete user failed: " + err.Error())
|
||||
}
|
||||
m.adjustCursor(len(m.users) - 1)
|
||||
}
|
||||
m.refreshData()
|
||||
m.state = stateDashboard
|
||||
@@ -319,9 +319,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
site := m.sites[m.cursor]
|
||||
monitor.ToggleSitePause(site.ID)
|
||||
site.Paused = !site.Paused
|
||||
if s := store.Get(); s != nil {
|
||||
_ = s.UpdateSitePaused(site.ID, site.Paused)
|
||||
}
|
||||
_ = m.store.UpdateSitePaused(site.ID, site.Paused)
|
||||
m.refreshData()
|
||||
}
|
||||
case "d", "backspace":
|
||||
@@ -470,23 +468,18 @@ func (m *Model) refreshData() {
|
||||
}
|
||||
ordered = append(ordered, ungrouped...)
|
||||
m.sites = ordered
|
||||
if s := store.Get(); s != nil {
|
||||
if alerts, err := s.GetAllAlerts(); err == nil {
|
||||
m.alerts = alerts
|
||||
}
|
||||
if m.isAdmin {
|
||||
if users, err := s.GetAllUsers(); err == nil {
|
||||
m.users = users
|
||||
}
|
||||
if alerts, err := m.store.GetAllAlerts(); err == nil {
|
||||
m.alerts = alerts
|
||||
}
|
||||
if m.isAdmin {
|
||||
if users, err := m.store.GetAllUsers(); err == nil {
|
||||
m.users = users
|
||||
}
|
||||
}
|
||||
m.logViewport.SetContent(strings.Join(monitor.GetLogs(), "\n"))
|
||||
}
|
||||
|
||||
func (m *Model) submitForm() {
|
||||
if store.Get() == nil {
|
||||
return
|
||||
}
|
||||
switch m.state {
|
||||
case stateFormSite:
|
||||
if m.siteFormData != nil {
|
||||
|
||||
Reference in New Issue
Block a user