fix: seven fixes — token scan, variadic cleanup, TUI layout, compose secrets
CI / test (pull_request) Successful in 1m54s
CI / lint (pull_request) Successful in 1m27s
CI / vulncheck (pull_request) Successful in 1m1s

1. UpdateSite handles token-read Scan error instead of ignoring it.
   sql.ErrNoRows (nonexistent site) passes through; real DB errors
   surface.

2. RunCheck allowPrivate changed from variadic to real bool param.
   Dead maxRequestBody duplicate removed from sqlstore.go.

3. Footer help bar documents [Space] for group collapse.

4. adjustCursor unified with clampCursor — one clamping path
   instead of two with different semantics.

5. Compose cluster/probe example files annotate hardcoded secrets
   with "EXAMPLE ONLY — rotate before use".

6. huhForm.WithHeight moved from View() to handleResize — no longer
   mutates form state during render.

7. maxTableRows recalculated on filter enter/exit via recalcLayout()
   — was only recalculated on resize, causing off-by-one when the
   filter bar appeared/disappeared.
This commit was merged in pull request #118.
This commit is contained in:
2026-06-12 09:36:00 -04:00
parent 9115ab720c
commit 6cf0efed9b
7 changed files with 40 additions and 40 deletions
+2 -2
View File
@@ -18,7 +18,7 @@ services:
# Cluster Config # Cluster Config
- UPTOP_CLUSTER_MODE=leader - UPTOP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=mysecret - UPTOP_CLUSTER_SECRET=mysecret # EXAMPLE ONLY — rotate before use
depends_on: depends_on:
- leader-db - leader-db
stdin_open: true stdin_open: true
@@ -53,7 +53,7 @@ services:
# Cluster Config # Cluster Config
- UPTOP_CLUSTER_MODE=follower - UPTOP_CLUSTER_MODE=follower
- UPTOP_CLUSTER_SECRET=mysecret - UPTOP_CLUSTER_SECRET=mysecret # EXAMPLE ONLY — rotate before use
# IMPORTANT: Uses the Service Name "leader" to connect internally # IMPORTANT: Uses the Service Name "leader" to connect internally
- UPTOP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
depends_on: depends_on:
+3 -3
View File
@@ -3,7 +3,7 @@ services:
build: . build: .
environment: environment:
- UPTOP_CLUSTER_MODE=leader - UPTOP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
- UPTOP_AGG_STRATEGY=any-down - UPTOP_AGG_STRATEGY=any-down
- UPTOP_STATUS_ENABLED=true - UPTOP_STATUS_ENABLED=true
ports: ports:
@@ -18,7 +18,7 @@ services:
- UPTOP_NODE_NAME=US East Probe - UPTOP_NODE_NAME=US East Probe
- UPTOP_NODE_REGION=us-east - UPTOP_NODE_REGION=us-east
- UPTOP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
depends_on: depends_on:
- leader - leader
@@ -30,6 +30,6 @@ services:
- UPTOP_NODE_NAME=EU West Probe - UPTOP_NODE_NAME=EU West Probe
- UPTOP_NODE_REGION=eu-west - UPTOP_NODE_REGION=eu-west
- UPTOP_PEER_URL=http://leader:8080 - UPTOP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme - UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
depends_on: depends_on:
- leader - leader
+3 -5
View File
@@ -36,10 +36,8 @@ type CheckResult struct {
ErrorReason string ErrorReason string
} }
func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure bool, allowPrivate ...bool) CheckResult { func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure, allowPrivate bool) CheckResult {
private := len(allowPrivate) > 0 && allowPrivate[0] if site.Type != "http" && site.Type != "dns" && !allowPrivate {
if site.Type != "http" && site.Type != "dns" && !private {
host := site.Hostname host := site.Hostname
if host == "" { if host == "" {
host = site.URL host = site.URL
@@ -63,7 +61,7 @@ func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *htt
case "port": case "port":
return runPortCheck(ctx, site) return runPortCheck(ctx, site)
case "dns": case "dns":
return runDNSCheck(ctx, site, private) return runDNSCheck(ctx, site, allowPrivate)
default: default:
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "unsupported monitor type: " + site.Type} return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "unsupported monitor type: " + site.Type}
} }
+8 -8
View File
@@ -20,7 +20,7 @@ func TestRunCheck_HTTP_Success(t *testing.T) {
defer srv.Close() defer srv.Close()
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false, false)
if result.Status != "UP" { if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status) t.Errorf("expected UP, got %s", result.Status)
@@ -40,7 +40,7 @@ func TestRunCheck_HTTP_ServerError(t *testing.T) {
defer srv.Close() defer srv.Close()
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
t.Errorf("expected DOWN, got %s", result.Status) t.Errorf("expected DOWN, got %s", result.Status)
@@ -61,7 +61,7 @@ func TestRunCheck_HTTP_CustomAcceptedCodes(t *testing.T) {
}} }}
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, AcceptedCodes: "200-399"} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, AcceptedCodes: "200-399"}
result := RunCheck(context.Background(), site, client, client, false) result := RunCheck(context.Background(), site, client, client, false, false)
if result.Status != "UP" { if result.Status != "UP" {
t.Errorf("expected UP with accepted 200-399, got %s", result.Status) t.Errorf("expected UP with accepted 200-399, got %s", result.Status)
@@ -77,7 +77,7 @@ func TestRunCheck_HTTP_MethodRespected(t *testing.T) {
defer srv.Close() defer srv.Close()
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Method: "HEAD"} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Method: "HEAD"}
RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false, false)
if receivedMethod != "HEAD" { if receivedMethod != "HEAD" {
t.Errorf("expected HEAD, got %s", receivedMethod) t.Errorf("expected HEAD, got %s", receivedMethod)
@@ -92,7 +92,7 @@ func TestRunCheck_HTTP_Timeout(t *testing.T) {
defer srv.Close() defer srv.Close()
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Timeout: 1} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, Timeout: 1}
result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, http.DefaultClient, false, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
t.Errorf("expected DOWN on timeout, got %s", result.Status) t.Errorf("expected DOWN on timeout, got %s", result.Status)
@@ -110,7 +110,7 @@ func TestRunCheck_HTTP_SSLFields(t *testing.T) {
} }
site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, CheckSSL: true, IgnoreTLS: true} site := models.SiteConfig{ID: 1, Type: "http", URL: srv.URL, CheckSSL: true, IgnoreTLS: true}
result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false) result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false, false)
if result.Status != "UP" { if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status) t.Errorf("expected UP, got %s", result.Status)
@@ -172,7 +172,7 @@ func TestRunCheck_Port_BlocksPrivateByDefault(t *testing.T) {
port, _ := strconv.Atoi(portStr) port, _ := strconv.Atoi(portStr)
site := models.SiteConfig{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2} site := models.SiteConfig{ID: 1, Type: "port", Hostname: "127.0.0.1", Port: port, Timeout: 2}
result := RunCheck(context.Background(), site, nil, nil, false) result := RunCheck(context.Background(), site, nil, nil, false, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
t.Errorf("expected DOWN when private targets blocked, got %s", result.Status) t.Errorf("expected DOWN when private targets blocked, got %s", result.Status)
@@ -181,7 +181,7 @@ func TestRunCheck_Port_BlocksPrivateByDefault(t *testing.T) {
func TestRunCheck_UnknownType(t *testing.T) { func TestRunCheck_UnknownType(t *testing.T) {
site := models.SiteConfig{ID: 1, Type: "invalid"} site := models.SiteConfig{ID: 1, Type: "invalid"}
result := RunCheck(context.Background(), site, nil, nil, false) result := RunCheck(context.Background(), site, nil, nil, false, false)
if result.Status != "DOWN" { if result.Status != "DOWN" {
t.Errorf("expected DOWN for unknown type, got %s", result.Status) t.Errorf("expected DOWN for unknown type, got %s", result.Status)
+3 -2
View File
@@ -17,7 +17,6 @@ const (
maxLogRows = 200 maxLogRows = 200
maxStateChangesPerSite = 5000 maxStateChangesPerSite = 5000
maxMaintenanceExport = 1000 maxMaintenanceExport = 1000
maxRequestBody = 1 << 20
) )
type SQLStore struct { type SQLStore struct {
@@ -154,7 +153,9 @@ func (s *SQLStore) AddSite(ctx context.Context, site models.SiteConfig) error {
func (s *SQLStore) UpdateSite(ctx context.Context, site models.SiteConfig) error { func (s *SQLStore) UpdateSite(ctx context.Context, site models.SiteConfig) error {
var existingToken string var existingToken string
_ = s.db.QueryRowContext(ctx, s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken) //nolint:errcheck if err := s.db.QueryRowContext(ctx, s.q("SELECT token FROM sites WHERE id=?"), site.ID).Scan(&existingToken); err != nil && err != sql.ErrNoRows {
return fmt.Errorf("read existing token: %w", err)
}
if site.Type == "push" && existingToken == "" { if site.Type == "push" && existingToken == "" {
var err error var err error
existingToken, err = generateToken() existingToken, err = generateToken()
+20 -14
View File
@@ -141,23 +141,34 @@ func (m *Model) handleFormMsg(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil return m, nil
} }
func (m *Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) { func (m *Model) recalcLayout() {
m.termWidth = msg.Width
m.termHeight = msg.Height
chrome := chromeBase chrome := chromeBase
if m.filterMode || m.filterText != "" { if m.filterMode || m.filterText != "" {
chrome++ chrome++
} }
m.maxTableRows = msg.Height - chrome m.maxTableRows = m.termHeight - chrome
if m.maxTableRows < 1 { if m.maxTableRows < 1 {
m.maxTableRows = 1 m.maxTableRows = 1
} }
}
func (m *Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
m.termWidth = msg.Width
m.termHeight = msg.Height
m.recalcLayout()
m.logViewport.Width = msg.Width - chromePadH m.logViewport.Width = msg.Width - chromePadH
m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeFooter + 2) m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeFooter + 2)
m.historyViewport.Width = msg.Width - chromePadH m.historyViewport.Width = msg.Width - chromePadH
m.historyViewport.Height = msg.Height - 10 m.historyViewport.Height = msg.Height - 10
m.slaViewport.Width = msg.Width - chromePadH m.slaViewport.Width = msg.Width - chromePadH
m.slaViewport.Height = msg.Height - 16 m.slaViewport.Height = msg.Height - 16
if m.huhForm != nil {
formHeight := msg.Height - 7
if formHeight < 5 {
formHeight = 5
}
m.huhForm.WithHeight(formHeight)
}
return m, nil return m, nil
} }
@@ -320,9 +331,11 @@ func (m *Model) handleFilterKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.filterText = "" m.filterText = ""
m.cursor = 0 m.cursor = 0
m.tableOffset = 0 m.tableOffset = 0
m.recalcLayout()
m.refreshLive() m.refreshLive()
case "enter": case "enter":
m.filterMode = false m.filterMode = false
m.recalcLayout()
case "backspace": case "backspace":
if len(m.filterText) > 0 { if len(m.filterText) > 0 {
m.filterText = m.filterText[:len(m.filterText)-1] m.filterText = m.filterText[:len(m.filterText)-1]
@@ -508,6 +521,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "/": case "/":
if m.currentTab == 0 { if m.currentTab == 0 {
m.filterMode = true m.filterMode = true
m.recalcLayout()
return m, nil return m, nil
} }
case "f": case "f":
@@ -740,16 +754,8 @@ func (m *Model) switchTab(idx int) {
} }
} }
func (m *Model) adjustCursor(newLen int) { func (m *Model) adjustCursor(_ int) {
if m.cursor >= newLen && m.cursor > 0 { m.clampCursor()
m.cursor--
}
if m.cursor < m.tableOffset {
m.tableOffset = m.cursor
if m.tableOffset < 0 {
m.tableOffset = 0
}
}
} }
func (m *Model) submitForm() tea.Cmd { func (m *Model) submitForm() tea.Cmd {
+1 -6
View File
@@ -85,11 +85,6 @@ func (m Model) View() string {
case stateFormMaint: case stateFormMaint:
title = "New Maintenance Window" title = "New Maintenance Window"
} }
formHeight := m.termHeight - 7
if formHeight < 5 {
formHeight = 5
}
m.huhForm.WithHeight(formHeight)
header := m.st.titleStyle.Render(title) header := m.st.titleStyle.Render(title)
footer := m.st.subtleStyle.Render("\n[Esc] Cancel") footer := m.st.subtleStyle.Render("\n[Esc] Cancel")
return lipgloss.NewStyle().Padding(1, 2).Render(header + "\n\n" + m.huhForm.View() + "\n" + footer) return lipgloss.NewStyle().Padding(1, 2).Render(header + "\n\n" + m.huhForm.View() + "\n" + footer)
@@ -270,7 +265,7 @@ func (m Model) renderFooter(stats dashboardStats) string {
var keys string var keys string
switch m.currentTab { switch m.currentTab {
case 0: case 0:
keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [T]Theme [Tab]Switch [q]Quit" keys = "[/]Filter [n]New [e]Edit [i]Info [d]Del [p]Pause [Space]Collapse [T]Theme [Tab]Switch [q]Quit"
case 1: case 1:
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit" keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
case 2: case 2: