feat: provider-strict configs, slot-based templates, broadcast targets, email bots, command templates

Major architectural improvements:
- Provider-type enforcement: configs validated against provider type at assignment
- TemplateConfig migrated to slot-based pattern (TemplateSlot child table)
- Broadcast targets: TargetReceiver child table for multi-receiver dispatch
- EmailBot: first-class email sender entity with SMTP config, test connection
- CommandTemplateConfig: generic slot-based command response templates
- Provider capability registry: dynamic slot/event/command definitions per provider
- CommandTracker play/pause button matches NotificationTracker style
This commit is contained in:
2026-03-21 16:33:24 +03:00
parent 371ea70756
commit 846d480d38
27 changed files with 2355 additions and 205 deletions
@@ -227,6 +227,16 @@
// tracker_targets already loaded in tracker response
}
function getProviderType(tracker: any): string {
const p = providers.find(p => p.id === tracker.provider_id);
return p?.type || '';
}
function configsForTracker(tracker: any, configs: (TrackingConfig | TemplateConfig)[]): any[] {
const pt = getProviderType(tracker);
return pt ? configs.filter((c: any) => c.provider_type === pt) : configs;
}
function getUnlinkedTargets(tracker: any): any[] {
const linkedIds = new Set((tracker.tracker_targets || []).map((tt: any) => tt.target_id));
return targets.filter(t => !linkedIds.has(t.id));
@@ -399,13 +409,13 @@
onchange={(e: Event) => updateTargetLink(tracker.id, tt, 'tracking_config_id', Number((e.target as HTMLSelectElement).value) || null)}
class="text-xs px-2 py-1 border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
<option value={0}>— {t('trackingConfig.title')} —</option>
{#each trackingConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
{#each configsForTracker(tracker, trackingConfigs) as tc}<option value={tc.id}>{tc.name}</option>{/each}
</select>
<select value={tt.template_config_id || 0}
onchange={(e: Event) => updateTargetLink(tracker.id, tt, 'template_config_id', Number((e.target as HTMLSelectElement).value) || null)}
class="text-xs px-2 py-1 border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
<option value={0}>— {t('templateConfig.title')} —</option>
{#each templateConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
{#each configsForTracker(tracker, templateConfigs) as tc}<option value={tc.id}>{tc.name}</option>{/each}
</select>
<div class="relative">
<IconButton icon="mdiDotsVertical" size={14} title={t('common.test')}
@@ -433,12 +443,12 @@
<select bind:value={newLinkTrackingConfigId[tracker.id]}
class="text-xs px-2 py-1 border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
<option value={0}> {t('trackingConfig.title')} —</option>
{#each trackingConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
{#each configsForTracker(tracker, trackingConfigs) as tc}<option value={tc.id}>{tc.name}</option>{/each}
</select>
<select bind:value={newLinkTemplateConfigId[tracker.id]}
class="text-xs px-2 py-1 border border-[var(--color-border)] rounded-md bg-[var(--color-background)]">
<option value={0}> {t('templateConfig.title')} —</option>
{#each templateConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
{#each configsForTracker(tracker, templateConfigs) as tc}<option value={tc.id}>{tc.name}</option>{/each}
</select>
<button onclick={() => addTargetLink(tracker.id)}
disabled={!newLinkTargetId[tracker.id] || addingTarget[tracker.id]}