feat(tui): add state change history view with outage duration
CI / test (pull_request) Successful in 2m30s
CI / lint (pull_request) Failing after 51s
CI / vulncheck (pull_request) Successful in 46s

Full-screen scrollable history view accessible via [h] from detail
panel. Shows all state transitions with computed outage durations,
event density sparkline for flapping detection, and summary stats.

- Detail panel STATE CHANGES now shows outage duration per recovery
- Event density sparkline highlights flapping periods
- Summary footer: event count, outage count, avg outage duration
- Vim-style navigation (j/k/g/G) + mouse scroll in history view
This commit is contained in:
2026-06-03 19:49:10 -04:00
parent c0ad51af9c
commit bc661f5207
6 changed files with 422 additions and 4 deletions
+48
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"time"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/huh"
)
@@ -122,6 +123,8 @@ func (m *Model) handleResize(msg tea.WindowSizeMsg) (tea.Model, tea.Cmd) {
}
m.logViewport.Width = msg.Width - chromePadH
m.logViewport.Height = msg.Height - (chromePadV + chromeHeader + chromeGaps + chromeFooter)
m.historyViewport.Width = msg.Width - chromePadH
m.historyViewport.Height = msg.Height - 10
return m, tea.ClearScreen
}
@@ -134,6 +137,14 @@ func (m *Model) handleTick(t time.Time) (tea.Model, tea.Cmd) {
}
func (m *Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) {
if m.state == stateHistory {
if msg.Button == tea.MouseButtonWheelUp {
m.historyViewport.ScrollUp(3)
} else if msg.Button == tea.MouseButtonWheelDown {
m.historyViewport.ScrollDown(3)
}
return m, nil
}
if m.state != stateDashboard && m.state != stateLogs && m.state != stateUsers {
return m, nil
}
@@ -187,6 +198,8 @@ func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch m.state {
case stateDetail:
return m.handleDetailKey(msg)
case stateHistory:
return m.handleHistoryKey(msg)
case stateAlertDetail:
return m.handleAlertDetailKey(msg)
case stateDashboard, stateLogs, stateUsers:
@@ -229,12 +242,47 @@ func (m *Model) handleDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "i", "esc":
m.state = stateDashboard
case "h":
if m.cursor < len(m.sites) {
site := m.sites[m.cursor]
m.historySiteName = site.Name
m.historyChanges = m.engine.GetStateChanges(site.ID, 100)
m.historyViewport = viewport.New(
m.termWidth-chromePadH,
m.termHeight-10,
)
m.historyViewport.SetContent(m.buildHistoryContent())
m.historyViewport.GotoTop()
m.state = stateHistory
}
case "q":
return m, tea.Quit
}
return m, nil
}
func (m *Model) handleHistoryKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "q", "esc":
m.state = stateDetail
case "up", "k":
m.historyViewport.LineUp(1)
case "down", "j":
m.historyViewport.LineDown(1)
case "pgup":
m.historyViewport.HalfViewUp()
case "pgdown":
m.historyViewport.HalfViewDown()
case "home", "g":
m.historyViewport.GotoTop()
case "end", "G":
m.historyViewport.GotoBottom()
case "ctrl+c":
return m, tea.Quit
}
return m, nil
}
func (m *Model) handleAlertDetailKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
switch msg.String() {
case "i", "esc":