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:
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { page } from '$app/state';
|
||||
import { api } from '$lib/api';
|
||||
import { t, getLocale } from '$lib/i18n';
|
||||
@@ -70,15 +70,17 @@
|
||||
|
||||
// ── Constants ──
|
||||
|
||||
const ALL_TYPES = ['telegram', 'webhook', 'email', 'discord', 'slack', 'ntfy', 'matrix'] as const;
|
||||
const ALL_TYPES = ['telegram', 'webhook', 'email', 'discord', 'slack', 'ntfy', 'matrix', 'broadcast'] as const;
|
||||
type TargetType = typeof ALL_TYPES[number];
|
||||
const TYPE_ICONS: Record<string, string> = {
|
||||
telegram: 'mdiSend', webhook: 'mdiWebhook', email: 'mdiEmailOutline',
|
||||
discord: 'mdiChat', slack: 'mdiSlack', ntfy: 'mdiBell', matrix: 'mdiMatrix',
|
||||
broadcast: 'mdiBullhorn',
|
||||
};
|
||||
const TYPE_DESC_KEYS: Record<string, string> = {
|
||||
telegram: 'targets.descTelegram', webhook: 'targets.descWebhook', email: 'targets.descEmail',
|
||||
discord: 'targets.descDiscord', slack: 'targets.descSlack', ntfy: 'targets.descNtfy', matrix: 'targets.descMatrix',
|
||||
broadcast: 'targets.descBroadcast',
|
||||
};
|
||||
|
||||
const typeGridItems = $derived(ALL_TYPES.map(tt => ({
|
||||
@@ -120,6 +122,8 @@
|
||||
matrix_bot_id: 0,
|
||||
// Email
|
||||
email_bot_id: 0,
|
||||
// Broadcast
|
||||
child_target_ids: [] as number[],
|
||||
});
|
||||
let form = $state(defaultForm());
|
||||
let error = $state('');
|
||||
@@ -128,6 +132,12 @@
|
||||
let loadError = $state('');
|
||||
let showTelegramSettings = $state(false);
|
||||
let confirmDelete = $state<NotificationTarget | null>(null);
|
||||
let formEl: HTMLElement;
|
||||
|
||||
async function scrollToForm() {
|
||||
await tick();
|
||||
formEl?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// ── Receiver inline form state ──
|
||||
|
||||
@@ -185,6 +195,7 @@
|
||||
editing = null;
|
||||
showTelegramSettings = false;
|
||||
showForm = true;
|
||||
scrollToForm();
|
||||
}
|
||||
|
||||
async function edit(tgt: NotificationTarget) {
|
||||
@@ -207,10 +218,13 @@
|
||||
email_bot_id: c.email_bot_id || 0,
|
||||
// matrix
|
||||
matrix_bot_id: c.matrix_bot_id || 0,
|
||||
// broadcast
|
||||
child_target_ids: c.child_target_ids || [],
|
||||
};
|
||||
editing = tgt.id;
|
||||
showTelegramSettings = false;
|
||||
showForm = true;
|
||||
scrollToForm();
|
||||
}
|
||||
|
||||
async function save(e: SubmitEvent) {
|
||||
@@ -245,6 +259,8 @@
|
||||
config = { email_bot_id: form.email_bot_id };
|
||||
} else if (formType === 'matrix') {
|
||||
config = { matrix_bot_id: form.matrix_bot_id };
|
||||
} else if (formType === 'broadcast') {
|
||||
config = { child_target_ids: form.child_target_ids };
|
||||
}
|
||||
|
||||
if (editing) {
|
||||
@@ -367,6 +383,21 @@
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
}
|
||||
|
||||
async function toggleBroadcastChild(targetId: number, childId: number) {
|
||||
const tgt = allTargets.find(t => t.id === targetId);
|
||||
if (!tgt) return;
|
||||
const disabled = new Set<number>(tgt.config?.disabled_child_ids || []);
|
||||
if (disabled.has(childId)) disabled.delete(childId);
|
||||
else disabled.add(childId);
|
||||
try {
|
||||
await api(`/targets/${targetId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ config: { ...tgt.config, disabled_child_ids: [...disabled] } }),
|
||||
});
|
||||
await load();
|
||||
} catch (err: any) { snackError(err.message); }
|
||||
}
|
||||
|
||||
async function testReceiver(targetId: number, receiverId: number) {
|
||||
receiverTesting = { ...receiverTesting, [receiverId]: true };
|
||||
try {
|
||||
@@ -392,6 +423,7 @@
|
||||
{/if}
|
||||
|
||||
{#if showForm}
|
||||
<div bind:this={formEl}></div>
|
||||
<TargetForm
|
||||
bind:form
|
||||
bind:formType
|
||||
@@ -404,6 +436,7 @@
|
||||
telegramBotCount={telegramBots.length}
|
||||
emailBotCount={emailBots.length}
|
||||
matrixBotCount={matrixBots.length}
|
||||
broadcastChildItems={allTargets.filter(t => t.type !== 'broadcast' && t.id !== editing).map(t => ({ value: t.id, label: t.name, icon: t.icon || TYPE_ICONS[t.type] || 'mdiTarget', desc: t.type }))}
|
||||
{editing}
|
||||
{submitting}
|
||||
{error}
|
||||
@@ -439,7 +472,11 @@
|
||||
<span style="color: var(--color-primary);"><MdiIcon name={target.icon || TYPE_ICONS[target.type] || 'mdiTarget'} size={20} /></span>
|
||||
<p class="font-medium">{target.name}</p>
|
||||
{#if !activeType}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.type}</span>{/if}
|
||||
{#if (target.receivers || []).length > 0}<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{(target.receivers || []).length} receiver(s)</span>{/if}
|
||||
{#if target.type === 'broadcast' && target.child_targets?.length}
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{target.child_targets.length} {t('targets.childTargets')}</span>
|
||||
{:else if target.type !== 'broadcast' && (target.receivers || []).length > 0}
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{(target.receivers || []).length} receiver(s)</span>
|
||||
{/if}
|
||||
{#if getBotName(target)}<CrossLink href={getBotHref(target)} icon="mdiRobot" label={getBotName(target) ?? ''} entityId={getBotEntityId(target)} />{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -469,6 +506,7 @@
|
||||
ontestReceiver={testReceiver}
|
||||
onloadBotChats={loadReceiverBotChats}
|
||||
onchangeReceiverForm={(f) => receiverForm = f}
|
||||
ontoggleBroadcastChild={toggleBroadcastChild}
|
||||
/>
|
||||
</Card>
|
||||
{/each}
|
||||
|
||||
Reference in New Issue
Block a user