Files
notify-bridge/frontend/src/lib/components/AuthLayout.svelte
T
alexei.dolgolyov 751097b347 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
2026-03-22 02:19:31 +03:00

192 lines
4.3 KiB
Svelte

<script lang="ts">
import type { Snippet } from 'svelte';
let { visible = false, children }: { visible: boolean; children: Snippet } = $props();
</script>
<div class="auth-page">
<!-- Animated gradient mesh background -->
<div class="auth-bg"></div>
<div class="auth-grid"></div>
<!-- Card -->
<div class="auth-card-wrapper" class:visible>
<div class="auth-card">
{@render children()}
</div>
</div>
</div>
<style>
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
background: var(--color-background);
}
.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;
backdrop-filter: blur(8px);
}
: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;
}
:global(.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);
}
:global(.auth-control-btn) {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem 0.625rem;
border-radius: 0.5rem;
font-size: 0.7rem;
font-weight: 500;
background: var(--color-muted);
color: var(--color-muted-foreground);
border: none;
cursor: pointer;
transition: all 0.2s ease;
}
:global(.auth-control-btn:hover) {
color: var(--color-foreground);
box-shadow: 0 0 8px var(--color-glow);
}
:global(.auth-label) {
display: block;
font-size: 0.8rem;
font-weight: 500;
margin-bottom: 0.375rem;
color: var(--color-foreground);
}
:global(.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;
}
:global(.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);
}
:global(.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);
}
:global(.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;
}
:global(.auth-submit:hover:not(:disabled)) {
box-shadow: 0 0 24px var(--color-glow-strong);
transform: translateY(-1px);
}
:global(.auth-submit:active:not(:disabled)) {
transform: translateY(0);
}
:global(.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>