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
@@ -61,6 +61,7 @@
const defaultForm = () => ({
name: '', icon: '', provider_id: 0, collection_ids: [] as string[],
scan_interval: 60, batch_duration: 0,
default_tracking_config_id: 0, default_template_config_id: 0,
filters: {} as Record<string, any>,
});
let form = $state(defaultForm());
@@ -143,6 +144,8 @@
name: trk.name, icon: trk.icon || '', provider_id: trk.provider_id,
collection_ids: [...(trk.collection_ids || [])],
scan_interval: trk.scan_interval, batch_duration: trk.batch_duration ?? 0,
default_tracking_config_id: (trk as any).default_tracking_config_id || 0,
default_template_config_id: (trk as any).default_template_config_id || 0,
filters: trk.filters || {},
};
previousCollectionIds = [...(trk.collection_ids || [])];
@@ -179,11 +182,16 @@
async function doSave() {
submitting = true;
try {
const payload = {
...form,
default_tracking_config_id: form.default_tracking_config_id || null,
default_template_config_id: form.default_template_config_id || null,
};
if (editing) {
await api(`/notification-trackers/${editing}`, { method: 'PUT', body: JSON.stringify(form) });
await api(`/notification-trackers/${editing}`, { method: 'PUT', body: JSON.stringify(payload) });
snackSuccess(t('snack.trackerUpdated'));
} else {
await api('/notification-trackers', { method: 'POST', body: JSON.stringify(form) });
await api('/notification-trackers', { method: 'POST', body: JSON.stringify(payload) });
snackSuccess(t('snack.trackerCreated'));
}
showForm = false; editing = null; linkWarning = null; await load();
@@ -371,6 +379,8 @@
{providerItems}
{collections}
bind:collectionFilter
trackingConfigItems={trackingConfigs.filter(c => !selectedProviderType || c.provider_type === selectedProviderType).map(c => ({ value: c.id, label: c.name, icon: (c as any).icon || 'mdiCog' }))}
templateConfigItems={templateConfigs.filter(c => !selectedProviderType || c.provider_type === selectedProviderType).map(c => ({ value: c.id, label: c.name, icon: (c as any).icon || 'mdiFileDocumentEdit' }))}
{editing}
{submitting}
{linkCheckLoading}
@@ -406,7 +416,7 @@
</Card>
{:else if !showForm}
<div class="space-y-3 stagger-children">
{#each notificationTrackers as tracker}
{#each notificationTrackers as tracker (tracker.id)}
<Card hover entityId={tracker.id}>
<div class="flex items-center justify-between">
<div>