feat: Last Seen column tracking Alertmanager re-send heartbeat
Release / build (amd64, darwin) (push) Failing after 10s
Release / build (amd64, linux) (push) Failing after 9s
Release / build (arm64, darwin) (push) Failing after 11s
Release / build (arm64, linux) (push) Failing after 9s
Release / release (push) Has been skipped

The alert list showed only Started, which comes from Prometheus and
never changes for the lifetime of an alert instance. A firing alert
that started 12 days ago looked identical whether Alertmanager
refreshed it 30 seconds ago or went silent a week ago.

terdut-server already tracks this: the webhook upsert sets
received_at on every accepted payload, including the periodic
re-sends issued at repeat_interval, and its archiver treats the
field as a liveness heartbeat. The field was already decoded into
api.Alert.ReceivedAt and simply never rendered.

Add a Last Seen column to the alert tables, rendered with the
existing humanAgo helper. The Alerts and Archived tabs share
alertColumns/alertRows, so both pick it up. The width budget is
re-derived for five columns; the slack constant now accounts for
all of bubbles' per-cell padding, so the table lands exactly on
the terminal width instead of overflowing by two columns as it
did with four.

The detail view gains a matching Last Seen line, with the timeline
labels widened to keep values aligned. Since received_at stops
advancing once an alert resolves, also pull through the server's
resolution_source and show it in the status header
(RESOLVED · alertmanager vs RESOLVED · expiry) so a frozen
timestamp is explained.
This commit is contained in:
Niklas Ye
2026-07-30 08:48:53 +02:00
parent 6834302622
commit e04cfcf433
4 changed files with 36 additions and 9 deletions
+8 -5
View File
@@ -340,18 +340,21 @@ func (m Model) detailViewportHeight() int {
// ── Column definitions ─────────────────────────────────────────────────────
func alertColumns(width int) []table.Column {
nameW := width/2 - 8
const statusW, timeW = 10, 12
nameW := width/2 - 14
if nameW < 20 {
nameW = 20
}
ackW := width - nameW - 10 - 12 - 6
// 10 = bubbles' Padding(0, 1) on each of the five cells.
ackW := width - nameW - statusW - 2*timeW - 10
if ackW < 8 {
ackW = 8
}
return []table.Column{
{Title: "Name", Width: nameW},
{Title: "Status", Width: 10},
{Title: "Started", Width: 12},
{Title: "Status", Width: statusW},
{Title: "Started", Width: timeW},
{Title: "Last Seen", Width: timeW},
{Title: "Ack By", Width: ackW},
}
}
@@ -400,7 +403,7 @@ func alertRows(alerts []api.Alert) []table.Row {
now := time.Now()
rows := make([]table.Row, len(alerts))
for i, a := range alerts {
rows[i] = table.Row{a.Name, a.Status, humanAgo(now, a.StartsAt), a.AcknowledgedBy}
rows[i] = table.Row{a.Name, a.Status, humanAgo(now, a.StartsAt), humanAgo(now, a.ReceivedAt), a.AcknowledgedBy}
}
return rows
}