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
+14 -12
View File
@@ -10,7 +10,8 @@ import (
"github.com/yeniklas/terdut-tui/internal/api"
)
var sectionNames = []string{"Incidents", "Alerts", "Archived", "Schedule", "Users"}
// Order must match the section constants — renderTabs indexes this by ordinal.
var sectionNames = []string{"Incidents", "Alerts", "Stats", "Archived", "Schedule", "Users"}
func (m Model) View() string {
if m.width == 0 {
@@ -68,8 +69,6 @@ func (m Model) renderBody() string {
default:
return m.renderSchedule()
}
case modeStats:
return m.renderStats()
case modeUserPicker:
return m.renderUserPicker()
case modeUserCreate:
@@ -99,12 +98,12 @@ func (m Model) renderFooter() string {
switch m.mode {
case modeIncidentDetail:
if !m.selectedIncident.IsOpen() {
return withStatus(" x·archive c·note [/]·select d·del S·stats esc·back")
return withStatus(" x·archive c·note [/]·select d·del esc·back")
}
return withStatus(" a·ack A·unack R·resolve s·assign z·snooze Z·unsnooze c·note [/]·select d·del S·stats esc·back")
return withStatus(" a·ack A·unack R·resolve s·assign z·snooze Z·unsnooze c·note [/]·select d·del esc·back")
case modeAlertDetail:
return withStatus(" i·open incident S·stats esc·back")
return withStatus(" i·open incident esc·back")
case modeNote:
return "\n" + styleFooter.Render(" enter·submit esc·cancel")
@@ -115,9 +114,6 @@ func (m Model) renderFooter() string {
case modeConfirm:
return "\n" + styleError.Render(" "+m.confirmPrompt())
case modeStats:
return withStatus(" esc·back")
case modeUserPicker:
if m.pickerTarget == pickerIncidentAssignee {
return withStatus(" j/k·navigate enter·assign incident esc·cancel")
@@ -146,9 +142,11 @@ func (m Model) renderFooter() string {
default:
switch m.activeSection {
case sectionIncidents:
return withStatus(" enter·detail x·archive f·filter S·stats r·refresh tab·section q·quit")
return withStatus(" enter·detail x·archive f·filter r·refresh tab·section q·quit")
case sectionAlerts:
return withStatus(" enter·detail f·filter S·stats r·refresh tab·section q·quit")
return withStatus(" enter·detail f·filter r·refresh tab·section q·quit")
case sectionStats:
return withStatus(" ↑/↓·scroll r·refresh tab·section q·quit")
case sectionArchived:
return withStatus(" enter·detail x·unarchive r·refresh tab·section q·quit")
case sectionSchedule:
@@ -187,6 +185,8 @@ func (m Model) renderDashboard() string {
return m.renderIncidents()
case sectionAlerts:
return m.renderAlerts()
case sectionStats:
return m.renderStats()
case sectionArchived:
return m.renderArchived()
case sectionSchedule:
@@ -359,7 +359,9 @@ func (m Model) renderPrompt(prompt string) string {
// ── Stats ──────────────────────────────────────────────────────────────────
func (m Model) renderStats() string {
if m.statsLoading {
// Only announce loading before the first result: a background refresh must not
// blank the page out from under whoever is reading it.
if m.statsLoading && !m.statsLoaded {
return "\n" + styleMuted.Render(" Loading statistics…")
}
return m.statsViewport.View()