fix: seven fixes — token scan, variadic cleanup, TUI layout, compose secrets
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:
@@ -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,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
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user