release: 2026.05.1 — distributed probing, config-as-code, TUI polish #15

Merged
lerko merged 47 commits from develop into main 2026-05-16 20:03:54 +00:00
Owner

Release 2026.05.1

Major release bringing distributed probing, config-as-code, and significant TUI improvements.

Features

  • Distributed probing — stateless probe nodes execute checks and report to leader with configurable aggregation (any-down, majority-down, all-down), region affinity, Nodes TUI tab
  • Config-as-code — YAML export/import with goupkeep export and goupkeep apply, dry-run, prune mode
  • 9 alert providers — added Telegram, PagerDuty, Pushover, Gotify
  • Prometheus /metrics endpoint
  • HTTP method + accepted status codes exposed in monitor form
  • Monitor groups with collapse/expand tree view
  • Per-site pause
  • TUI polish — status bar, tab badges, detail panel (i), filter (/), type icons, bordered modals, welcome state, dynamic column widths, health-aware pulse, DOWN-first sort

Fixes

  • Push tokens stripped from public /status/json (security)
  • XSS fix in status page and import error responses
  • Uptime computed from windowed checks, not unbounded counters
  • Status/latency/logs persist across restarts
  • Sparkline right-aligned with current time at right edge
  • Stable sort prevents list shuffling

Refactors

  • Engine struct encapsulates all monitor state
  • Store singleton removed, threaded explicitly
  • SQLite/Postgres unified via Dialect interface
  • Alert providers share HTTPProvider base
  • Check logic extracted for probe reuse
## Release 2026.05.1 Major release bringing distributed probing, config-as-code, and significant TUI improvements. ### Features - **Distributed probing** — stateless probe nodes execute checks and report to leader with configurable aggregation (any-down, majority-down, all-down), region affinity, Nodes TUI tab - **Config-as-code** — YAML export/import with `goupkeep export` and `goupkeep apply`, dry-run, prune mode - **9 alert providers** — added Telegram, PagerDuty, Pushover, Gotify - **Prometheus /metrics endpoint** - **HTTP method + accepted status codes** exposed in monitor form - **Monitor groups** with collapse/expand tree view - **Per-site pause** - **TUI polish** — status bar, tab badges, detail panel (i), filter (/), type icons, bordered modals, welcome state, dynamic column widths, health-aware pulse, DOWN-first sort ### Fixes - Push tokens stripped from public /status/json (security) - XSS fix in status page and import error responses - Uptime computed from windowed checks, not unbounded counters - Status/latency/logs persist across restarts - Sparkline right-aligned with current time at right edge - Stable sort prevents list shuffling ### Refactors - Engine struct encapsulates all monitor state - Store singleton removed, threaded explicitly - SQLite/Postgres unified via Dialect interface - Alert providers share HTTPProvider base - Check logic extracted for probe reuse
lerko added 47 commits 2026-05-16 20:00:17 +00:00
Per-site pause: [p] key toggles pause for selected monitor in TUI.
Paused monitors skip checks, persist to DB, show on status page.

Status page: replace full-page reload with fetch-based DOM updates
to eliminate scroll-jump on refresh. Add summary bar (UP/DOWN/PAUSED
counts), stale-data indicator, and fix SSL EXP CSS class bug.

TUI: constrain tables to terminal width via lipgloss .Width() to
prevent row wrapping that pushed header off-screen. Add MaxHeight
safety net. Bump subtle style from #383838 to #565f89 for
readability on dark terminals.
Reviewed-on: lerko/uptime#1
Prevent accidental deletes with y/n confirmation dialog. Validate all
numeric form inputs (interval, port, timeout, threshold, retries) with
range checks instead of silently defaulting to zero. Escape user-supplied
data in status page JavaScript to close XSS via monitor names. Persist
check history to new check_history table so sparklines and uptime
percentages survive restarts.
Display sequential # instead of internal database IDs in sites, alerts,
and users tables for a cleaner view without gaps from deleted records.
Groups act as visual organizers in the sites table. Monitors can be
assigned to a parent group via the form. Group rows show aggregated
worst-child status, children render with tree chars (├/└), and Space
toggles collapse/expand. Group form hides irrelevant connection and
advanced sections.
Reviewed-on: lerko/uptime#2
Use lipgloss StyleFunc to set per-column widths, with NAME as
the flex column absorbing remaining space. History column tied
to sparkWidth for consistency.
- Move status page template to package-level template.Must (panic on
  parse error at init instead of nil deref at runtime)
- Fix XSS in import error responses (log detail server-side, return
  generic message to client)
- Handle ListenAndServe errors in HTTP and SSH servers
- Use defer resp.Body.Close() in all alert providers, check
  json.Marshal errors
- Share HTTP clients across checks instead of creating per-request
- Use http.NewRequestWithContext for per-site timeout control
- Support HTTP method field (was always GET despite DB storing method)
- Implement AcceptedCodes validation (was hardcoded >= 400 despite DB
  storing accepted code ranges)
- Add defer tx.Rollback() to ImportData for transaction safety
Extract shared SQLStore with Dialect interface for the ~5% that
differs between backends (DDL, placeholders, sequence resets).

- New dialect.go: Dialect interface + placeholder rewriter (? → $N)
- New sqlstore.go: single implementation of all 19 Store methods
- sqlite.go: reduced from 286 to 83 lines (SQLiteDialect only)
- postgres.go: reduced from 266 to 78 lines (PostgresDialect only)
- main.go: use NewSQLiteStore/NewPostgresStore constructors

Zero CRUD logic duplication. Every future schema change written once.
Every Store method now returns an error. Callers handle errors
gracefully — TUI logs to event log, server returns HTTP 500,
monitor engine logs and retries. All rows.Scan() errors are now
checked in sqlstore.go instead of silently appending corrupt data.

- GetSites, GetAllAlerts, GetAllUsers return ([]T, error)
- GetAlert returns (AlertConfig, error) instead of (AlertConfig, bool)
- AddSite, UpdateSite, DeleteSite, etc. all return error
- SaveCheck, LoadAllHistory, ExportData return error
- ~25 caller sites updated across tui, server, monitor, main
Remove store.Get()/SetGlobal()/Current. Store is now passed explicitly
to all consumers via constructor parameters and function arguments.

- TUI Model holds store field, set via InitialModel(isAdmin, store)
- monitor.StartEngine(s) and InitHistoryFromStore(s) accept store
- server.Start(cfg, s) closes over store in HTTP handlers
- main.go threads store to SSH server, TUI, monitor, server
- isKeyAllowed receives store as parameter

No more hidden dependency on package-level mutable state in store pkg.
Monitor package still uses package-level state (LiveState, etc.) — will
be encapsulated into Engine struct in Phase 7.
Discord, Slack, and Webhook providers now use a single HTTPProvider
struct with a PayloadFunc for the only part that differs. Centralizes
response body handling and adds HTTP status code checking (4xx/5xx
now return errors instead of being silently ignored).

Email and Ntfy keep separate implementations (different protocols).
Adding a new HTTP-based alert provider is now a one-line PayloadFunc.
- New table_helpers.go with renderTable() and shared styles
- Remove 4 duplicated style blocks (header/cell/selected/border)
  from tab_alerts.go and tab_users.go
- All 3 tab views now use renderTable() for offset/end calc,
  selected row highlighting, and table construction
- Sites tab keeps siteGroupStyle via StyleOverride callback
- Clamp cursor to list length at end of refreshData() to prevent
  index-out-of-bounds after concurrent list changes
- Fix off-by-one in tab click handler (i <= maxTabs → i < tabCount)
Replace all monitor package-level mutable state with Engine struct.
All state (liveState, logStore, histories, tokenIndex, HTTP clients)
is now encapsulated in Engine, created via NewEngine(store).

Key changes:
- Engine struct holds all monitor state with proper mutex protection
- Engine.Start(ctx) and monitorRoutine respect context cancellation
  for graceful shutdown — no more leaked goroutines
- cluster.runFollowerLoop also respects context for clean exit
- Token index (map[string]int) for O(1) push heartbeat lookup,
  replacing O(n) linear scan through LiveState
- UpdateSiteConfig preserves 8 runtime fields instead of copying
  17 config fields individually
- triggerAlert goroutines get 30s timeout context
- All consumers (TUI, server, cluster, main) receive *Engine via
  constructor/parameter — no package-level state access
- main.go creates context.WithCancel, passes to engine and cluster

First test suite: 12 tests across store and alert packages
- Store: CRUD for sites/alerts/users, push token generation,
  import/export round-trip, check history persistence
- Alert: Discord/Slack/Webhook payload format, HTTP 4xx error
  propagation, Ntfy headers, unknown provider returns nil
