Manage notification channels instead of an ntfy topic

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
This commit is contained in:
Niklas Ye
2026-10-12 21:00:40 +02:00
parent 77f2d6775b
commit ec5df0449b
10 changed files with 405 additions and 174 deletions
+32 -13
View File
@@ -1,6 +1,9 @@
package api
import "time"
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
@@ -147,7 +150,7 @@ const (
// 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 topic rather than to a person.
// the shared fallback rather than to a person.
EventNotified = "notified"
EventNotifyFailed = "notify_failed"
)
@@ -224,20 +227,36 @@ type User struct {
// 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
// topic, which carries no Acknowledge button.
NtfyTopic *string `json:"ntfy_topic,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"`
}
// Topic reads the user's ntfy topic, flattening the nil and empty cases the
// server treats alike.
func (u User) Topic() string {
if u.NtfyTopic == nil {
return ""
// 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 *u.NtfyTopic
return strings.Join(parts, ", ")
}
// IsDisabled reports whether the account has been disabled.