feat: comprehensive code review fixes + receivers-only architecture

Security:
- Refuse startup with default secret_key in production (was just logging)
- Settings endpoint now requires admin role
- Password validation on initial setup
- DOM-based HTML sanitizer replaces regex in template previews
- Add *.log to .gitignore

Performance & reliability:
- Token refresh deduplication prevents race condition on concurrent 401s
- Theme media query listener registered once (no leak)
- IconPicker uses $derived instead of function call per render
- Snackbar uses single-batch state update instead of while loop
- Replace 11 inline hover handlers with CSS :hover in layout

Architecture - receivers-only:
- Delivery endpoints (chat_id, email, url, room_id, topic) now stored
  exclusively in TargetReceiver rows, never in target.config
- Migration extracts existing delivery fields to receiver rows
- Notifier and dispatcher remove all config fallbacks
- Frontend targets page shows receivers list per target with
  add/remove/toggle/test per receiver
- Single-receiver test endpoint: POST /targets/{id}/receivers/{id}/test

Code quality:
- Extract AuthLayout.svelte from login/setup (150 lines CSS dedup)
- Split telegram-bots page (754→51 lines + 3 tab components)
- Split notification-trackers page (547→432 lines + 4 components)
- Deduplicate _send_reply into shared handler.send_reply()
- Add locale column to template models, replace name-based detection
- Fix delete_notification_tracker dead protection check
- Fix check_telegram_bot query (filter by type, remove bogus OR)
- Add graceful scheduler shutdown in lifespan
- Consistent /bots?tab=telegram URLs across all nav links

