feat: Discord/Slack/ntfy/Matrix targets, command templates, delete protection, email/matrix bots

- Discord, Slack, ntfy, Matrix notification target types with clients and dispatch
- MatrixBot model + API + frontend in Bots tab
- Command template system fully wired into all handler commands
- Default command templates seeded (EN/RU, 14 slots each)
- Command template editor with variables reference including child fields
- Delete protection on all 10 entity types (409 with consumer details)
- Provider type selector on template config forms
- Target type selector as dropdown with all 7 types
- Response template selector on command config form
- CLAUDE.md: mandatory server restart rule, child properties rule
This commit is contained in:
2026-03-21 20:36:12 +03:00
parent 846d480d38
commit 3e3a6f0777
64 changed files with 1861 additions and 180 deletions
@@ -13,6 +13,7 @@
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
let configs = $state<any[]>([]);
let cmdTemplateConfigs = $state<any[]>([]);
let loaded = $state(false);
let showForm = $state(false);
let editing = $state<number | null>(null);
@@ -46,13 +47,17 @@
response_mode: 'media',
default_count: 5,
rate_limits: { search: 30, default: 10 },
command_template_config_id: null as number | null,
});
let form = $state(defaultForm());
onMount(load);
async function load() {
try {
configs = await api('/command-configs');
[configs, cmdTemplateConfigs] = await Promise.all([
api('/command-configs'),
api('/command-template-configs'),
]);
} catch (err: any) { error = err.message || t('common.loadError'); snackError(error); }
finally { loaded = true; }
}
@@ -68,6 +73,7 @@
response_mode: cfg.response_mode || 'media',
default_count: cfg.default_count || 5,
rate_limits: { search: cfg.rate_limits?.search || 30, default: cfg.rate_limits?.default || 10 },
command_template_config_id: cfg.command_template_config_id || null,
};
editing = cfg.id;
showForm = true;
@@ -157,6 +163,17 @@
</div>
</div>
<div>
<label for="cc-template" class="block text-sm font-medium mb-1">{t('commandConfig.responseTemplate')}</label>
<select id="cc-template" bind:value={form.command_template_config_id}
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]">
<option value={null}> {t('commandConfig.noTemplate')} —</option>
{#each cmdTemplateConfigs.filter((c: any) => c.provider_type === form.provider_type) as tpl}
<option value={tpl.id}>{tpl.name}{tpl.user_id === 0 ? ' (System)' : ''}</option>
{/each}
</select>
</div>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
<div>
<label class="block text-xs mb-1">{t('commandConfig.locale')}</label>