feat: replace YAML notification channel editor with form-based UI
Notification channels are now managed through a proper web form instead
of a raw YAML textarea. Any authenticated user can create channels; private
channels (owner-scoped) are hidden from other users. The user profile
channel selector becomes a tag/chip picker with a "My Channels" CRUD section.
- settings.py: add CHANNEL_TYPE_SCHEMAS for all 6 notifier types; channel
section switches to section_mode="channels"; cards include owner/private/min_level
- configio.py: add apply_channel() and delete_channel() for per-entry CRUD
- notify.py: strip owner/private metadata before dispatching to drivers
- http.py: add GET/POST /api/0/notification_channels, PUT/DELETE /{name},
GET /api/0/notification_channel_types; visibility helper filters private
channels per user; PUT /api/0/users/me validates against visible channels
- settings.html: card grid with edit/delete per channel; add/edit modal
with type dropdown and dynamically rendered type-specific fields
- profile.html: chip picker replaces checkbox list; My Channels section
for creating/editing/deleting user-owned channels
- tests: update test_settings_sections, test_http_users_me; add
test_notification_channels_api (16 new tests, 46 total passing)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -83,3 +83,41 @@ def test_put_users_me_notification_channels(tmp_path):
|
||||
configio.write_config(str(cfg), data)
|
||||
result = configio.read_roundtrip(str(cfg))
|
||||
assert result["users"]["alice"]["notification_channels"] == ["pushover_ops"]
|
||||
|
||||
|
||||
def test_visible_channels_excludes_private_from_others():
|
||||
"""Private channels owned by another user must not appear in the visible set."""
|
||||
from hbd.server import settings as settings_mod
|
||||
|
||||
config = {
|
||||
"notification_channels": {
|
||||
"public_ch": {"type": "pushover", "token": "t", "user": "u"},
|
||||
"alice_priv": {"type": "email", "owner": "alice", "private": True,
|
||||
"recipients": ["a@b.com"], "sender": "s@b.com", "smtp_server": "s"},
|
||||
"bob_priv": {"type": "email", "owner": "bob", "private": True,
|
||||
"recipients": ["b@b.com"], "sender": "s@b.com", "smtp_server": "s"},
|
||||
}
|
||||
}
|
||||
|
||||
class FakeUser:
|
||||
def __init__(self, username, admin=False):
|
||||
self.username = username
|
||||
self.admin = admin
|
||||
|
||||
alice = FakeUser("alice")
|
||||
bob = FakeUser("bob")
|
||||
admin = FakeUser("admin", admin=True)
|
||||
|
||||
# Simulate _visible_channels_for_user logic (mirrors http.py implementation)
|
||||
def visible(user):
|
||||
all_channels = config.get("notification_channels") or {}
|
||||
if user.admin:
|
||||
return set(all_channels.keys())
|
||||
return {
|
||||
name for name, cfg in all_channels.items()
|
||||
if not cfg.get("private") or cfg.get("owner") == user.username
|
||||
}
|
||||
|
||||
assert visible(alice) == {"public_ch", "alice_priv"}
|
||||
assert visible(bob) == {"public_ch", "bob_priv"}
|
||||
assert visible(admin) == {"public_ch", "alice_priv", "bob_priv"}
|
||||
|
||||
Reference in New Issue
Block a user