fix: quick deploy duplicate detection, logout UX, backup toggle, CSP, SSE guard, and migration

- Detect existing projects with same image on quick deploy; show conflict dialog with options
- Move logout button to sidebar header as icon-only
- Replace backup checkbox with ToggleSwitch component
- Allow unsafe-inline in CSP script-src for SvelteKit hydration
- Guard SSE connection behind isAuthenticated() check
- Add notification_url ALTER TABLE migration for existing databases
- Restore RegisterPersistentLogger on event bus
This commit is contained in:
2026-04-04 14:40:59 +03:00
parent 205a5a36c6
commit 6667abf03c
11 changed files with 259 additions and 35 deletions
+30 -11
View File
@@ -6,12 +6,13 @@
import Toast from '$lib/components/Toast.svelte';
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
import LocaleSwitcher from '$lib/components/LocaleSwitcher.svelte';
import { IconDashboard, IconProjects, IconDeploy, IconSettings, IconMenu, IconX } from '$lib/components/icons';
import { IconDashboard, IconProjects, IconDeploy, IconSettings, IconMenu, IconX, IconLogout } from '$lib/components/icons';
import { goto } from '$app/navigation';
import { connectGlobalEvents, type SSEConnection } from '$lib/sse';
import { instanceStatusStore } from '$lib/stores/instance-status';
import { resolvedTheme, applyTheme } from '$lib/stores/theme';
import { exchangeOidcToken, setAuthToken } from '$lib/auth';
import { exchangeOidcToken, setAuthToken, clearAuth, isAuthenticated } from '$lib/auth';
import { logout as apiLogout } from '$lib/api';
import { t } from '$lib/i18n';
interface Props {
@@ -68,14 +69,17 @@
}
}
sseConnection = connectGlobalEvents({
onInstanceStatus(payload) {
instanceStatusStore.update(payload);
},
onDeployStatus(payload) {
instanceStatusStore.notifyDeploy(payload);
}
});
// Only connect SSE when authenticated (has a token).
if (isAuthenticated()) {
sseConnection = connectGlobalEvents({
onInstanceStatus(payload) {
instanceStatusStore.update(payload);
},
onDeployStatus(payload) {
instanceStatusStore.notifyDeploy(payload);
}
});
}
});
onDestroy(() => {
@@ -112,9 +116,24 @@
</div>
<span class="text-base font-bold text-[var(--text-primary)]">{$t('app.name')}</span>
<!-- Logout button -->
<button
type="button"
title={$t('nav.logout')}
aria-label={$t('nav.logout')}
onclick={async () => {
try { await apiLogout(); } catch { /* best effort */ }
clearAuth();
goto('/login');
}}
class="ml-auto rounded-md p-1.5 text-[var(--text-tertiary)] hover:text-[var(--color-danger)] transition-colors"
>
<IconLogout size={18} />
</button>
<!-- Close sidebar (mobile) -->
<button
class="ml-auto rounded-md p-1 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] lg:hidden"
class="rounded-md p-1 text-[var(--text-tertiary)] hover:text-[var(--text-primary)] lg:hidden"
onclick={() => { sidebarOpen = false; }}
aria-label="Close sidebar"
>