feat: broadcast notification target + UX improvements
Add broadcast target type that fans out notifications to multiple child targets. Dispatch expands broadcast into children in load_link_data() — dispatcher stays unaware. Children can be toggled on/off via disabled_child_ids in config. Also: dashboard provider card smaller font for names, scroll-to-form on target edit, broadcast nav tab with counter, flag_modified fix for JSON column updates, CLAUDE.md nav tree docs.
This commit is contained in:
@@ -96,6 +96,88 @@ def event_allowed_by_config(event: ServiceEvent, tc: TrackingConfig) -> bool:
|
||||
return flag_map.get(event_type, True)
|
||||
|
||||
|
||||
async def _resolve_target(
|
||||
session: AsyncSession,
|
||||
target: NotificationTarget,
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve a single target into dispatch-ready data (config + receivers + credentials).
|
||||
|
||||
Returns a dict with target_type, target_config, and receivers.
|
||||
Does NOT include tracking_config or template_slots — those come from the tracker link.
|
||||
"""
|
||||
# Load receivers as typed Receiver objects
|
||||
recv_result = await session.exec(
|
||||
select(TargetReceiver).where(
|
||||
TargetReceiver.target_id == target.id,
|
||||
TargetReceiver.enabled == True,
|
||||
)
|
||||
)
|
||||
recv_rows = recv_result.all()
|
||||
|
||||
# For Telegram targets, resolve locale from TelegramChat
|
||||
chat_locale_map: dict[str, str] = {}
|
||||
if target.type == "telegram":
|
||||
bot_id = target.config.get("bot_id")
|
||||
if bot_id:
|
||||
chat_ids = [str(r.config.get("chat_id", "")) for r in recv_rows if r.config.get("chat_id")]
|
||||
if chat_ids:
|
||||
chat_result = await session.exec(
|
||||
select(TelegramChat).where(
|
||||
TelegramChat.bot_id == bot_id,
|
||||
TelegramChat.chat_id.in_(chat_ids),
|
||||
)
|
||||
)
|
||||
for chat in chat_result.all():
|
||||
resolved = (
|
||||
getattr(chat, 'language_override', '') or
|
||||
getattr(chat, 'language_code', '') or ''
|
||||
)
|
||||
if resolved:
|
||||
chat_locale_map[chat.chat_id] = resolved[:2].lower()
|
||||
|
||||
receivers: list[Receiver] = []
|
||||
for r in recv_rows:
|
||||
explicit_locale = getattr(r, 'locale', '') or ''
|
||||
locale = explicit_locale
|
||||
if not locale and target.type == "telegram":
|
||||
chat_id = str(r.config.get("chat_id", ""))
|
||||
locale = chat_locale_map.get(chat_id, "")
|
||||
receivers.append(build_receiver(target.type, dict(r.config), locale))
|
||||
|
||||
target_config = dict(target.config)
|
||||
# Inject chat_action for Telegram targets
|
||||
if hasattr(target, 'chat_action') and target.chat_action:
|
||||
target_config["chat_action"] = target.chat_action
|
||||
# Inject bot credentials for bot-backed target types
|
||||
if target.type == "email":
|
||||
email_bot_id = target.config.get("email_bot_id")
|
||||
if email_bot_id:
|
||||
email_bot = await session.get(EmailBot, email_bot_id)
|
||||
if email_bot:
|
||||
target_config["smtp"] = {
|
||||
"host": email_bot.smtp_host,
|
||||
"port": email_bot.smtp_port,
|
||||
"username": email_bot.smtp_username,
|
||||
"password": email_bot.smtp_password,
|
||||
"from_address": email_bot.email,
|
||||
"from_name": email_bot.name,
|
||||
"use_tls": email_bot.smtp_use_tls,
|
||||
}
|
||||
elif target.type == "matrix":
|
||||
matrix_bot_id = target.config.get("matrix_bot_id")
|
||||
if matrix_bot_id:
|
||||
matrix_bot = await session.get(MatrixBot, matrix_bot_id)
|
||||
if matrix_bot:
|
||||
target_config["homeserver_url"] = matrix_bot.homeserver_url
|
||||
target_config["access_token"] = matrix_bot.access_token
|
||||
|
||||
return {
|
||||
"target_type": target.type,
|
||||
"target_config": target_config,
|
||||
"receivers": receivers,
|
||||
}
|
||||
|
||||
|
||||
async def load_link_data(
|
||||
session: AsyncSession,
|
||||
tracker_id: int,
|
||||
@@ -127,45 +209,7 @@ async def load_link_data(
|
||||
if not target:
|
||||
continue
|
||||
|
||||
# Load receivers as typed Receiver objects
|
||||
recv_result = await session.exec(
|
||||
select(TargetReceiver).where(
|
||||
TargetReceiver.target_id == target.id,
|
||||
TargetReceiver.enabled == True,
|
||||
)
|
||||
)
|
||||
recv_rows = recv_result.all()
|
||||
|
||||
# For Telegram targets, resolve locale from TelegramChat
|
||||
chat_locale_map: dict[str, str] = {}
|
||||
if target.type == "telegram":
|
||||
bot_id = target.config.get("bot_id")
|
||||
if bot_id:
|
||||
chat_ids = [str(r.config.get("chat_id", "")) for r in recv_rows if r.config.get("chat_id")]
|
||||
if chat_ids:
|
||||
chat_result = await session.exec(
|
||||
select(TelegramChat).where(
|
||||
TelegramChat.bot_id == bot_id,
|
||||
TelegramChat.chat_id.in_(chat_ids),
|
||||
)
|
||||
)
|
||||
for chat in chat_result.all():
|
||||
resolved = (
|
||||
getattr(chat, 'language_override', '') or
|
||||
getattr(chat, 'language_code', '') or ''
|
||||
)
|
||||
if resolved:
|
||||
chat_locale_map[chat.chat_id] = resolved[:2].lower()
|
||||
|
||||
receivers: list[Receiver] = []
|
||||
for r in recv_rows:
|
||||
explicit_locale = getattr(r, 'locale', '') or ''
|
||||
locale = explicit_locale
|
||||
if not locale and target.type == "telegram":
|
||||
chat_id = str(r.config.get("chat_id", ""))
|
||||
locale = chat_locale_map.get(chat_id, "")
|
||||
receivers.append(build_receiver(target.type, dict(r.config), locale))
|
||||
|
||||
# Load tracking config and template slots (shared across broadcast children)
|
||||
tracking_config = None
|
||||
if tt.tracking_config_id:
|
||||
tracking_config = await session.get(TrackingConfig, tt.tracking_config_id)
|
||||
@@ -184,37 +228,29 @@ async def load_link_data(
|
||||
raw_slots.setdefault(event_key, {})[s.locale] = s.template
|
||||
template_slots = raw_slots
|
||||
|
||||
target_config = dict(target.config)
|
||||
# Inject chat_action for Telegram targets
|
||||
if hasattr(target, 'chat_action') and target.chat_action:
|
||||
target_config["chat_action"] = target.chat_action
|
||||
# Inject bot credentials for bot-backed target types
|
||||
if target.type == "email":
|
||||
email_bot_id = target.config.get("email_bot_id")
|
||||
if email_bot_id:
|
||||
email_bot = await session.get(EmailBot, email_bot_id)
|
||||
if email_bot:
|
||||
target_config["smtp"] = {
|
||||
"host": email_bot.smtp_host,
|
||||
"port": email_bot.smtp_port,
|
||||
"username": email_bot.smtp_username,
|
||||
"password": email_bot.smtp_password,
|
||||
"from_address": email_bot.email,
|
||||
"from_name": email_bot.name,
|
||||
"use_tls": email_bot.smtp_use_tls,
|
||||
}
|
||||
elif target.type == "matrix":
|
||||
matrix_bot_id = target.config.get("matrix_bot_id")
|
||||
if matrix_bot_id:
|
||||
matrix_bot = await session.get(MatrixBot, matrix_bot_id)
|
||||
if matrix_bot:
|
||||
target_config["homeserver_url"] = matrix_bot.homeserver_url
|
||||
target_config["access_token"] = matrix_bot.access_token
|
||||
# Broadcast target: expand into child targets
|
||||
if target.type == "broadcast":
|
||||
child_ids = target.config.get("child_target_ids", [])
|
||||
disabled_ids = set(target.config.get("disabled_child_ids", []))
|
||||
for child_id in child_ids:
|
||||
if child_id in disabled_ids:
|
||||
continue
|
||||
child_target = await session.get(NotificationTarget, child_id)
|
||||
if not child_target or child_target.type == "broadcast":
|
||||
continue
|
||||
resolved = await _resolve_target(session, child_target)
|
||||
link_data.append({
|
||||
**resolved,
|
||||
"tracking_config": tracking_config,
|
||||
"template_config": template_config,
|
||||
"template_slots": template_slots,
|
||||
})
|
||||
continue
|
||||
|
||||
# Regular target
|
||||
resolved = await _resolve_target(session, target)
|
||||
link_data.append({
|
||||
"target_type": target.type,
|
||||
"target_config": target_config,
|
||||
"receivers": receivers,
|
||||
**resolved,
|
||||
"tracking_config": tracking_config,
|
||||
"template_config": template_config,
|
||||
"template_slots": template_slots,
|
||||
|
||||
Reference in New Issue
Block a user