fix: seven quick-win bug fixes across engine, server, TUI, CLI
CI / test (pull_request) Successful in 1m55s
CI / lint (pull_request) Successful in 1m27s
CI / vulncheck (pull_request) Successful in 1m1s

1. Alertless monitors no longer spam error logs — triggerAlert
   returns early when alertID <= 0.

2. HTTP response body drained before close — enables connection
   reuse via keep-alive instead of fresh TCP+TLS per check.

3. /api/backup/export enforces GET — was the only endpoint
   accepting any HTTP method.

4. limitStr guards against max < 3 — prevents negative slice
   index panic on very narrow terminals.

5. Filter input accepts multibyte characters — len(msg.Runes)
   instead of len(msg.String()) for proper Unicode support.

6. Startup warning corrected — with no UPTOP_CLUSTER_SECRET,
   endpoints reject (401), not accept. Warning now says so.

7. UPTOP_KEYS file open failure logged — was silently swallowed,
   leaving operators with no admin seeded and no message.
This commit was merged in pull request #111.
This commit is contained in:
2026-06-11 18:28:32 -04:00
parent 341d60d2fe
commit 5d2b7a3e66
6 changed files with 21 additions and 5 deletions
+3 -1
View File
@@ -603,7 +603,9 @@ func seedKeysFromEnv(s store.Store) {
if path := os.Getenv("UPTOP_KEYS"); path != "" { if path := os.Getenv("UPTOP_KEYS"); path != "" {
f, err := os.Open(filepath.Clean(path)) f, err := os.Open(filepath.Clean(path))
if err == nil { if err != nil {
slog.Warn("failed to open UPTOP_KEYS file", "path", path, "err", err) //nolint:gosec // structured slog, not format string
} else {
scanner := bufio.NewScanner(f) scanner := bufio.NewScanner(f)
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimSpace(scanner.Text()) line := strings.TrimSpace(scanner.Text())
+5 -1
View File
@@ -3,6 +3,7 @@ package monitor
import ( import (
"context" "context"
"fmt" "fmt"
"io"
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
@@ -103,7 +104,10 @@ func runHTTPCheck(ctx context.Context, site models.SiteConfig, strict, insecure
result.ErrorReason = truncateError(err.Error(), maxErrorLength) result.ErrorReason = truncateError(err.Error(), maxErrorLength)
return result return result
} }
defer resp.Body.Close() defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
result.StatusCode = resp.StatusCode result.StatusCode = resp.StatusCode
if !isCodeAccepted(resp.StatusCode, site.AcceptedCodes) { if !isCodeAccepted(resp.StatusCode, site.AcceptedCodes) {
+3
View File
@@ -864,6 +864,9 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
} }
func (e *Engine) triggerAlert(alertID int, title, message string) { func (e *Engine) triggerAlert(alertID int, title, message string) {
if alertID <= 0 {
return
}
cfg, err := e.db.GetAlert(context.Background(), alertID) cfg, err := e.db.GetAlert(context.Background(), alertID)
if err != nil { if err != nil {
e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err)) e.AddLog(fmt.Sprintf("Failed to load alert config %d: %v", alertID, err))
+5 -1
View File
@@ -64,7 +64,7 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
func (s *Server) Start() *http.Server { func (s *Server) Start() *http.Server {
if s.cfg.ClusterKey == "" { if s.cfg.ClusterKey == "" {
slog.Warn("no UPTOP_CLUSTER_SECRET set, cluster API endpoints are unauthenticated") slog.Warn("no UPTOP_CLUSTER_SECRET set, cluster API endpoints will reject all requests")
} }
if s.cfg.ClusterMode != "" && s.cfg.ClusterMode != "leader" && s.cfg.TLSCert == "" { if s.cfg.ClusterMode != "" && s.cfg.ClusterMode != "leader" && s.cfg.TLSCert == "" {
@@ -168,6 +168,10 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) { func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
if !s.requireAuth(r) { if !s.requireAuth(r) {
http.Error(w, "Unauthorized: UPTOP_CLUSTER_SECRET required", http.StatusUnauthorized) http.Error(w, "Unauthorized: UPTOP_CLUSTER_SECRET required", http.StatusUnauthorized)
return return
+3
View File
@@ -34,6 +34,9 @@ func (m Model) emptyState(message, hint string) string {
} }
func limitStr(text string, max int) string { func limitStr(text string, max int) string {
if max < 3 {
return text
}
runes := []rune(text) runes := []rune(text)
if len(runes) > max { if len(runes) > max {
return string(runes[:max-3]) + "..." return string(runes[:max-3]) + "..."
+2 -2
View File
@@ -336,8 +336,8 @@ func (m *Model) handleFilterKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "ctrl+c": case "ctrl+c":
return m, tea.Quit return m, tea.Quit
default: default:
if len(msg.String()) == 1 { if len(msg.Runes) == 1 {
m.filterText += msg.String() m.filterText += string(msg.Runes)
m.cursor = 0 m.cursor = 0
m.tableOffset = 0 m.tableOffset = 0
m.refreshLive() m.refreshLive()