fix: seven quick-win bug fixes across engine, server, TUI, CLI
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:
+3
-1
@@ -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())
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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]) + "..."
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user