feat: comprehensive code review fixes + receivers-only architecture
Security:
- Refuse startup with default secret_key in production (was just logging)
- Settings endpoint now requires admin role
- Password validation on initial setup
- DOM-based HTML sanitizer replaces regex in template previews
- Add *.log to .gitignore
Performance & reliability:
- Token refresh deduplication prevents race condition on concurrent 401s
- Theme media query listener registered once (no leak)
- IconPicker uses $derived instead of function call per render
- Snackbar uses single-batch state update instead of while loop
- Replace 11 inline hover handlers with CSS :hover in layout
Architecture - receivers-only:
- Delivery endpoints (chat_id, email, url, room_id, topic) now stored
exclusively in TargetReceiver rows, never in target.config
- Migration extracts existing delivery fields to receiver rows
- Notifier and dispatcher remove all config fallbacks
- Frontend targets page shows receivers list per target with
add/remove/toggle/test per receiver
- Single-receiver test endpoint: POST /targets/{id}/receivers/{id}/test
Code quality:
- Extract AuthLayout.svelte from login/setup (150 lines CSS dedup)
- Split telegram-bots page (754→51 lines + 3 tab components)
- Split notification-trackers page (547→432 lines + 4 components)
- Deduplicate _send_reply into shared handler.send_reply()
- Add locale column to template models, replace name-based detection
- Fix delete_notification_tracker dead protection check
- Fix check_telegram_bot query (filter by type, remove bogus OR)
- Add graceful scheduler shutdown in lifespan
- Consistent /bots?tab=telegram URLs across all nav links
i18n:
- Error page, chat actions, target types, provider types internationalized
- All new receiver UI strings in EN + RU
This commit is contained in:
@@ -116,8 +116,9 @@ class NotificationDispatcher:
|
||||
if not bot_token:
|
||||
return {"success": False, "error": "Missing bot_token"}
|
||||
|
||||
# Resolve receivers — broadcast to each, or fall back to legacy chat_id in config
|
||||
receivers = target.receivers or [{"chat_id": target.config.get("chat_id")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
|
||||
# Prepare assets list once (shared across receivers)
|
||||
provider_urls = []
|
||||
@@ -182,8 +183,9 @@ class NotificationDispatcher:
|
||||
async def _send_webhook(
|
||||
self, target: TargetConfig, message: str, event: ServiceEvent
|
||||
) -> dict[str, Any]:
|
||||
# Resolve receivers — broadcast to each, or fall back to legacy url in config
|
||||
receivers = target.receivers or [{"url": target.config.get("url"), "headers": target.config.get("headers", {})}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
|
||||
payload = {
|
||||
"message": message,
|
||||
@@ -226,8 +228,9 @@ class NotificationDispatcher:
|
||||
use_tls=smtp_cfg.get("use_tls", True),
|
||||
))
|
||||
|
||||
# Resolve receivers
|
||||
receivers = target.receivers or [{"email": target.config.get("email", "")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
subject = f"[Notify Bridge] {event.event_type.value}: {event.collection_name}"
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -251,7 +254,9 @@ class NotificationDispatcher:
|
||||
) -> dict[str, Any]:
|
||||
from .discord.client import DiscordClient
|
||||
|
||||
receivers = target.receivers or [{"webhook_url": target.config.get("webhook_url", "")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
username = target.config.get("username")
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -271,7 +276,9 @@ class NotificationDispatcher:
|
||||
) -> dict[str, Any]:
|
||||
from .slack.client import SlackClient
|
||||
|
||||
receivers = target.receivers or [{"webhook_url": target.config.get("webhook_url", "")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
username = target.config.get("username")
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
@@ -293,7 +300,9 @@ class NotificationDispatcher:
|
||||
|
||||
server_url = target.config.get("server_url", "https://ntfy.sh")
|
||||
auth_token = target.config.get("auth_token")
|
||||
receivers = target.receivers or [{"topic": target.config.get("topic", "")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
|
||||
title = f"{event.event_type.value}: {event.collection_name}"
|
||||
|
||||
@@ -323,7 +332,9 @@ class NotificationDispatcher:
|
||||
if not homeserver or not access_token:
|
||||
return {"success": False, "error": "Missing Matrix homeserver_url or access_token"}
|
||||
|
||||
receivers = target.receivers or [{"room_id": target.config.get("room_id", "")}]
|
||||
if not target.receivers:
|
||||
return {"success": False, "error": "No receivers configured"}
|
||||
receivers = target.receivers
|
||||
|
||||
results: list[dict[str, Any]] = []
|
||||
async with aiohttp.ClientSession() as session:
|
||||
|
||||
Reference in New Issue
Block a user