feat: Archived alerts tab with archive/unarchive actions
Release / build (amd64, linux) (push) Failing after 6s
Release / release (push) Has been skipped
Release / build (amd64, darwin) (push) Failing after 5s
Release / build (arm64, darwin) (push) Failing after 6s
Release / build (arm64, linux) (push) Failing after 11s

Add a fourth tab (Alerts | Archived | Schedule | Users).
Archived alerts are fetched lazily on first visit using the
archived=true query param on GET /api/alerts.

Press x from the Alerts list or detail to archive an alert;
the non-archived list refreshes immediately. Press x from the
Archived list or detail to unarchive; the archived list
refreshes. Ack/unack are disabled in the Archived detail view.

New API methods: ArchiveAlert (POST), UnarchiveAlert (DELETE).
ArchivedAt field added to the Alert type.
This commit is contained in:
Niklas Ye
2026-05-22 13:45:30 +02:00
parent 24c2e6003a
commit 6834302622
5 changed files with 203 additions and 8 deletions
+37 -2
View File
@@ -10,7 +10,7 @@ import (
"github.com/yeniklas/terdut-tui/internal/api"
)
var sectionNames = []string{"Alerts", "Schedule", "Users"}
var sectionNames = []string{"Alerts", "Archived", "Schedule", "Users"}
func (m Model) View() string {
if m.width == 0 {
@@ -92,7 +92,12 @@ func (m Model) renderBody() string {
func (m Model) renderFooter() string {
switch m.mode {
case modeDetail:
actions := styleFooter.Render(" a·ack A·unack c·comment [/]·select d·del s·assign S·stats esc·back")
var actions string
if m.activeSection == sectionArchived {
actions = styleFooter.Render(" x·unarchive c·comment [/]·select d·del S·stats esc·back")
} else {
actions = styleFooter.Render(" a·ack A·unack x·archive c·comment [/]·select d·del s·assign S·stats esc·back")
}
if m.statusMsg != "" {
return styleStatus.Render(" "+m.statusMsg) + "\n" + actions
}
@@ -163,6 +168,13 @@ func (m Model) renderFooter() string {
return "\n" + styleFooter.Render(" enter·revoke esc·back")
default:
if m.activeSection == sectionArchived {
actions := styleFooter.Render(" x·unarchive enter·detail r·refresh tab·section q·quit")
if m.statusMsg != "" {
return styleStatus.Render(" "+m.statusMsg) + "\n" + actions
}
return "\n" + actions
}
if m.activeSection == sectionSchedule {
actions := styleFooter.Render(" +·assign day W·assign week d·del ←/→·shift week tab·section r·refresh q·quit")
if m.statusMsg != "" {
@@ -177,6 +189,10 @@ func (m Model) renderFooter() string {
}
return "\n" + actions
}
var footerLeft string
if m.activeSection == sectionAlerts {
footerLeft = styleFooter.Render(" x·archive")
}
helpView := styleFooter.Render(m.help.ShortHelpView(m.keys.ShortHelp()))
if m.statusMsg != "" {
status := styleStatus.Render(m.statusMsg)
@@ -186,6 +202,13 @@ func (m Model) renderFooter() string {
}
return "\n" + status + strings.Repeat(" ", gap) + helpView
}
if footerLeft != "" {
gap := m.width - lipgloss.Width(footerLeft) - lipgloss.Width(helpView)
if gap < 0 {
gap = 0
}
return "\n" + footerLeft + strings.Repeat(" ", gap) + helpView
}
return "\n" + helpView
}
}
@@ -196,6 +219,8 @@ func (m Model) renderDashboard() string {
switch m.activeSection {
case sectionAlerts:
return m.renderAlerts()
case sectionArchived:
return m.renderArchived()
case sectionSchedule:
return m.renderSchedule()
case sectionUsers:
@@ -284,6 +309,16 @@ func (m Model) renderAlerts() string {
return lipgloss.JoinVertical(lipgloss.Left, statsBar, content)
}
func (m Model) renderArchived() string {
if m.archivedLoading {
return styleMuted.Render(" Loading archived alerts…")
}
if len(m.archivedAlerts) == 0 {
return styleMuted.Render(" No archived alerts.")
}
return m.archivedTable.View()
}
func (m Model) renderStatsBar() string {
total, firing, resolved := 0, 0, 0
if m.stats != nil {