751097b347
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
192 lines
4.3 KiB
Svelte
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>
|