Follow terdut-server into teams: switch team, per-team schedule
terdut-server v0.12 made everything team-scoped and v0.20 is what this
client now targets. Against it the old client was wrong in three ways:
the schedule moved to /api/teams/{id}/schedule, GET /api/schedule/current
became a list with one entry per team, and users, incidents, alerts and
schedule entries all grew fields the client ignored.
T steps through all teams and then each of yours. The header names what
is showing, and incident and alert rows gain a Team column when more than
one team can appear. team: in config.yaml picks the team to start on, by
name or id; an unknown one is reported and falls back to all teams.
The schedule is one team's rota, so it shows the active team, or with
all teams showing the first one you own. Writes need an owner or an
administrator, and the picker offers only the team's members, since the
server answers 404 for anybody else. Both are checked up front and the
reason goes in the status bar, rather than surfacing as a 403 after the
user has picked somebody. Stats are not team-scoped by the server and
stay that way here.
Users shows an admin/disabled Flags column. Creating and deleting users
is administrators only, and topic, keys and password work on your own
row or on anyone's for an administrator; the server enforces the same
rule, this only explains it before the round trip.
The server has no version endpoint, so an older one is recognised by
GET /api/teams answering 404, and the TUI says it needs v0.20 or later.
Connecting now also loads /api/teams and /api/me with the key, which
means a wrong key fails on start instead of on the first list; /healthz
does not check it. There is no fallback to the pre-team paths.
Rebuilding a table whose column count changes under loaded rows panicked
inside bubbles, because it re-renders the old rows on SetColumns. The
rows are now cleared first and the cursor put back, so a refresh still
does not jump to the top.
Escalation ladders, invites, integrations and the admin settings are
left to the server's web UI. Checked against a real v0.20.1 server with
two teams, an administrator and a plain member.
Breaking: requires terdut-server v0.20.0 or later. Use terdut-tui v0.9.x
with servers before v0.12.
This commit is contained in:
+56
-3
@@ -7,6 +7,8 @@ import "time"
|
||||
// alert belongs to.
|
||||
type Alert struct {
|
||||
ID int64 `json:"id"`
|
||||
TeamID int64 `json:"team_id"`
|
||||
TeamName string `json:"team_name,omitempty"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
@@ -25,7 +27,8 @@ type Alert struct {
|
||||
|
||||
// ResolutionSource records why a resolved alert left the firing state:
|
||||
// "alertmanager" for a real resolved webhook, "expiry" when the server
|
||||
// inferred it after the alert stopped being refreshed.
|
||||
// inferred it after the alert stopped being refreshed, "deadman" for a
|
||||
// dead man's switch that came back. Treat the value set as open.
|
||||
ResolutionSource *string `json:"resolution_source,omitempty"`
|
||||
}
|
||||
|
||||
@@ -41,6 +44,8 @@ const (
|
||||
// groupKey Alertmanager computed from the operator's group_by configuration.
|
||||
type Incident struct {
|
||||
ID int64 `json:"id"`
|
||||
TeamID int64 `json:"team_id"`
|
||||
TeamName string `json:"team_name,omitempty"`
|
||||
GroupKey string `json:"group_key"`
|
||||
Title string `json:"title"`
|
||||
GroupLabels map[string]string `json:"group_labels"`
|
||||
@@ -63,10 +68,17 @@ type Incident struct {
|
||||
|
||||
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
||||
|
||||
// ResolutionSource is "alerts" when every alert stopped firing, or "manual"
|
||||
// when a person closed it. Treat the value set as open.
|
||||
// ResolutionSource is "alerts" when every alert stopped firing, "manual"
|
||||
// when a person closed it, or "recovered" when a dead man's switch came back.
|
||||
// Treat the value set as open.
|
||||
ResolutionSource *string `json:"resolution_source,omitempty"`
|
||||
|
||||
// EscalationLevel is how far up the team's escalation ladder the incident has
|
||||
// climbed (0 = not escalated). EscalationDueAt is when the next step fires,
|
||||
// and is nil once the ladder is exhausted or the incident is acknowledged.
|
||||
EscalationLevel int `json:"escalation_level"`
|
||||
EscalationDueAt *time.Time `json:"escalation_due_at,omitempty"`
|
||||
|
||||
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
||||
|
||||
// Alerts is populated by GET /api/incidents/{id} only.
|
||||
@@ -96,6 +108,9 @@ const (
|
||||
EventResolved = "resolved"
|
||||
EventNote = "note"
|
||||
|
||||
// Written when a team's dead man's switch stops reporting.
|
||||
EventDeadmanSilent = "deadman_silent"
|
||||
|
||||
// Written by the server's notifier from the delivery result, not at enqueue.
|
||||
// Detail carries the notification kind ("triggered", "reminder", "resolved"),
|
||||
// and on a failure the reason after it. An absent user means the page went to
|
||||
@@ -154,6 +169,8 @@ type DayStat struct {
|
||||
|
||||
type ScheduleEntry struct {
|
||||
ID int64 `json:"id"`
|
||||
TeamID int64 `json:"team_id"`
|
||||
TeamName string `json:"team_name,omitempty"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Date string `json:"date"` // YYYY-MM-DD
|
||||
@@ -166,6 +183,14 @@ type User struct {
|
||||
Email string `json:"email"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
// IsAdmin marks a system administrator: the only kind of user who can create
|
||||
// or delete users and act on other people's passwords and keys.
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
|
||||
// DisabledAt is set when an administrator has disabled the account. A
|
||||
// disabled user cannot sign in or use their keys.
|
||||
DisabledAt *time.Time `json:"disabled_at,omitempty"`
|
||||
|
||||
// NtfyTopic is where this user's push notifications go. Nil and empty mean
|
||||
// the same thing — no topic of their own — because the server stores a blank
|
||||
// string as NULL. Their incidents fall back to the server's shared fallback
|
||||
@@ -182,12 +207,40 @@ func (u User) Topic() string {
|
||||
return *u.NtfyTopic
|
||||
}
|
||||
|
||||
// IsDisabled reports whether the account has been disabled.
|
||||
func (u User) IsDisabled() bool { return u.DisabledAt != nil }
|
||||
|
||||
// Me is GET /api/me: the caller, and whether they can sign in to the web UI.
|
||||
type Me struct {
|
||||
User User `json:"user"`
|
||||
HasPassword bool `json:"has_password"`
|
||||
}
|
||||
|
||||
// Team roles.
|
||||
const (
|
||||
RoleOwner = "owner"
|
||||
RoleMember = "member"
|
||||
)
|
||||
|
||||
// Team is a group that owns integrations, incidents, a schedule and an
|
||||
// escalation ladder. Role is the caller's role in it, and is only present on
|
||||
// the caller's own team lists (GET /api/teams).
|
||||
type Team struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
Role string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
// TeamMember is one person's membership of a team.
|
||||
type TeamMember struct {
|
||||
TeamID int64 `json:"team_id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
JoinedAt time.Time `json:"joined_at"`
|
||||
}
|
||||
|
||||
type APIKey struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
|
||||
Reference in New Issue
Block a user