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:
@@ -5,6 +5,7 @@
|
||||
import { t } from '$lib/i18n';
|
||||
import { initTheme } from '$lib/theme.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import AuthLayout from '$lib/components/AuthLayout.svelte';
|
||||
|
||||
let username = $state('admin');
|
||||
let password = $state('');
|
||||
@@ -29,201 +30,42 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="auth-page">
|
||||
<div class="auth-bg"></div>
|
||||
<div class="auth-grid"></div>
|
||||
|
||||
<div class="auth-card-wrapper" class:visible={mounted}>
|
||||
<div class="auth-card">
|
||||
<div class="text-center mb-8">
|
||||
<div class="auth-logo-icon">
|
||||
<MdiIcon name="mdiShieldAccount" size={28} />
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mt-4 tracking-tight">
|
||||
<span style="color: var(--color-primary);">Notify</span> Bridge
|
||||
</h1>
|
||||
<p class="text-sm mt-1" style="color: var(--color-muted-foreground);">{t('auth.setupDescription')}</p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="auth-error animate-fade-slide-in">
|
||||
<MdiIcon name="mdiAlertCircle" size={16} />
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="auth-label">{t('auth.username')}</label>
|
||||
<input id="username" type="text" bind:value={username} required class="auth-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="auth-label">{t('auth.password')}</label>
|
||||
<input id="password" type="password" bind:value={password} required class="auth-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm" class="auth-label">{t('auth.confirmPassword')}</label>
|
||||
<input id="confirm" type="password" bind:value={confirmPassword} required class="auth-input" />
|
||||
</div>
|
||||
<button type="submit" disabled={submitting} class="auth-submit">
|
||||
{#if submitting}
|
||||
<div class="w-4 h-4 rounded-full border-2 border-current border-t-transparent animate-spin"></div>
|
||||
{/if}
|
||||
{submitting ? t('auth.creatingAccount') : t('auth.createAccount')}
|
||||
</button>
|
||||
</form>
|
||||
<AuthLayout visible={mounted}>
|
||||
<div class="text-center mb-8">
|
||||
<div class="auth-logo-icon">
|
||||
<MdiIcon name="mdiShieldAccount" size={28} />
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mt-4 tracking-tight">
|
||||
<span style="color: var(--color-primary);">Notify</span> Bridge
|
||||
</h1>
|
||||
<p class="text-sm mt-1" style="color: var(--color-muted-foreground);">{t('auth.setupDescription')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.auth-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: var(--color-background);
|
||||
}
|
||||
{#if error}
|
||||
<div class="auth-error animate-fade-slide-in">
|
||||
<MdiIcon name="mdiAlertCircle" size={16} />
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
.auth-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 20% 30%, var(--color-glow-strong), transparent 60%),
|
||||
radial-gradient(ellipse 60% 80% at 80% 70%, rgba(99, 102, 241, 0.08), transparent 60%),
|
||||
radial-gradient(ellipse 50% 50% at 50% 50%, var(--color-glow), transparent 70%);
|
||||
animation: gradientShift 12s ease-in-out infinite;
|
||||
background-size: 200% 200%;
|
||||
}
|
||||
|
||||
.auth-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
opacity: 0.3;
|
||||
background-image: radial-gradient(circle at 1px 1px, var(--color-border) 0.5px, transparent 0);
|
||||
background-size: 32px 32px;
|
||||
}
|
||||
|
||||
.auth-card-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
padding: 1rem;
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.98);
|
||||
transition: opacity 0.5s ease-out, transform 0.5s ease-out;
|
||||
}
|
||||
|
||||
.auth-card-wrapper.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
background: var(--color-card);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.08),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
|
||||
}
|
||||
|
||||
:global([data-theme="dark"]) .auth-card {
|
||||
box-shadow:
|
||||
0 4px 24px rgba(0, 0, 0, 0.3),
|
||||
0 0 48px var(--color-glow),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.03) inset;
|
||||
}
|
||||
|
||||
.auth-logo-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.5rem;
|
||||
height: 3.5rem;
|
||||
border-radius: 1rem;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-foreground);
|
||||
box-shadow: 0 0 24px var(--color-glow-strong);
|
||||
}
|
||||
|
||||
.auth-label {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.375rem;
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
.auth-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.875rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
background: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.auth-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 3px var(--color-glow), 0 0 16px var(--color-glow);
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.625rem;
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background: var(--color-error-bg);
|
||||
color: var(--color-error-fg);
|
||||
}
|
||||
|
||||
.auth-submit {
|
||||
width: 100%;
|
||||
padding: 0.625rem;
|
||||
border-radius: 0.625rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
background: var(--color-primary);
|
||||
color: var(--color-primary-foreground);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.auth-submit:hover:not(:disabled) {
|
||||
box-shadow: 0 0 24px var(--color-glow-strong);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.auth-submit:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.auth-submit:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@keyframes gradientShift {
|
||||
0% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
100% { background-position: 0% 50%; }
|
||||
}
|
||||
</style>
|
||||
<form onsubmit={handleSubmit} class="space-y-4">
|
||||
<div>
|
||||
<label for="username" class="auth-label">{t('auth.username')}</label>
|
||||
<input id="username" type="text" bind:value={username} required class="auth-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" class="auth-label">{t('auth.password')}</label>
|
||||
<input id="password" type="password" bind:value={password} required class="auth-input" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="confirm" class="auth-label">{t('auth.confirmPassword')}</label>
|
||||
<input id="confirm" type="password" bind:value={confirmPassword} required class="auth-input" />
|
||||
</div>
|
||||
<button type="submit" disabled={submitting} class="auth-submit">
|
||||
{#if submitting}
|
||||
<div class="w-4 h-4 rounded-full border-2 border-current border-t-transparent animate-spin"></div>
|
||||
{/if}
|
||||
{submitting ? t('auth.creatingAccount') : t('auth.createAccount')}
|
||||
</button>
|
||||
</form>
|
||||
</AuthLayout>
|
||||
|
||||
Reference in New Issue
Block a user