feat: default tracker configs, email validation, expandable target links
- Tracker now has default_tracking_config_id and default_template_config_id that apply to all linked targets unless overridden per-target - Dispatch falls back to tracker defaults when per-link configs are null - Email bot creation validates SMTP connection before saving - Email notifications sent as HTML (links render properly) - Linked target items are expandable: collapsed shows config CrossLinks, expanded shows config selectors; action buttons always visible - Fix email bot test button icon (mdiEmailSend → mdiSend) - Fix target type icons in LinkedTargetsSection for all types - Provider filter moved above search in sidebar
This commit is contained in:
@@ -16,6 +16,7 @@ from ..database.models import (
|
||||
EmailBot,
|
||||
MatrixBot,
|
||||
NotificationTarget,
|
||||
NotificationTracker,
|
||||
NotificationTrackerTarget,
|
||||
TargetReceiver,
|
||||
TelegramBot,
|
||||
@@ -191,6 +192,11 @@ async def load_link_data(
|
||||
tracker_id: ID of the tracker whose links to load.
|
||||
check_quiet_hours: If True, skip links currently in quiet hours.
|
||||
"""
|
||||
# Load the tracker itself for default config IDs
|
||||
tracker = await session.get(NotificationTracker, tracker_id)
|
||||
default_tc_id = getattr(tracker, "default_tracking_config_id", None) if tracker else None
|
||||
default_tmpl_id = getattr(tracker, "default_template_config_id", None) if tracker else None
|
||||
|
||||
tt_result = await session.exec(
|
||||
select(NotificationTrackerTarget).where(
|
||||
NotificationTrackerTarget.tracker_id == tracker_id
|
||||
@@ -198,35 +204,61 @@ async def load_link_data(
|
||||
)
|
||||
tracker_targets = tt_result.all()
|
||||
|
||||
link_data: list[dict[str, Any]] = []
|
||||
for tt in tracker_targets:
|
||||
if not tt.enabled:
|
||||
continue
|
||||
if check_quiet_hours and in_quiet_hours(tt.quiet_hours_start, tt.quiet_hours_end):
|
||||
continue
|
||||
# Filter enabled links and quiet hours upfront
|
||||
active_links = [
|
||||
tt for tt in tracker_targets
|
||||
if tt.enabled and not (check_quiet_hours and in_quiet_hours(tt.quiet_hours_start, tt.quiet_hours_end))
|
||||
]
|
||||
if not active_links:
|
||||
return []
|
||||
|
||||
target = await session.get(NotificationTarget, tt.target_id)
|
||||
# Batch-load targets
|
||||
target_ids = list({tt.target_id for tt in active_links})
|
||||
target_result = await session.exec(
|
||||
select(NotificationTarget).where(NotificationTarget.id.in_(target_ids))
|
||||
)
|
||||
target_map = {t.id: t for t in target_result.all()}
|
||||
|
||||
# Batch-load tracking configs (per-link + tracker default)
|
||||
tc_ids = list({tid for tid in
|
||||
[tt.tracking_config_id for tt in active_links] + [default_tc_id]
|
||||
if tid})
|
||||
tc_map: dict[int, TrackingConfig] = {}
|
||||
if tc_ids:
|
||||
tc_result = await session.exec(select(TrackingConfig).where(TrackingConfig.id.in_(tc_ids)))
|
||||
tc_map = {tc.id: tc for tc in tc_result.all()}
|
||||
|
||||
# Batch-load template configs (per-link + tracker default)
|
||||
tmpl_ids = list({tid for tid in
|
||||
[tt.template_config_id for tt in active_links] + [default_tmpl_id]
|
||||
if tid})
|
||||
tmpl_map: dict[int, TemplateConfig] = {}
|
||||
if tmpl_ids:
|
||||
tmpl_result = await session.exec(select(TemplateConfig).where(TemplateConfig.id.in_(tmpl_ids)))
|
||||
tmpl_map = {tc.id: tc for tc in tmpl_result.all()}
|
||||
|
||||
# Batch-load template slots for all template configs
|
||||
slots_by_config: dict[int, dict[str, dict[str, str]]] = {}
|
||||
if tmpl_ids:
|
||||
slot_result = await session.exec(
|
||||
select(TemplateSlot).where(TemplateSlot.config_id.in_(tmpl_ids))
|
||||
)
|
||||
for s in slot_result.all():
|
||||
event_key = s.slot_name.removeprefix("message_") if s.slot_name.startswith("message_") else s.slot_name
|
||||
slots_by_config.setdefault(s.config_id, {}).setdefault(event_key, {})[s.locale] = s.template
|
||||
|
||||
link_data: list[dict[str, Any]] = []
|
||||
for tt in active_links:
|
||||
target = target_map.get(tt.target_id)
|
||||
if not target:
|
||||
continue
|
||||
|
||||
# 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)
|
||||
|
||||
template_config = None
|
||||
template_slots: dict[str, dict[str, str]] | None = None
|
||||
if tt.template_config_id:
|
||||
template_config = await session.get(TemplateConfig, tt.template_config_id)
|
||||
if template_config:
|
||||
slot_result = await session.exec(
|
||||
select(TemplateSlot).where(TemplateSlot.config_id == template_config.id)
|
||||
)
|
||||
raw_slots: dict[str, dict[str, str]] = {}
|
||||
for s in slot_result.all():
|
||||
event_key = s.slot_name.removeprefix("message_") if s.slot_name.startswith("message_") else s.slot_name
|
||||
raw_slots.setdefault(event_key, {})[s.locale] = s.template
|
||||
template_slots = raw_slots
|
||||
# Per-link config overrides tracker defaults
|
||||
tc_id = tt.tracking_config_id or default_tc_id
|
||||
tmpl_id = tt.template_config_id or default_tmpl_id
|
||||
tracking_config = tc_map.get(tc_id) if tc_id else None
|
||||
template_config = tmpl_map.get(tmpl_id) if tmpl_id else None
|
||||
template_slots = slots_by_config.get(template_config.id) if template_config else None
|
||||
|
||||
# Broadcast target: expand into child targets
|
||||
if target.type == "broadcast":
|
||||
|
||||
Reference in New Issue
Block a user