Thread context.Context from callers instead of creating
context.Background() at every call site. Engine stores its
lifecycle ctx for use by triggerAlert goroutines. TUI Model
carries ctx for store operations in Cmd closures. CLI commands
create a root ctx and thread it through openStore, seed
functions, and init methods.
Only two intentional context.Background() remain in non-root
positions: engine constructor default (overridden by Start)
and drainWrites (parent already cancelled at shutdown).
Replace inline numeric literals with named constants across 14 files:
server timeouts/rate limits, cluster thresholds/intervals, DB pool
sizes, alert/dial timeouts, TUI uptime thresholds, node status
thresholds, and state history limits.
Warn via log when creating a maintenance window that overlaps with an
existing one for the same monitor, parent group, or global scope.
Handles both timed and indefinite windows.
Bottom section: graphs (latency + histogram) 70% left, state changes
30% right. Top section rearranged: endpoint details left, status +
timing + HTTP right. Bottom section labels use bold accent without
trailing rules. Spacing between top and bottom halves.
Removed unused renderSparkTooltip (zone marks not wired in column
layout — can restore later).
Section labels (STATE CHANGES, LATENCY, RESPONSE TIME DISTRIBUTION,
PROBE RESULTS) now render in bold accent color with a trailing ───
rule that fills the width. Two-column info block separated by a full
divider below. Creates clear visual boundaries between sections.
Refactored full-screen detail panel: status+endpoint on left column,
timing+config on right column. Halves vertical height of the info
section. Content wraps in a viewport when it exceeds terminal height
— scroll with j/k/arrows/pgup/pgdn.
Restored renderSparkTooltip lost in rewrite. Fixed lint warnings.
Horizontal bar chart showing latency distribution across 5 buckets:
0-50ms, 50-100ms, 100-200ms, 200-500ms, 500ms+. Color-coded green
(fast), yellow (medium), red (slow). Shows count per bucket.
Appears in the full-screen detail panel (Enter) after the sparkline
stats. Skipped for push monitors (no latency data).
Statuspage-style colored bar — one █ per day, colored by uptime:
green (>= 99%), yellow (>= 95%), red (< 95%), gray (no data).
30-day percentage shown to the right.
Daily breakdown computed via ComputeDailyBreakdown from state_changes
and loaded alongside detail data in loadDetailCmd. Auto-updates on
cursor move.
Settings tab warn count now includes failed alert channels — alerts
where the last send failed (LastSendOK=false, LastSendAt not zero).
Shows as a red badge number alongside offline node count. Surfaces
broken alert channels without navigating to Settings.
Press < / > to cycle sort column (Status, Name, Latency). Press r to
reverse direction. Sorted column shows ▲/▼ arrow in the header.
Groups always float to top. Sort applies to ungrouped monitors and
group children independently. Default: Status descending (DOWN first).
Replace ntcharts with custom multi-row sparkline. Each bar is color-
coded by latency threshold (green < 200ms, yellow < 500ms, red > 500ms)
and DOWN checks render in red. 3 rows tall with 24 discrete levels.
Y-axis labels (max/min ms) on the left, Min/Avg/Max stats below.
Zero external dependencies — removed ntcharts.
Replaced streamline chart with ntcharts sparkline (block elements,
auto-scaling). Height=2, Y-axis labels (max/min ms) on the left,
Min/Avg/Max stats below. Denser and more readable than the line chart.
Replace block-element sparkline with ntcharts streamline chart in the
inline detail panel. Renders a 4-row line chart with thin line style
using the theme accent color. Auto-scales Y axis to latency range.
Added github.com/NimbleMarkets/ntcharts v0.5.1 dependency (lipgloss v1
compatible). Min/Avg/Max stats rendered below the chart.
Introduced models.LogEntry{Message, CreatedAt} to replace raw strings
in the log pipeline. Timestamps are now formatted at render time, not
baked into stored messages.
- Engine: appendLog stores LogEntry with time.Now()
- Store: LoadLogs returns []LogEntry, selects created_at from DB
- Store: strips legacy [HH:MM] prefix from pre-refactor DB entries
- TUI: sidebar shows "MM/DD HH:MM" from CreatedAt
- TUI: full log view shows "MM/DD HH:MM" from CreatedAt
- SaveLog still receives plain message string (DB handles timestamp)
Detail open/closed state saved via SetPreference on toggle and
restored on session start. Same pattern as theme persistence —
survives restarts and works per-user over SSH.
Click any panel (Monitors, Logs, Detail) to focus it — accent border
follows focus. Mouse wheel scrolls the focused panel.
Keyboard: l toggles log panel focus. Arrow keys scroll logs when
focused, navigate monitors when not. Esc returns focus to monitors.
Log sidebar now supports scroll offset — tracks position across
renders without a viewport. Mouse wheel scrolls 3 lines, keyboard
scrolls 1.
All panels wrapped in titled rounded borders (╭─ Title ──╮). Focused
panel gets accent-colored border, unfocused panels get muted border.
- Monitors panel: titled "Monitors", focused when detail is closed
- Logs panel: titled "Logs", always unfocused (passive display)
- Detail panel: titled with monitor name, focused when open
Table's own RoundedBorder replaced with HiddenBorder — the titled
panel border provides the visual frame, table uses space-separated
columns internally. Consistent chrome across all panels.
Log sidebar wrapped in rounded border (no left/bottom edge — shared
with monitors table). Creates visual separation between panels.
Enter on a monitor opens the full-screen detail view (existing
stateDetail) for deep dive — history, SLA, probe results, connection
chain. i stays as inline detail toggle.
Footer key hints now context-sensitive: show h/s/Enter when detail
is open, show full keybindings when closed.
Log sidebar was rendering all lines regardless of table height. When
detail panel was open and table shrank, the sidebar stayed tall, pushing
the detail panel past MaxHeight (clipped to empty). Now sidebar accepts
a maxLines parameter capped to table row count.
Press i to toggle a compact detail panel below the monitors+logs
split. Shows status, latency, uptime, state changes, sparkline, and
key hints in ~6 lines. Auto-updates when cursor moves between
monitors. h/s/e keys work from the inline detail for history, SLA,
and edit. Escape closes the panel.
No more full-screen detail takeover for the common case. The old
stateDetail path remains for h/s sub-views which still go full-screen.
Log lines now hard-clamped to panel width via lipgloss MaxWidth.
Stripped "Monitor " and "Push " prefixes from sidebar messages —
redundant in a monitoring app, saves 8 chars per line. Improved
prefix width calculation to prevent line wrapping at narrow widths.
Table columns were computed from terminal width, causing row wrapping
when the monitors panel only gets 70% of the space. Introduced
contentWidth field set per-tab in viewDashboard. computeLayout,
isWide, and renderTable now use contentWidth for column visibility,
available space, and max table width calculations.
Columns gracefully hide (SSL, RETRIES, TYPE, UPTIME) when the panel
is narrower, matching the existing responsive breakpoint behavior.
Replace full viewLogsTab with compact sidebar renderer for the 70/30
monitors split. Single-char severity icons (▼▲◆●·), truncated messages,
no header chrome. Renders directly from engine logs without viewport.
Tab bar: Monitors | Maint | Settings (was 6 tabs).
Settings tab merges Alerts, Nodes, Users as sub-sections with
left/right arrow navigation. Each section keeps its own cursor,
keybindings, and CRUD operations.
Monitors tab now shows a log sidebar at >= 120 cols (70/30 split).
Under 120 cols, monitors render full-width without logs.
- Introduced tab constants (tabMonitors, tabMaint, tabSettings)
- Introduced section constants (sectionAlerts, sectionNodes, sectionUsers)
- Removed stateLogs and stateUsers states
- All magic tab numbers replaced with named constants
Full-width horizontal rules above and below the content area. Tab bar
sits above the top divider, status/keys bar sits below the bottom
divider. Creates three clear visual zones: navigation, content, status.
Revert upper-third centering — inconsistent start positions across tabs
felt jumpy. Back to standard top-align with consistent table placement.
Demo GIF now pauses on Nodes tab (cluster view is a selling point) and
skips Maint/Users quickly instead of sprinting through all three.
Tables on tabs with few rows (Alerts, Nodes, Maint, Users) now sit in
the upper third of the viewport instead of flush against the tab bar.
Dense tabs like Monitors and Logs fill naturally and are unaffected.
Strip Go module pseudo-version suffix (timestamp+hash+dirty) from the
footer version string — shows "v0.1.0" instead of the full build
metadata. Rename "Sites" tab and breadcrumbs to "Monitors" for
consistency with README, CLI help, and user-facing docs.
Bright black ("8") plus Faint made PENDING status and dividers nearly
invisible in 16-color terminals. White ("7") with Faint renders as a
readable dim gray while still sitting below Muted in the hierarchy.
Apply Bold/Faint attributes to semantic styles following htop's
monochrome design principle. Creates 4-tier visual hierarchy that
works even when colors collapse: Bold (danger/warn), Normal (success/
default), Faint (subtle/stale/borders/inactive tabs). Complements
the ANSI-16 color fallbacks without affecting TrueColor appearance.
Theme colors now use lipgloss.CompleteColor with hand-picked ANSI-16
values instead of raw hex. Prevents algorithmic degradation from
collapsing dark backgrounds into indistinguishable ANSI colors over
SSH. Backgrounds fall through to terminal default in 16-color mode;
semantic colors map to distinct ANSI indices (green/yellow/red/blue/
cyan/magenta). TrueColor rendering is unchanged.
Replace 4-page paginated form (17 fields for HTTP) with a 2-page
type-aware layout. Page 1 shows core fields + type-specific target
(URL for HTTP, Hostname for ping, etc). Page 2 shows configuration
with pre-filled defaults. Group type gets 1 page.
Form rebuilds dynamically when monitor type changes, preserving
all entered values via pointer-bound siteFormData. Focus returns
to the Type select after rebuild so users can continue forward.
WithWidth set explicitly on rebuild to prevent placeholder truncation.
handleClick set m.cursor but returned without calling syncSelectedID,
causing the next refreshLive tick to snap the cursor back to the
previously selected site.
viewLogsTab filtered logViewport.View() — the visible window — so the
entry count showed the window size and hidden lines reappeared while
scrolling. Filter and render now happen at content-set time from
engine.GetLogs(); the view only reads stored counts.
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.
1. Rate limiter cleanup goroutine now stoppable via Stop() channel
instead of looping forever. Prevents goroutine leak in tests.
2. Dead WindowSizeMsg branch in handleFormMsg removed — top-level
Update handles resize before forms see it.
3. Probe results sorted by node ID — map iteration no longer
reorders rows every render.
4. fmtAlertConfig takes models.AlertConfig directly instead of an
anonymous struct the caller builds inline.
5. Backspace no longer aliases delete — d is the documented key.
Prevents accidental delete-confirm on habitual backspace.
6. SLA daily buckets use time.Date day arithmetic instead of
Add(-i*24h) — lands on midnight correctly across DST transitions.
1. Delete braille.go + braille_test.go — dead code, only referenced
by its own test. Can be re-added when latency charts are built.
2. Hoist duplicate `const sparkWidth = 40` (update.go + view_detail.go)
to package-level `detailSparkWidth`. Click-index resolution and
rendering now share one constant.
3. Remove tea.ClearScreen on every resize — caused full-screen flash
during continuous resizes. ctrl+l manual clear kept.
Cursor tracked by site ID instead of positional index. When the
list re-sorts every tick (sites change status), the selection stays
on the same monitor instead of silently jumping to whatever now
occupies that index position.
q now means "back" in detail, history, SLA, and alert-detail views
— consistent with muscle memory from navigating deeper views.
Only the dashboard q quits the app. ctrl+c always quits from
anywhere.
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.
Site now embeds SiteConfig (22 persistent fields) and SiteState
(11 ephemeral runtime fields). Field access unchanged via promotion
— site.Name and site.Status still work.
Store layer deals exclusively in SiteConfig — the DB never sees
runtime state. Engine's liveState keeps full Site composites.
UpdateSiteConfig reduced from 11-line field-by-field copy to
`existing.SiteConfig = cfg`.
RunCheck takes SiteConfig (only needs config fields). Checker is
now statically prevented from reading/writing runtime state.
Backup.Sites changed to []SiteConfig — exports no longer carry
zero-valued runtime fields. Import backward-compatible (json
ignores unknown fields).
New internal/store/storetest/mock.go provides BaseMock implementing
the full Store interface with no-op defaults and optional Func field
overrides. Each test file embeds BaseMock and shadows only the methods
it needs.
Removes ~400 lines of duplicated stub methods across 6 test files.
Adding a Store method now requires one addition (BaseMock) instead
of editing 6 files.
Replace ~150 bare status string comparisons with typed models.Status
constants (StatusUp, StatusDown, StatusPending, StatusLate, StatusStale,
StatusSSLExp). Single IsBroken() method replaces the duplicated
isBroken lambda in monitor.go and isDown function in sla.go.
Adding a new status value (e.g. DEGRADED) now requires one constant
definition instead of grep-and-pray across 16 files.
CheckResult.Status stays string — the checker is the boundary between
raw protocol results and typed status. Cast happens at the edge in
handleStatusChange.