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:
2026-03-24 22:32:37 +03:00
parent d4cb388c74
commit 6e35926772
16 changed files with 246 additions and 102 deletions
+17 -8
View File
@@ -35,10 +35,12 @@
...allProviders.map(p => ({ value: p.id, icon: providerDefaultIcon(p), label: p.name, desc: p.type })),
]);
let providerFilterValue = $state(globalProviderFilter.id ?? 0);
let _syncingFilter = false;
// Sync filter value → store
$effect(() => {
const v = providerFilterValue;
if (_syncingFilter) return;
globalProviderFilter.set(v === 0 ? null : v);
});
@@ -46,7 +48,9 @@
$effect(() => {
const storeId = globalProviderFilter.id;
if (storeId === null && providerFilterValue !== 0) {
_syncingFilter = true;
providerFilterValue = 0;
_syncingFilter = false;
}
});
@@ -85,6 +89,11 @@
ptype ? items.filter(i => i.provider_type === ptype) : items;
const targets = targetsCache.items;
// Single pass to count targets by type
const targetsByType = new Map<string, number>();
for (const t of targets) {
targetsByType.set(t.type, (targetsByType.get(t.type) || 0) + 1);
}
return {
providers: pid ? 1 : providersCache.items.length,
notification_trackers: filterById(notificationTrackersCache.items as any[]).length,
@@ -97,14 +106,14 @@
telegram_bots: telegramBotsCache.items.length,
email_bots: emailBotsCache.items.length,
matrix_bots: matrixBotsCache.items.length,
targets_telegram: targets.filter(t => t.type === 'telegram').length,
targets_webhook: targets.filter(t => t.type === 'webhook').length,
targets_email: targets.filter(t => t.type === 'email').length,
targets_discord: targets.filter(t => t.type === 'discord').length,
targets_slack: targets.filter(t => t.type === 'slack').length,
targets_ntfy: targets.filter(t => t.type === 'ntfy').length,
targets_matrix: targets.filter(t => t.type === 'matrix').length,
targets_broadcast: targets.filter(t => t.type === 'broadcast').length,
targets_telegram: targetsByType.get('telegram') || 0,
targets_webhook: targetsByType.get('webhook') || 0,
targets_email: targetsByType.get('email') || 0,
targets_discord: targetsByType.get('discord') || 0,
targets_slack: targetsByType.get('slack') || 0,
targets_ntfy: targetsByType.get('ntfy') || 0,
targets_matrix: targetsByType.get('matrix') || 0,
targets_broadcast: targetsByType.get('broadcast') || 0,
} as Record<string, number>;
});