diff --git a/.gitignore b/.gitignore index e7e32ab..a580b95 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ htmlcov/ # Claude Code .claude/ +__pycache__/ +test-data/ diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index dcfcbad..e0d12de 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -244,6 +244,10 @@ "dark": "Dark", "system": "System", "test": "Test", - "create": "Create" + "create": "Create", + "changePassword": "Change Password", + "currentPassword": "Current password", + "newPassword": "New password", + "passwordChanged": "Password changed successfully" } } diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index 27880c2..295d048 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -244,6 +244,10 @@ "dark": "Тёмная", "system": "Системная", "test": "Тест", - "create": "Создать" + "create": "Создать", + "changePassword": "Сменить пароль", + "currentPassword": "Текущий пароль", + "newPassword": "Новый пароль", + "passwordChanged": "Пароль успешно изменён" } } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index dc4c162..d68efd7 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -3,6 +3,7 @@ import { page } from '$app/state'; import { goto } from '$app/navigation'; import { onMount } from 'svelte'; + import { api } from '$lib/api'; import { getAuth, loadUser, logout } from '$lib/auth.svelte'; import { t, initLocale, getLocale, setLocale, type Locale } from '$lib/i18n'; import { getTheme, initTheme, setTheme, type Theme } from '$lib/theme.svelte'; @@ -11,6 +12,21 @@ const auth = getAuth(); const theme = getTheme(); + let showPasswordForm = $state(false); + let pwdCurrent = $state(''); + let pwdNew = $state(''); + let pwdMsg = $state(''); + + async function changePassword(e: SubmitEvent) { + e.preventDefault(); pwdMsg = ''; + try { + await api('/auth/password', { method: 'PUT', body: JSON.stringify({ current_password: pwdCurrent, new_password: pwdNew }) }); + pwdMsg = t('common.passwordChanged'); + pwdCurrent = ''; pwdNew = ''; + setTimeout(() => { showPasswordForm = false; pwdMsg = ''; }, 2000); + } catch (err: any) { pwdMsg = err.message; } + } + // Reactive counter to force re-render on locale change let localeVersion = $state(0); let collapsed = $state(false); @@ -146,17 +162,33 @@ ⏻ {:else} -
-
-

{auth.user.username}

-

{auth.user.role}

+
+
+
+

{auth.user.username}

+

{auth.user.role}

+
+
- + {#if showPasswordForm} +
+ + + {#if pwdMsg}

{pwdMsg}

{/if} + +
+ {/if}
{/if}
diff --git a/frontend/src/routes/servers/+page.svelte b/frontend/src/routes/servers/+page.svelte index 03b500f..9da4a94 100644 --- a/frontend/src/routes/servers/+page.svelte +++ b/frontend/src/routes/servers/+page.svelte @@ -14,8 +14,17 @@ let submitting = $state(false); let loaded = $state(false); + let health = $state>({}); + onMount(load); - async function load() { try { servers = await api('/servers'); } catch {} finally { loaded = true; } } + async function load() { + try { servers = await api('/servers'); } catch {} finally { loaded = true; } + // Ping all servers in background + for (const s of servers) { + health[s.id] = null; // loading + api(`/servers/${s.id}/ping`).then(r => health[s.id] = r.online).catch(() => health[s.id] = false); + } + } function openNew() { form = { name: 'Immich', url: '', api_key: '' }; @@ -88,9 +97,13 @@ {#each servers as server}
-
-

{server.name}

-

{server.url}

+
+ +
+

{server.name}

+

{server.url}

+
diff --git a/frontend/src/routes/trackers/+page.svelte b/frontend/src/routes/trackers/+page.svelte index 2902720..65e1b8a 100644 --- a/frontend/src/routes/trackers/+page.svelte +++ b/frontend/src/routes/trackers/+page.svelte @@ -13,6 +13,7 @@ let albums = $state([]); let showForm = $state(false); let editing = $state(null); + let albumFilter = $state(''); const defaultForm = () => ({ name: '', server_id: 0, album_ids: [] as string[], target_ids: [] as number[], scan_interval: 60, @@ -84,12 +85,19 @@
{#if albums.length > 0}
- -
- {#each albums as album} -