feat: locale-aware notification templates + UX improvements

- Add locale support to notification templates (matching command template
  pattern): TemplateSlot now has locale field with (config_id, slot_name,
  locale) uniqueness, nested API format {slot: {locale: template}}
- Migration merges separate EN/RU system configs into unified per-provider
  configs; seeds create one config per provider with multi-locale slots
- Locale-aware dispatch with EN fallback in NotificationDispatcher
- Frontend locale tabs (EN/RU) on template config editor
- Fix tracking config cards not showing default provider icons
- Global provider filter, search palette, and various UX polish
This commit is contained in:
2026-03-23 19:08:48 +03:00
parent 6a559bfcd2
commit 37388c430c
30 changed files with 628 additions and 318 deletions
@@ -180,7 +180,7 @@
align-items: center;
gap: 0.5rem;
width: 100%;
padding: 0.5rem 0.75rem;
padding: 0.375rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 0.375rem;
font-size: 0.875rem;
@@ -3,6 +3,8 @@
import { t } from '$lib/i18n';
import MdiIcon from './MdiIcon.svelte';
import { requestHighlight } from '$lib/highlight';
import { providerDefaultIcon } from '$lib/grid-items';
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
import {
fetchAllCaches,
providersCache,
@@ -44,7 +46,7 @@
const GROUPS: readonly { key: string; label: string; icon: string; href: string; mapFn: (e: CacheEntity) => { detail: string; icon: string } }[] = [
{ key: 'providers', label: 'nav.providers', icon: 'mdiServer', href: '/providers',
mapFn: (e) => ({ detail: String(e.type || ''), icon: String(e.icon || 'mdiServer') }) },
mapFn: (e) => ({ detail: String(e.type || ''), icon: providerDefaultIcon(e as any) }) },
{ key: 'notification_trackers', label: 'nav.notification', icon: 'mdiRadar', href: '/notification-trackers',
mapFn: (e) => ({ detail: e.enabled ? 'enabled' : 'disabled', icon: String(e.icon || 'mdiRadar') }) },
{ key: 'tracking_configs', label: 'nav.trackingConfigs', icon: 'mdiCog', href: '/tracking-configs',
@@ -87,10 +89,20 @@
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
const all: SearchResult[] = [];
const gpid = globalProviderFilter.id;
const gpt = globalProviderFilter.providerType;
const providerScoped = new Set(['notification_trackers', 'command_trackers', 'actions']);
const typeScoped = new Set(['tracking_configs', 'template_configs', 'command_configs', 'command_template_configs']);
for (const group of GROUPS) {
const cache = cacheMap[group.key];
if (!cache) continue;
for (const entity of cache.items) {
// Apply global provider filter
if (gpid && group.key === 'providers' && entity.id !== gpid) continue;
if (gpid && providerScoped.has(group.key) && (entity as any).provider_id !== gpid) continue;
if (gpt && typeScoped.has(group.key) && (entity as any).provider_type !== gpt) continue;
const mapped = group.mapFn(entity);
const name = entity.name || '';
const searchable = `${name} ${mapped.detail} ${t(group.label)}`.toLowerCase();