Put a team's own settings in the web UI
CI / chart (pull_request) Successful in 1s
CI / security (pull_request) Successful in 13s
CI / test (pull_request) Successful in 2m19s

Closes #17. Everything a team owner configures was API-only: escalation,
integrations, dead man's switches, membership, and the rota -- which the
on-call view still described as the TUI's job, and the TUI has been broken
against this server since teams landed. Setting up the feature this whole
line of work exists for meant using curl.

A Team tab now holds all of it, one team at a time, with a picker for
somebody in more than one. An owner edits; a member sees the same page
without the controls, because the server refuses their writes anyway --
hiding a button is a courtesy to the reader, not the thing enforcing
anything.

The escalation editor holds a draft and sends the whole ladder, because
the API replaces it wholesale: the levels are an order, and patching one
rung leaves the numbering of the others undecided. Adding a level
defaults to five minutes and the rota, which is the shape almost every
ladder starts as.

An integration key is returned exactly once, so creating one opens a
panel that says so, shows the URL large with a copy button, and renders
the Alertmanager receiver snippet with the URL already in it -- the next
thing anybody does with that key is paste it into a config. The panel
stays until it is dismissed rather than disappearing on the next
re-render.

The incident view gains where an incident is on the ladder and when the
next page is due, which is the question somebody looking at an
unacknowledged incident actually has. The API carries it: the incident
payload now includes escalation_level and escalation_due_at, the latter
computed in the incident SELECT by joining the level's timeout, so a list
costs no extra queries.

Verified against a live server by making every call the page makes,
including the writes: the six reads the Team tab issues, a two-level
ladder saved and read back, an integration created and its key returned
once, three days of rota assigned, switches set, and an incident showing
level 1 with a due time five minutes out.

Claude-Session: https://claude.ai/code/session_01RHPj4ggeFdEjKKfm4SHbD7
This commit is contained in:
Niklas Ye
2026-09-20 21:22:24 +02:00
parent 53e5e03f4e
commit d728af53b1
9 changed files with 555 additions and 2 deletions
+10 -1
View File
@@ -52,6 +52,13 @@ type querier interface {
const incidentSelectFrom = `
SELECT i.id, i.team_id, t.name, i.group_key, i.title, i.group_labels, i.status, i.severity,
i.escalation_level,
-- When this level runs out. Computed here rather than in Go because
-- the timeout lives beside the level in the policy, and one join is
-- cheaper than a second query per incident in a list.
(SELECT i.escalation_level_at + el.timeout_seconds
FROM escalation_levels el
WHERE el.team_id = i.team_id AND el.position = i.escalation_level),
i.triggered_at,
i.acknowledged_by, i.acknowledged_at, ack.username,
i.assigned_to, asg.username, i.snoozed_until,
@@ -65,10 +72,11 @@ func scanIncident(s scanner) (models.Incident, error) {
var i models.Incident
var groupLabelsJSON string
var triggeredAt int64
var ackAt, snoozedUntil, resolvedAt, archivedAt *int64
var ackAt, snoozedUntil, resolvedAt, archivedAt, escalationDue *int64
if err := s.Scan(
&i.ID, &i.TeamID, &i.TeamName, &i.GroupKey, &i.Title, &groupLabelsJSON, &i.Status, &i.Severity,
&i.EscalationLevel, &escalationDue,
&triggeredAt,
&i.AcknowledgedByID, &ackAt, &i.AcknowledgedByUser,
&i.AssignedToID, &i.AssignedToUser, &snoozedUntil,
@@ -83,6 +91,7 @@ func scanIncident(s scanner) (models.Incident, error) {
i.SnoozedUntil = unixPtr(snoozedUntil)
i.ResolvedAt = unixPtr(resolvedAt)
i.ArchivedAt = unixPtr(archivedAt)
i.EscalationDueAt = unixPtr(escalationDue)
return i, nil
}