Expand alert provider count from 5 to 9. All new providers use
the shared HTTPProvider with closure-based payload functions.
Includes TUI form support and tests for each provider.
Zero-dependency Prometheus text exposition format. Exposes monitor
up/down, latency, status code, check timestamps, pause state,
SSL cert expiry, and check counters — all from in-memory state.
Reviewed-on: lerko/uptime#5
Reviewed-on: lerko/uptime#6
DB fields existed but were never surfaced in the TUI. Adds an HTTP
Settings form group with method select (7 methods) and accepted
codes input, visible only for HTTP monitors.
Reviewed-on: lerko/uptime#7
Add declarative config-as-code support via YAML files. Monitors and
alerts can be exported, version controlled, and applied across instances.

- goupkeep export [-o file.yaml] dumps current state
- goupkeep apply -f file.yaml creates/updates to match desired state
- --dry-run shows planned changes without applying
- --prune deletes monitors/alerts not in the YAML
- Matching by name, alert references by name, nested group children
- CLI refactored to subcommands (apply, export, serve) with backward compat
- 24 tests covering apply, export, validation, round-trip idempotency
Reviewed-on: lerko/uptime#8
Replace old README that referenced rdgames1000 Docker images and
goupkeep.org docs. New README reflects current feature set and
credits the original project as the fork source.
Add node-aware check history and probe registration infrastructure:
- ProbeNode model and nodes table (SQLite + Postgres)
- node_id column on check_history for multi-source tracking
- Store interface: RegisterNode, GetNode, GetAllNodes, DeleteNode, SaveCheckFromNode
- Dialect: UpsertNodeSQL (INSERT OR REPLACE / ON CONFLICT)
- API endpoints: POST /api/probe/register, GET /api/probe/assignments, POST /api/probe/results
- Backward compatible: existing SaveCheck wraps SaveCheckFromNode with empty node_id
Phase 2 of distributed probing:
- Extract check logic into standalone RunCheck() for use by probes
- Add probe cluster mode: stateless nodes that fetch assignments, execute
  checks, and report results to the leader
- Add multi-node result aggregation with configurable strategy
  (any-down, majority-down, all-down)
- Leader ingests probe results into engine live state and triggers alerts
- New env vars: UPKEEP_NODE_ID, UPKEEP_NODE_NAME, UPKEEP_NODE_REGION,
  UPKEEP_AGG_STRATEGY
- Example docker-compose.probe.yml with leader + 2 regional probes
Phase 3 of distributed probing:
- Add regions column to sites table for per-monitor probe affinity
- Region-filtered probe assignments (empty regions = all probes)
- New Nodes TUI tab showing connected probes with status/region/last-seen
- Regions input field in site form for configuring probe affinity
- Config-as-code support for regions (export/import/diff)
- Prometheus upkeep_probe_up metric with per-node labels
- Reindex TUI tabs: Sites, Alerts, Logs, Nodes, Users
Polish pass for TUI professionalism:
- Status bar replaces generic footer with live stats (UP/DOWN count,
  online probes) plus contextual key hints
- Tab badges show DOWN count on Sites tab and offline count on Nodes tab
- Detail panel (press i) shows full monitor info: URL, latency, uptime,
  SSL, probe results, sparkline — without entering edit mode
- Tab badges now always show count (Sites (12)), not just on failure
- Status bar UP count uses green/red coloring instead of subtle gray
- Delete confirmation wrapped in rounded border box with danger color
- Empty sites view shows styled welcome box with onboarding hint
- NAME column width scales with terminal width (13-40 chars)
- DOWN/SSL EXP monitors float to top of sites list
- Pulse indicator turns red when any monitor is down, green when healthy
- Press / to filter sites by name, Enter to lock filter, Esc to clear
- Active filter shown in status bar
Arrow-style icons per monitor type plus Nerd Font folder icons for
groups (closed when collapsed, open when expanded):
  → http, ↓ push, ↔ ping, ⊡ port, ◆ dns, / group
The public status JSON endpoint was serializing full Site structs
including heartbeat tokens. An attacker could extract tokens and
forge heartbeats to suppress DOWN alerts. Now tokens are stripped
before encoding. Backup/export endpoint is unaffected.
lerko merged commit b13b1f18b1 into main 2026-05-16 20:03:54 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lerkolabs/uptop#15