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
- UPTOP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=mysecret
- UPTOP_CLUSTER_SECRET=mysecret # EXAMPLE ONLY — rotate before use
depends_on:
- leader-db
stdin_open: true
@@ -53,7 +53,7 @@ services:
# Cluster Config
- 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
- UPTOP_PEER_URL=http://leader:8080
depends_on:
+3 -3
View File
@@ -3,7 +3,7 @@ services:
build: .
environment:
- UPTOP_CLUSTER_MODE=leader
- UPTOP_CLUSTER_SECRET=changeme
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
- UPTOP_AGG_STRATEGY=any-down
- UPTOP_STATUS_ENABLED=true
ports:
@@ -18,7 +18,7 @@ services:
- UPTOP_NODE_NAME=US East Probe
- UPTOP_NODE_REGION=us-east
- UPTOP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
depends_on:
- leader
@@ -30,6 +30,6 @@ services:
- UPTOP_NODE_NAME=EU West Probe
- UPTOP_NODE_REGION=eu-west
- UPTOP_PEER_URL=http://leader:8080
- UPTOP_CLUSTER_SECRET=changeme
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
depends_on:
- leader
+3 -5
View File
@@ -36,10 +36,8 @@ type CheckResult struct {
ErrorReason string
}
func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure bool, allowPrivate ...bool) CheckResult {
private := len(allowPrivate) > 0 && allowPrivate[0]
if site.Type != "http" && site.Type != "dns" && !private {
func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *http.Client, globalInsecure, allowPrivate bool) CheckResult {
if site.Type != "http" && site.Type != "dns" && !allowPrivate {
host := site.Hostname
if host == "" {
host = site.URL
@@ -63,7 +61,7 @@ func RunCheck(ctx context.Context, site models.SiteConfig, strict, insecure *htt
case "port":
return runPortCheck(ctx, site)
case "dns":
return runDNSCheck(ctx, site, private)
return runDNSCheck(ctx, site, allowPrivate)
default:
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()
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" {
t.Errorf("expected UP, got %s", result.Status)
@@ -40,7 +40,7 @@ func TestRunCheck_HTTP_ServerError(t *testing.T) {
defer srv.Close()
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" {
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"}
result := RunCheck(context.Background(), site, client, client, false)
result := RunCheck(context.Background(), site, client, client, false, false)
if result.Status != "UP" {
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()
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" {
t.Errorf("expected HEAD, got %s", receivedMethod)
@@ -92,7 +92,7 @@ func TestRunCheck_HTTP_Timeout(t *testing.T) {
defer srv.Close()
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" {
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}
result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false)
result := RunCheck(context.Background(), site, http.DefaultClient, insecureClient, false, false)
if result.Status != "UP" {
t.Errorf("expected UP, got %s", result.Status)
@@ -172,7 +172,7 @@ func TestRunCheck_Port_BlocksPrivateByDefault(t *testing.T) {
port, _ := strconv.Atoi(portStr)
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" {
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) {
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" {
t.Errorf("expected DOWN for unknown type, got %s", result.Status)
+3 -2
View File
@@ -17,7 +17,6 @@ const (
maxLogRows = 200
maxStateChangesPerSite = 5000
maxMaintenanceExport = 1000
maxRequestBody = 1 << 20
)
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 {
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 == "" {
var err error
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
}
func (m *Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
m.termWidth = msg.Width
m.termHeight = msg.Height
func (m *Model) recalcLayout() {
chrome := chromeBase
if m.filterMode || m.filterText != "" {
chrome++
}
m.maxTableRows = msg.Height - chrome
m.maxTableRows = m.termHeight - chrome
if 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.Height = msg.Height - (chromePadV + chromeHeader + chromeFooter + 2)
m.historyViewport.Width = msg.Width - chromePadH
m.historyViewport.Height = msg.Height - 10
m.slaViewport.Width = msg.Width - chromePadH
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
}
@@ -320,9 +331,11 @@ func (m *Model) handleFilterKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
m.filterText = ""
m.cursor = 0
m.tableOffset = 0
m.recalcLayout()
m.refreshLive()
case "enter":
m.filterMode = false
m.recalcLayout()
case "backspace":
if len(m.filterText) > 0 {
m.filterText = m.filterText[:len(m.filterText)-1]
@@ -508,6 +521,7 @@ func (m *Model) handleDashboardKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
case "/":
if m.currentTab == 0 {
m.filterMode = true
m.recalcLayout()
return m, nil
}
case "f":
@@ -740,16 +754,8 @@ func (m *Model) switchTab(idx int) {
}
}
func (m *Model) adjustCursor(newLen int) {
if m.cursor >= newLen && m.cursor > 0 {
m.cursor--
}
if m.cursor < m.tableOffset {
m.tableOffset = m.cursor
if m.tableOffset < 0 {
m.tableOffset = 0
}
}
func (m *Model) adjustCursor(_ int) {
m.clampCursor()
}
func (m *Model) submitForm() tea.Cmd {
+1 -6
View File
@@ -85,11 +85,6 @@ func (m Model) View() string {
case stateFormMaint:
title = "New Maintenance Window"
}
formHeight := m.termHeight - 7
if formHeight < 5 {
formHeight = 5
}
m.huhForm.WithHeight(formHeight)
header := m.st.titleStyle.Render(title)
footer := m.st.subtleStyle.Render("\n[Esc] Cancel")
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
switch m.currentTab {
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:
keys = "[n]New [e]Edit [i]Info [d]Del [t]Test [T]Theme [Tab]Switch [q]Quit"
case 2: