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:
+56
-16
@@ -161,8 +161,12 @@ func (m Model) renderBody() string {
|
||||
return m.renderTeamPicker()
|
||||
case modeUserCreate:
|
||||
return m.renderUserCreate()
|
||||
case modeUserNotifyEdit:
|
||||
return m.renderUserNotifyEdit()
|
||||
case modeChannelMenu:
|
||||
return m.renderChannelMenu()
|
||||
case modeChannelAdd:
|
||||
return m.renderChannelAdd()
|
||||
case modeChannelRemoveByID:
|
||||
return m.renderChannelRemoveByID()
|
||||
case modeAPIKeyMenu:
|
||||
return m.renderAPIKeyMenu()
|
||||
case modeAPIKeyCreate:
|
||||
@@ -222,8 +226,14 @@ func (m Model) renderFooter() string {
|
||||
case modeUserCreate:
|
||||
return withStatus(" tab·next field enter·create esc·cancel")
|
||||
|
||||
case modeUserNotifyEdit:
|
||||
return withStatus(" enter·save esc·cancel (empty clears the topic)")
|
||||
case modeChannelMenu:
|
||||
return withStatus(" a·add a channel r·remove by ID esc·back")
|
||||
|
||||
case modeChannelAdd:
|
||||
return withStatus(" enter·add esc·back (type:address)")
|
||||
|
||||
case modeChannelRemoveByID:
|
||||
return withStatus(" enter·remove esc·back")
|
||||
|
||||
case modeAPIKeyMenu:
|
||||
return withStatus(" n·new key r·revoke by ID esc·back")
|
||||
@@ -256,7 +266,7 @@ func (m Model) renderFooter() string {
|
||||
case sectionSchedule:
|
||||
return withStatus(" +·assign day W·assign week d·del ←/→·shift week " + m.teamHint() + "tab·section r·refresh q·quit")
|
||||
case sectionUsers:
|
||||
return withStatus(" n·new user t·topic d·delete k·API keys p·password r·refresh L·sign out q·quit")
|
||||
return withStatus(" n·new user t·channels d·delete k·API keys p·password r·refresh L·sign out q·quit")
|
||||
}
|
||||
return "\n" + m.styles.Footer.Render(m.help.ShortHelpView(m.keys.ShortHelp()))
|
||||
}
|
||||
@@ -748,10 +758,10 @@ func eventLabel(e api.IncidentEvent) string {
|
||||
return " Resolved (all alerts stopped firing)"
|
||||
case api.EventNotified:
|
||||
// An empty username here is not "the server acted": it means the page
|
||||
// went to the shared fallback topic, so it belongs to nobody.
|
||||
// went to the shared fallback, so it belongs to nobody.
|
||||
return fmt.Sprintf(" Notified %s%s", notifiedTarget(who), notifyKind(e.Detail))
|
||||
case api.EventNotifyFailed:
|
||||
// The detail is "<kind>: <reason>", and the reason is the point — it is
|
||||
// The detail is "<kind> via <channel>: <reason>", and the reason is the point — it is
|
||||
// the only thing that says why nobody's phone rang.
|
||||
return truncate(fmt.Sprintf(" Notification to %s failed · %s",
|
||||
notifiedTarget(who), e.Detail), 52)
|
||||
@@ -767,11 +777,11 @@ func eventLabel(e api.IncidentEvent) string {
|
||||
}
|
||||
|
||||
// notifiedTarget names who a page reached. The server attaches no user when it
|
||||
// published to the shared fallback topic, and saying so is the difference
|
||||
// published to the shared fallback, and saying so is the difference
|
||||
// between "somebody was paged" and "the on-call rota was empty".
|
||||
func notifiedTarget(username string) string {
|
||||
if username == "" {
|
||||
return "the fallback topic"
|
||||
return "the fallback"
|
||||
}
|
||||
return username
|
||||
}
|
||||
@@ -959,14 +969,44 @@ func (m Model) renderUserCreate() string {
|
||||
emailLabel + m.userFormInputs[1].View() + "\n"
|
||||
}
|
||||
|
||||
func (m Model) renderUserNotifyEdit() string {
|
||||
header := fmt.Sprintf("\n Push notifications for %s\n", m.styles.Bold.Render(m.selectedUser.Username))
|
||||
func (m Model) renderChannelMenu() string {
|
||||
header := fmt.Sprintf("\n Notification channels for %s\n\n", m.styles.Bold.Render(m.selectedUser.Username))
|
||||
var list string
|
||||
if len(m.selectedUser.Channels) == 0 {
|
||||
list = line(m.styles.Muted, " None — their incidents page the server's shared fallback,\n"+
|
||||
" which carries no Acknowledge button.")
|
||||
}
|
||||
for _, c := range m.selectedUser.Channels {
|
||||
state := ""
|
||||
if !c.Enabled {
|
||||
state = " (off)"
|
||||
}
|
||||
// The address is only sent to its owner; an administrator sees the kind.
|
||||
address := c.Address
|
||||
if address == "" {
|
||||
address = "(address hidden)"
|
||||
}
|
||||
list += fmt.Sprintf(" %s %s %s%s\n", m.styles.Accent.Render(fmt.Sprintf("#%d", c.ID)), c.Type, address, state)
|
||||
}
|
||||
options := "\n" +
|
||||
m.styles.Accent.Render(" a") + " · add a channel (type:address, e.g. ntfy:my-topic)\n" +
|
||||
m.styles.Accent.Render(" r") + " · remove a channel by ID\n"
|
||||
return header + list + options
|
||||
}
|
||||
|
||||
func (m Model) renderChannelAdd() string {
|
||||
header := fmt.Sprintf("\n New channel for %s\n\n", m.styles.Bold.Render(m.selectedUser.Username))
|
||||
hint := line(m.styles.Muted,
|
||||
" The ntfy topic this user's pages go to. Leave it empty to clear it —\n"+
|
||||
" their incidents then page the server's shared fallback topic, which\n"+
|
||||
" carries no Acknowledge button.")
|
||||
label := m.styles.Selected.Render(" Topic: ")
|
||||
return header + "\n" + hint + "\n" + label + m.ntfyTopicInput.View() + "\n"
|
||||
" The kind of channel, a colon, then where it goes — for ntfy, the topic.\n"+
|
||||
" Only kinds the server has configured are accepted.")
|
||||
label := m.styles.Selected.Render(" Channel: ")
|
||||
return header + hint + "\n" + label + m.channelAddInput.View() + "\n"
|
||||
}
|
||||
|
||||
func (m Model) renderChannelRemoveByID() string {
|
||||
header := fmt.Sprintf("\n Remove a channel from %s\n\n", m.styles.Bold.Render(m.selectedUser.Username))
|
||||
label := m.styles.Selected.Render(" Channel ID: ")
|
||||
return header + label + m.channelRemoveIn.View() + "\n"
|
||||
}
|
||||
|
||||
func (m Model) renderPasswordSet() string {
|
||||
|
||||
Reference in New Issue
Block a user