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:
2026-03-24 15:15:41 +03:00
parent 8cb836e16c
commit d8ecb60073
13 changed files with 327 additions and 102 deletions
@@ -309,11 +309,35 @@ async def send_to_receiver(target: NotificationTarget, receiver_config: dict, me
async def send_test_notification(target: NotificationTarget, locale: str = "en") -> dict:
"""Send a simple test message."""
"""Send a simple test message. For broadcast targets, fans out to all children."""
if target.type == "broadcast":
return await _send_broadcast_test(target, locale)
message = _get_test_message(locale, target.type)
return await send_to_target(target, message)
async def _send_broadcast_test(target: NotificationTarget, locale: str) -> dict:
"""Send test notifications to all child targets of a broadcast target."""
child_ids = target.config.get("child_target_ids", [])
if not child_ids:
return {"success": False, "error": "No child targets configured"}
disabled_ids = set(target.config.get("disabled_child_ids", []))
engine = get_engine()
results: list[dict] = []
async with AsyncSession(engine) as session:
for child_id in child_ids:
if child_id in disabled_ids:
continue
child = await session.get(NotificationTarget, child_id)
if not child or child.type == "broadcast":
continue
message = _get_test_message(locale, child.type)
results.append(await send_to_target(child, message))
return _aggregate(results)
async def send_test_template_notification(
target: NotificationTarget, slot: str, template_str: str
) -> dict: