ec5df0449b
Mirrors the server's channels API: add/remove channels per user from the Users section. Requires the matching server release. Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
320 lines
12 KiB
Go
320 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Alert is the server's record of what Alertmanager said. It is read-only:
|
|
// acknowledging, assigning, noting and resolving all happen on the Incident an
|
|
// 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"`
|
|
Labels map[string]string `json:"labels"`
|
|
Annotations map[string]string `json:"annotations"`
|
|
StartsAt time.Time `json:"starts_at"`
|
|
EndsAt *time.Time `json:"ends_at"`
|
|
GeneratorURL string `json:"generator_url"`
|
|
ReceivedAt time.Time `json:"received_at"`
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
|
|
// IncidentID is the most recent incident this alert belongs to. An alert row
|
|
// is reused across occurrences of the same fingerprint, so it belongs to a
|
|
// series of incidents over its life and this is only the newest.
|
|
IncidentID *int64 `json:"incident_id,omitempty"`
|
|
|
|
// 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, "deadman" for a
|
|
// dead man's switch that came back. Treat the value set as open.
|
|
ResolutionSource *string `json:"resolution_source,omitempty"`
|
|
}
|
|
|
|
// AuthConfig is how the server can be signed in to, from the unauthenticated
|
|
// GET /api/auth/config. A server too old to have the endpoint answers 404, which
|
|
// callers treat as "passwords only".
|
|
type AuthConfig struct {
|
|
PasswordLogin bool `json:"password_login"`
|
|
OIDC struct {
|
|
Enabled bool `json:"enabled"`
|
|
Name string `json:"name"`
|
|
} `json:"oidc"`
|
|
|
|
// DeviceLogin is whether the server can sign in a client that has no browser,
|
|
// by showing a code (see StartDeviceLogin).
|
|
DeviceLogin bool `json:"device_login"`
|
|
}
|
|
|
|
// DeviceLogin is a sign-in the server has started for this client: the person
|
|
// opens VerificationURL, checks UserCode, and approves; the client polls with
|
|
// DeviceCode until the server hands over a session.
|
|
type DeviceLogin struct {
|
|
DeviceCode string `json:"device_code"`
|
|
UserCode string `json:"user_code"`
|
|
VerificationURL string `json:"verification_url"`
|
|
|
|
// Interval is how many seconds to wait between polls, and ExpiresIn how many
|
|
// the person has to approve.
|
|
Interval int `json:"interval"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
|
|
// Incident statuses.
|
|
const (
|
|
StatusTriggered = "triggered"
|
|
StatusAcknowledged = "acknowledged"
|
|
StatusResolved = "resolved"
|
|
)
|
|
|
|
// Incident is the work item: what a person acknowledges, assigns, snoozes,
|
|
// discusses and resolves. Many alerts map to one incident, correlated by the
|
|
// 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"`
|
|
Status string `json:"status"`
|
|
|
|
// Severity is a high-water mark across the incident's alerts, never lowered,
|
|
// so a resolved incident still says how bad it got.
|
|
Severity string `json:"severity,omitempty"`
|
|
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
|
|
AcknowledgedByID *int64 `json:"acknowledged_by_id,omitempty"`
|
|
AcknowledgedBy string `json:"acknowledged_by,omitempty"`
|
|
AcknowledgedAt *time.Time `json:"acknowledged_at,omitempty"`
|
|
|
|
AssignedToID *int64 `json:"assigned_to_id,omitempty"`
|
|
AssignedTo string `json:"assigned_to,omitempty"`
|
|
|
|
SnoozedUntil *time.Time `json:"snoozed_until,omitempty"`
|
|
|
|
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
|
|
|
|
// 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.
|
|
Alerts []Alert `json:"alerts,omitempty"`
|
|
}
|
|
|
|
// IsSnoozed reports whether the incident is currently quietened. A snooze
|
|
// expires by falling into the past; nothing on the server sweeps it.
|
|
func (i Incident) IsSnoozed() bool {
|
|
return i.SnoozedUntil != nil && i.SnoozedUntil.After(time.Now())
|
|
}
|
|
|
|
// IsOpen reports whether the incident is still work in progress.
|
|
func (i Incident) IsOpen() bool { return i.ResolvedAt == nil }
|
|
|
|
// Incident timeline event types written by the server. New ones may be added,
|
|
// so render unrecognised types generically rather than dropping them.
|
|
const (
|
|
EventTriggered = "triggered"
|
|
EventAlertAdded = "alert_added"
|
|
EventAlertResolved = "alert_resolved"
|
|
EventAcknowledged = "acknowledged"
|
|
EventUnacknowledged = "unacknowledged"
|
|
EventAssigned = "assigned"
|
|
EventSnoozed = "snoozed"
|
|
EventUnsnoozed = "unsnoozed"
|
|
EventResolved = "resolved"
|
|
EventNote = "note"
|
|
|
|
// A note marked as what fixed the incident. The server leads similar
|
|
// incidents with these.
|
|
EventResolutionNote = "resolution_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
|
|
// the shared fallback rather than to a person.
|
|
EventNotified = "notified"
|
|
EventNotifyFailed = "notify_failed"
|
|
)
|
|
|
|
// IncidentEvent is one entry in an incident's timeline. An empty Username means
|
|
// the server acted rather than a person. On an "assigned" event the user is the
|
|
// assignee, not the actor.
|
|
type IncidentEvent struct {
|
|
ID int64 `json:"id"`
|
|
IncidentID int64 `json:"incident_id"`
|
|
Type string `json:"type"`
|
|
UserID *int64 `json:"user_id,omitempty"`
|
|
Username string `json:"username,omitempty"`
|
|
AlertID *int64 `json:"alert_id,omitempty"`
|
|
Detail string `json:"detail,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type AlertStats struct {
|
|
Total int `json:"total"`
|
|
Firing int `json:"firing"`
|
|
Resolved int `json:"resolved"`
|
|
}
|
|
|
|
// IncidentStats carries the queue counts plus mean time to acknowledge and to
|
|
// resolve. Both averages are nil until something has actually been acknowledged
|
|
// or resolved — that is "no data", not zero.
|
|
type IncidentStats struct {
|
|
Total int `json:"total"`
|
|
Triggered int `json:"triggered"`
|
|
Acknowledged int `json:"acknowledged"`
|
|
Resolved int `json:"resolved"`
|
|
MTTASeconds *float64 `json:"mtta_seconds"`
|
|
MTTRSeconds *float64 `json:"mttr_seconds"`
|
|
}
|
|
|
|
type TopAlert struct {
|
|
Name string `json:"name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type HourStat struct {
|
|
Hour int `json:"hour"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
type DayStat struct {
|
|
Day int `json:"day"`
|
|
DayName string `json:"day_name"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
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
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
type User struct {
|
|
ID int64 `json:"id"`
|
|
Username string `json:"username"`
|
|
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"`
|
|
|
|
// Channels are where this user's notifications go: a provider type and an
|
|
// address each. Only filled in for the caller's own row and, without the
|
|
// addresses, for an administrator's view of others. A user with none has
|
|
// their incidents page the server's shared fallback, which carries no
|
|
// Acknowledge button.
|
|
Channels []NotificationChannel `json:"channels,omitempty"`
|
|
}
|
|
|
|
// NotificationChannel is one way of reaching a user.
|
|
type NotificationChannel struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
|
|
// Address is blank unless the caller owns the channel: it can be a secret.
|
|
Address string `json:"address,omitempty"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// ChannelSummary lists the kinds of channel the user has, "ntfy, mail (off)",
|
|
// or "" for none. Never an address.
|
|
func (u User) ChannelSummary() string {
|
|
parts := make([]string, 0, len(u.Channels))
|
|
for _, c := range u.Channels {
|
|
if c.Enabled {
|
|
parts = append(parts, c.Type)
|
|
} else {
|
|
parts = append(parts, c.Type+" (off)")
|
|
}
|
|
}
|
|
return strings.Join(parts, ", ")
|
|
}
|
|
|
|
// 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"`
|
|
Name string `json:"name"`
|
|
Key string `json:"key,omitempty"` // only present in create response
|
|
CreatedAt time.Time `json:"created_at"`
|
|
LastUsedAt *time.Time `json:"last_used_at"`
|
|
// ExpiresAt is nil for a key that never expires, including on a server
|
|
// old enough to not send the field at all — same decoded result either
|
|
// way. Not yet surfaced anywhere in the TUI.
|
|
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
|
}
|
|
|
|
// SimilarIncident is an earlier, resolved incident with the same signature
|
|
// (alert name plus stable group labels) as the one being viewed. ResolutionNotes
|
|
// are its "what fixed it" notes; NoteCount counts its plain notes.
|
|
type SimilarIncident struct {
|
|
ID int64 `json:"id"`
|
|
Title string `json:"title"`
|
|
TriggeredAt time.Time `json:"triggered_at"`
|
|
ResolvedAt time.Time `json:"resolved_at"`
|
|
NoteCount int `json:"note_count"`
|
|
ResolutionNotes []IncidentEvent `json:"resolution_notes"`
|
|
}
|