feat!: stats as a section instead of an overlay
Release / test (push) Failing after 6s
Release / build (amd64, darwin) (push) Has been skipped
Release / build (amd64, linux) (push) Has been skipped
Release / build (arm64, darwin) (push) Has been skipped
Release / build (arm64, linux) (push) Has been skipped
Release / release (push) Has been skipped

Stats was the one full-screen view reached by a key of its own rather
than by tab, and the interface was less coherent for it. It is now a
section sitting third, after Alerts, and behaves like every other one:
tab in, tab out, r to refresh.

Three things fall out of the move. It auto-refreshes for the first time
— the tick handler skips every non-dashboard mode, which is why the
overlay never updated while it was open. Its error path no longer forces
the queue back into view on a failed fetch, an assumption that only made
sense while stats floated above the dashboard. And first-visit loading
keys off a statsLoaded flag rather than slice emptiness, because the
three empty slices a quiet server returns are a real answer, not a
missing one; the loading placeholder is likewise suppressed once
something has been drawn, so a background refresh cannot blank the page
out from under whoever is reading it.

The S key is gone, and with it the ability to peek at statistics from an
open incident and land back on it. That round-trip was the only thing
statsReturnMode bought, and it was the whole reason stats needed a mode.
This commit is contained in:
Niklas Ye
2026-08-06 12:46:38 +02:00
parent 1cb3fc3d14
commit 4740687b96
7 changed files with 134 additions and 87 deletions
+21 -8
View File
@@ -21,11 +21,12 @@ const (
// Incidents lead: they are the work. Alerts is the raw feed underneath.
sectionIncidents section = iota
sectionAlerts
sectionStats
sectionArchived
sectionSchedule
sectionUsers
sectionCount = 5
sectionCount = 6
)
type mode int
@@ -37,7 +38,6 @@ const (
modeNote
modeSnooze
modeConfirm
modeStats
modeUserPicker
modeUserCreate
modeAPIKeyMenu
@@ -195,14 +195,14 @@ type Model struct {
pendingDeleteEntry *api.ScheduleEntry
// Stats
topAlerts []api.TopAlert
hourStats []api.HourStat
dayStats []api.DayStat
topAlerts []api.TopAlert
hourStats []api.HourStat
dayStats []api.DayStat
// statsLoaded tracks the first fetch separately from emptiness: a server with
// no alerts yet legitimately returns three empty slices.
statsLoaded bool
statsLoading bool
statsViewport viewport.Model
// statsReturnMode is where esc goes back to, since stats opens from both
// the dashboard and an incident.
statsReturnMode mode
// Schedule
scheduleWindow time.Time
@@ -253,6 +253,10 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
manageT := table.New(table.WithFocused(true))
manageT.SetStyles(ts)
// Sized by the first tea.WindowSizeMsg; built here so it carries the default
// scroll keymap, which the zero value lacks.
statsVP := viewport.New(0, 0)
noteIn := textinput.New()
noteIn.Placeholder = "type your note…"
noteIn.CharLimit = 1000
@@ -298,6 +302,7 @@ func NewModel(client *api.Client, serverURL string, refreshInterval time.Duratio
incidentTable: incidentT,
alertTable: alertT,
archivedTable: archivedT,
statsViewport: statsVP,
noteInput: noteIn,
snoozeInput: snoozeIn,
scheduleWindow: window,
@@ -397,6 +402,14 @@ func (m *Model) refreshStatsContent() {
buildStatsContent(m.incidentStats, m.topAlerts, m.hourStats, m.dayStats, m.width))
}
func (m Model) statsViewportHeight() int {
h := m.height - 5
if h < 1 {
h = 1
}
return h
}
func (m Model) detailViewportHeight() int {
h := m.height - 5
if m.mode == modeNote || m.mode == modeSnooze {