i18n:
- Error page, chat actions, target types, provider types internationalized
- All new receiver UI strings in EN + RU
This commit is contained in:
2026-03-22 02:19:31 +03:00
parent b525e3e7f4
commit 751097b347
43 changed files with 2584 additions and 1685 deletions
+80 -46
View File
@@ -83,7 +83,7 @@
{
key: 'nav.bots', icon: 'mdiRobot',
children: [
{ href: '/bots', key: 'nav.telegram', icon: 'mdiSendCircle', countKey: 'telegram_bots' },
{ href: '/bots?tab=telegram', key: 'nav.telegram', icon: 'mdiSendCircle', countKey: 'telegram_bots' },
{ href: '/bots?tab=email', key: 'nav.email', icon: 'mdiEmailOutline', countKey: 'email_bots' },
{ href: '/bots?tab=matrix', key: 'nav.matrix', icon: 'mdiMatrix', countKey: 'matrix_bots' },
],
@@ -139,7 +139,7 @@
{ href: '/notification-trackers', key: 'nav.notification', icon: 'mdiBellOutline' },
{ href: '/command-trackers', key: 'nav.commands', icon: 'mdiConsoleLine' },
{ href: '/targets', key: 'nav.targets', icon: 'mdiTarget' },
{ href: '/bots', key: 'nav.bots', icon: 'mdiRobot' },
{ href: '/bots?tab=telegram', key: 'nav.bots', icon: 'mdiRobot' },
]);
const isAuthPage = $derived(
@@ -256,10 +256,7 @@
</div>
{/if}
<button onclick={toggleSidebar}
class="flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200"
style="color: var(--color-muted-foreground); background: transparent;"
onmouseenter={(e) => { e.currentTarget.style.background = 'var(--color-muted)'; e.currentTarget.style.color = 'var(--color-foreground)'; }}
onmouseleave={(e) => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--color-muted-foreground)'; }}
class="sidebar-icon-btn flex items-center justify-center w-8 h-8 rounded-lg transition-all duration-200"
title={collapsed ? t('common.expand') : t('common.collapse')}>
<MdiIcon name={collapsed ? 'mdiChevronRight' : 'mdiChevronLeft'} size={18} />
</button>
@@ -268,10 +265,7 @@
<!-- Search button -->
<div class="{collapsed ? 'px-2 py-1.5' : 'px-3 py-1.5'}" style="border-bottom: 1px solid var(--color-border);">
<button onclick={() => openSearch?.()}
class="flex items-center gap-2 w-full {collapsed ? 'justify-center px-2' : 'px-2.5'} py-1.5 rounded-lg text-sm transition-all duration-200"
style="background: var(--color-muted); color: var(--color-muted-foreground); border: 1px solid var(--color-border);"
onmouseenter={(e) => { e.currentTarget.style.borderColor = 'var(--color-primary)'; e.currentTarget.style.color = 'var(--color-foreground)'; }}
onmouseleave={(e) => { e.currentTarget.style.borderColor = 'var(--color-border)'; e.currentTarget.style.color = 'var(--color-muted-foreground)'; }}
class="search-btn flex items-center gap-2 w-full {collapsed ? 'justify-center px-2' : 'px-2.5'} py-1.5 rounded-lg text-sm transition-all duration-200"
title={t('searchPalette.placeholder')}>
<MdiIcon name="mdiMagnify" size={16} />
{#if !collapsed}
@@ -288,10 +282,7 @@
<!-- Group header -->
<button
onclick={() => collapsed ? null : toggleGroup(entry.key)}
class="nav-item group flex items-center gap-2.5 {collapsed ? 'justify-center px-2' : 'px-3'} py-2 rounded-lg text-sm transition-all duration-200 w-full text-left relative"
style="color: {isGroupActive(entry) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isGroupActive(entry) && !expandedGroups[entry.key] ? 'var(--color-sidebar-active)' : 'transparent'}; font-weight: {isGroupActive(entry) ? '500' : '400'};"
onmouseenter={(e) => { if (!isGroupActive(entry)) { e.currentTarget.style.background = 'var(--color-muted)'; e.currentTarget.style.color = 'var(--color-foreground)'; } }}
onmouseleave={(e) => { if (!isGroupActive(entry)) { e.currentTarget.style.background = isGroupActive(entry) && !expandedGroups[entry.key] ? 'var(--color-sidebar-active)' : 'transparent'; e.currentTarget.style.color = isGroupActive(entry) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'; } }}
class="nav-link group flex items-center gap-2.5 {collapsed ? 'justify-center px-2' : 'px-3'} py-2 rounded-lg text-sm transition-all duration-200 w-full text-left relative {isGroupActive(entry) ? 'active' : ''} {isGroupActive(entry) && !expandedGroups[entry.key] ? 'active-bg' : ''}"
title={collapsed ? t(entry.key) : ''}
>
{#if isGroupActive(entry) && !expandedGroups[entry.key]}
@@ -311,10 +302,7 @@
{#each entry.children as child}
<a
href={child.href}
class="nav-item group flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-sm transition-all duration-200 relative"
style="color: {isActive(child.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(child.href) ? 'var(--color-sidebar-active)' : 'transparent'}; font-weight: {isActive(child.href) ? '500' : '400'};"
onmouseenter={(e) => { if (!isActive(child.href)) { e.currentTarget.style.background = 'var(--color-muted)'; e.currentTarget.style.color = 'var(--color-foreground)'; } }}
onmouseleave={(e) => { if (!isActive(child.href)) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--color-muted-foreground)'; } }}
class="nav-link nav-link-child group flex items-center gap-2 px-2.5 py-1.5 rounded-lg text-sm transition-all duration-200 relative {isActive(child.href) ? 'active' : ''}"
>
{#if isActive(child.href)}
<div class="active-indicator" style="position: absolute; left: -13px; top: 50%; transform: translateY(-50%); width: 3px; height: 60%; border-radius: 0 3px 3px 0; background: var(--color-primary); box-shadow: 0 0 8px var(--color-glow-strong);"></div>
@@ -332,10 +320,7 @@
<!-- Top-level item -->
<a
href={entry.href}
class="nav-item group flex items-center gap-2.5 {collapsed ? 'justify-center px-2' : 'px-3'} py-2 rounded-lg text-sm transition-all duration-200 relative"
style="color: {isActive(entry.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(entry.href) ? 'var(--color-sidebar-active)' : 'transparent'}; font-weight: {isActive(entry.href) ? '500' : '400'};"
onmouseenter={(e) => { if (!isActive(entry.href)) { e.currentTarget.style.background = 'var(--color-muted)'; e.currentTarget.style.color = 'var(--color-foreground)'; } }}
onmouseleave={(e) => { if (!isActive(entry.href)) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'var(--color-muted-foreground)'; } }}
class="nav-link group flex items-center gap-2.5 {collapsed ? 'justify-center px-2' : 'px-3'} py-2 rounded-lg text-sm transition-all duration-200 relative {isActive(entry.href) ? 'active' : ''}"
title={collapsed ? t(entry.key) : ''}
>
{#if isActive(entry.href)}
@@ -358,18 +343,12 @@
<!-- Theme & Language -->
<div class="flex {collapsed ? 'flex-col items-center gap-1 p-2' : 'gap-1.5 px-4 py-2.5'}">
<button onclick={toggleLocale}
class="flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs font-medium transition-all duration-200"
style="background: var(--color-muted); color: var(--color-muted-foreground);"
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--color-foreground)'; e.currentTarget.style.boxShadow = '0 0 8px var(--color-glow)'; }}
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--color-muted-foreground)'; e.currentTarget.style.boxShadow = 'none'; }}
class="footer-pill flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs font-medium transition-all duration-200"
title={t('common.language')}>
{getLocale().toUpperCase()}
</button>
<button onclick={cycleTheme}
class="flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs transition-all duration-200"
style="background: var(--color-muted); color: var(--color-muted-foreground);"
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--color-foreground)'; e.currentTarget.style.boxShadow = '0 0 8px var(--color-glow)'; }}
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--color-muted-foreground)'; e.currentTarget.style.boxShadow = 'none'; }}
class="footer-pill flex items-center justify-center {collapsed ? 'w-8 h-8' : 'px-2.5 py-1'} rounded-lg text-xs transition-all duration-200"
title={t('common.theme')}>
<MdiIcon name={theme.resolved === 'dark' ? 'mdiWeatherNight' : theme.current === 'system' ? 'mdiDesktopTowerMonitor' : 'mdiWeatherSunny'} size={14} />
</button>
@@ -379,10 +358,7 @@
<div class="p-2.5" style="border-top: 1px solid var(--color-border);">
{#if collapsed}
<button onclick={logout}
class="w-full flex justify-center py-2 rounded-lg transition-all duration-200"
style="color: var(--color-muted-foreground);"
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--color-foreground)'; e.currentTarget.style.background = 'var(--color-muted)'; }}
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--color-muted-foreground)'; e.currentTarget.style.background = 'transparent'; }}
class="sidebar-icon-btn w-full flex justify-center py-2 rounded-lg transition-all duration-200"
title={t('nav.logout')}>
<MdiIcon name="mdiLogout" size={16} />
</button>
@@ -400,19 +376,13 @@
</div>
</div>
<button onclick={logout}
class="p-1.5 rounded-lg transition-all duration-200"
style="color: var(--color-muted-foreground);"
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--color-foreground)'; e.currentTarget.style.background = 'var(--color-muted)'; }}
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--color-muted-foreground)'; e.currentTarget.style.background = 'transparent'; }}
class="sidebar-icon-btn p-1.5 rounded-lg transition-all duration-200"
title={t('nav.logout')}>
<MdiIcon name="mdiLogout" size={15} />
</button>
</div>
<button onclick={() => showPasswordForm = true}
class="text-[0.7rem] mt-1.5 transition-colors duration-200 flex items-center gap-1"
style="color: var(--color-muted-foreground);"
onmouseenter={(e) => { e.currentTarget.style.color = 'var(--color-primary)'; }}
onmouseleave={(e) => { e.currentTarget.style.color = 'var(--color-muted-foreground)'; }}>
class="change-pwd-link text-[0.7rem] mt-1.5 transition-colors duration-200 flex items-center gap-1">
<MdiIcon name="mdiKeyVariant" size={12} />
{t('common.changePassword')}
</button>
@@ -472,10 +442,7 @@
<p class="text-sm" style="color: var({pwdSuccess ? '--color-success-fg' : '--color-error-fg'});">{pwdMsg}</p>
{/if}
<button type="submit"
class="w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200"
style="background: var(--color-primary); color: var(--color-primary-foreground);"
onmouseenter={(e) => { e.currentTarget.style.boxShadow = '0 0 16px var(--color-glow-strong)'; }}
onmouseleave={(e) => { e.currentTarget.style.boxShadow = 'none'; }}>
class="primary-btn w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200">
{t('common.save')}
</button>
</form>
@@ -488,6 +455,73 @@
@media (max-width: 767px) {
.mobile-nav { display: flex !important; }
}
/* Sidebar icon button (toggle, logout) */
.sidebar-icon-btn {
color: var(--color-muted-foreground);
background: transparent;
}
.sidebar-icon-btn:hover {
background: var(--color-muted);
color: var(--color-foreground);
}
/* Search button */
.search-btn {
background: var(--color-muted);
color: var(--color-muted-foreground);
border: 1px solid var(--color-border);
}
.search-btn:hover {
border-color: var(--color-primary);
color: var(--color-foreground);
}
/* Nav links (top-level items, group headers, group children) */
.nav-link {
color: var(--color-muted-foreground);
background: transparent;
font-weight: 400;
}
.nav-link:not(.active):hover {
background: var(--color-muted);
color: var(--color-foreground);
}
.nav-link.active {
color: var(--color-primary);
font-weight: 500;
}
.nav-link.active-bg {
background: var(--color-sidebar-active);
}
/* Footer pill buttons (locale, theme) */
.footer-pill {
background: var(--color-muted);
color: var(--color-muted-foreground);
}
.footer-pill:hover {
color: var(--color-foreground);
box-shadow: 0 0 8px var(--color-glow);
}
/* Change password link */
.change-pwd-link {
color: var(--color-muted-foreground);
}
.change-pwd-link:hover {
color: var(--color-primary);
}
/* Primary action button (password form submit) */
.primary-btn {
background: var(--color-primary);
color: var(--color-primary-foreground);
}
.primary-btn:hover {
box-shadow: 0 0 16px var(--color-glow-strong);
}
.nav-badge {
font-size: 0.6rem;
font-weight: 600;