From b708b14f32f9fd500ec2375293771bacb762cd16 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 19 Mar 2026 17:10:34 +0300 Subject: [PATCH] Add frontend for TrackingConfig + TemplateConfig, fix locale, simplify trackers New pages: - /tracking-configs: Full CRUD with event tracking, asset display, periodic summary, scheduled assets, and memory mode sections. Collapsible sub-sections that show/hide based on enabled state. - /template-configs: Full CRUD with all 21 template slots organized into 5 fieldsets (event messages, asset formatting, date/location, scheduled messages, telegram). Preview support per slot. Updated pages: - Targets: added tracking_config_id + template_config_id selectors (dropdowns populated from configs). Configs are reusable. - Trackers: simplified to album selection + scan interval + targets. Added Test, Test Periodic, Test Memory buttons per tracker. - Nav: replaced Templates with Tracking + Templates config links Other fixes: - Language button: now triggers window.location.reload() to force all child pages to re-evaluate t() calls - Dark theme buttons: changed primary color to dark gray in dark mode - Removed old /templates page (replaced by /template-configs) - Added .gitignore for __pycache__ in server package Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/app.css | 4 +- frontend/src/lib/i18n/en.json | 76 ++++++- frontend/src/lib/i18n/ru.json | 76 ++++++- frontend/src/routes/+layout.svelte | 7 +- frontend/src/routes/login/+page.svelte | 2 +- frontend/src/routes/targets/+page.svelte | 41 +++- .../src/routes/template-configs/+page.svelte | 176 +++++++++++++++ frontend/src/routes/templates/+page.svelte | 117 ---------- frontend/src/routes/trackers/+page.svelte | 69 +----- .../src/routes/tracking-configs/+page.svelte | 204 ++++++++++++++++++ packages/server/.gitignore | 1 + .../src/immich_watcher_server/api/trackers.py | 38 ++++ 12 files changed, 619 insertions(+), 192 deletions(-) create mode 100644 frontend/src/routes/template-configs/+page.svelte delete mode 100644 frontend/src/routes/templates/+page.svelte create mode 100644 frontend/src/routes/tracking-configs/+page.svelte create mode 100644 packages/server/.gitignore diff --git a/frontend/src/app.css b/frontend/src/app.css index 9b532e2..5aef0c0 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -30,8 +30,8 @@ --color-muted: #27272a; --color-muted-foreground: #a1a1aa; --color-border: #3f3f46; - --color-primary: #fafafa; - --color-primary-foreground: #18181b; + --color-primary: #3f3f46; + --color-primary-foreground: #fafafa; --color-accent: #27272a; --color-accent-foreground: #fafafa; --color-destructive: #f87171; diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index eaa97c6..d4adb1f 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -7,7 +7,8 @@ "dashboard": "Dashboard", "servers": "Servers", "trackers": "Trackers", - "templates": "Templates", + "trackingConfigs": "Tracking", + "templateConfigs": "Templates", "targets": "Targets", "users": "Users", "logout": "Logout" @@ -157,6 +158,75 @@ "confirmDelete": "Delete this user?", "joined": "joined" }, + "trackingConfig": { + "title": "Tracking Configs", + "description": "Define what events and assets to react to", + "newConfig": "New Config", + "name": "Name", + "namePlaceholder": "Default tracking", + "noConfigs": "No tracking configs yet.", + "eventTracking": "Event Tracking", + "assetsAdded": "Assets added", + "assetsRemoved": "Assets removed", + "albumRenamed": "Album renamed", + "albumDeleted": "Album deleted", + "trackImages": "Track images", + "trackVideos": "Track videos", + "favoritesOnly": "Favorites only", + "assetDisplay": "Asset Display", + "includePeople": "Include people", + "includeDetails": "Include asset details", + "maxAssets": "Max assets to show", + "sortBy": "Sort by", + "sortOrder": "Sort order", + "periodicSummary": "Periodic Summary", + "enabled": "Enabled", + "intervalDays": "Interval (days)", + "startDate": "Start date", + "times": "Times (HH:MM)", + "scheduledAssets": "Scheduled Assets", + "albumMode": "Album mode", + "limit": "Limit", + "assetType": "Asset type", + "minRating": "Min rating", + "memoryMode": "Memory Mode (On This Day)", + "test": "Test" + }, + "templateConfig": { + "title": "Template Configs", + "description": "Define how notification messages are formatted", + "newConfig": "New Config", + "name": "Name", + "namePlaceholder": "Default EN", + "noConfigs": "No template configs yet.", + "eventMessages": "Event Messages", + "assetsAdded": "Assets added", + "assetsRemoved": "Assets removed", + "albumRenamed": "Album renamed", + "albumDeleted": "Album deleted", + "assetFormatting": "Asset Formatting", + "imageTemplate": "Image item", + "videoTemplate": "Video item", + "assetsWrapper": "Assets wrapper", + "moreMessage": "More message", + "peopleFormat": "People format", + "dateLocation": "Date & Location", + "dateFormat": "Date format", + "commonDate": "Common date", + "uniqueDate": "Per-asset date", + "locationFormat": "Location format", + "commonLocation": "Common location", + "uniqueLocation": "Per-asset location", + "favoriteIndicator": "Favorite indicator", + "scheduledMessages": "Scheduled Messages", + "periodicSummary": "Periodic summary", + "periodicAlbum": "Per-album item", + "scheduledAssets": "Scheduled assets", + "memoryMode": "Memory mode", + "telegramSettings": "Telegram", + "videoWarning": "Video warning", + "preview": "Preview" + }, "common": { "loading": "Loading...", "save": "Save", @@ -171,6 +241,8 @@ "theme": "Theme", "light": "Light", "dark": "Dark", - "system": "System" + "system": "System", + "test": "Test", + "create": "Create" } } diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index 8fee732..3619da5 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -7,7 +7,8 @@ "dashboard": "Главная", "servers": "Серверы", "trackers": "Трекеры", - "templates": "Шаблоны", + "trackingConfigs": "Отслеживание", + "templateConfigs": "Шаблоны", "targets": "Получатели", "users": "Пользователи", "logout": "Выход" @@ -157,6 +158,75 @@ "confirmDelete": "Удалить этого пользователя?", "joined": "зарегистрирован" }, + "trackingConfig": { + "title": "Конфигурации отслеживания", + "description": "Определите, на какие события и файлы реагировать", + "newConfig": "Новая конфигурация", + "name": "Название", + "namePlaceholder": "Основное отслеживание", + "noConfigs": "Конфигураций отслеживания пока нет.", + "eventTracking": "Отслеживание событий", + "assetsAdded": "Добавлены файлы", + "assetsRemoved": "Удалены файлы", + "albumRenamed": "Альбом переименован", + "albumDeleted": "Альбом удалён", + "trackImages": "Фото", + "trackVideos": "Видео", + "favoritesOnly": "Только избранные", + "assetDisplay": "Отображение файлов", + "includePeople": "Включать людей", + "includeDetails": "Включать детали", + "maxAssets": "Макс. файлов", + "sortBy": "Сортировка", + "sortOrder": "Порядок", + "periodicSummary": "Периодическая сводка", + "enabled": "Включено", + "intervalDays": "Интервал (дни)", + "startDate": "Дата начала", + "times": "Время (ЧЧ:ММ)", + "scheduledAssets": "Запланированные фото", + "albumMode": "Режим альбомов", + "limit": "Лимит", + "assetType": "Тип файлов", + "minRating": "Мин. рейтинг", + "memoryMode": "Воспоминания (В этот день)", + "test": "Тест" + }, + "templateConfig": { + "title": "Конфигурации шаблонов", + "description": "Определите формат уведомлений", + "newConfig": "Новая конфигурация", + "name": "Название", + "namePlaceholder": "По умолчанию RU", + "noConfigs": "Конфигураций шаблонов пока нет.", + "eventMessages": "Сообщения о событиях", + "assetsAdded": "Добавлены файлы", + "assetsRemoved": "Удалены файлы", + "albumRenamed": "Альбом переименован", + "albumDeleted": "Альбом удалён", + "assetFormatting": "Форматирование файлов", + "imageTemplate": "Шаблон фото", + "videoTemplate": "Шаблон видео", + "assetsWrapper": "Обёртка списка", + "moreMessage": "Сообщение \"ещё\"", + "peopleFormat": "Формат людей", + "dateLocation": "Дата и место", + "dateFormat": "Формат даты", + "commonDate": "Общая дата", + "uniqueDate": "Дата файла", + "locationFormat": "Формат места", + "commonLocation": "Общее место", + "uniqueLocation": "Место файла", + "favoriteIndicator": "Индикатор избранного", + "scheduledMessages": "Запланированные сообщения", + "periodicSummary": "Периодическая сводка", + "periodicAlbum": "Элемент альбома", + "scheduledAssets": "Запланированные фото", + "memoryMode": "Воспоминания", + "telegramSettings": "Telegram", + "videoWarning": "Предупреждение о видео", + "preview": "Предпросмотр" + }, "common": { "loading": "Загрузка...", "save": "Сохранить", @@ -171,6 +241,8 @@ "theme": "Тема", "light": "Светлая", "dark": "Тёмная", - "system": "Системная" + "system": "Системная", + "test": "Тест", + "create": "Создать" } } diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte index d655538..dc4c162 100644 --- a/frontend/src/routes/+layout.svelte +++ b/frontend/src/routes/+layout.svelte @@ -19,7 +19,8 @@ { href: '/', key: 'nav.dashboard', icon: '⊞' }, { href: '/servers', key: 'nav.servers', icon: '⬡' }, { href: '/trackers', key: 'nav.trackers', icon: '◎' }, - { href: '/templates', key: 'nav.templates', icon: '⎘' }, + { href: '/tracking-configs', key: 'nav.trackingConfigs', icon: '⚙' }, + { href: '/template-configs', key: 'nav.templateConfigs', icon: '⎘' }, { href: '/targets', key: 'nav.targets', icon: '◇' }, ]; @@ -54,7 +55,9 @@ function toggleLocale() { setLocale(getLocale() === 'en' ? 'ru' : 'en'); - localeVersion++; // trigger re-render + localeVersion++; + // Force full page re-render so child components re-evaluate t() calls + window.location.reload(); } function toggleSidebar() { diff --git a/frontend/src/routes/login/+page.svelte b/frontend/src/routes/login/+page.svelte index c2ff7d7..e739635 100644 --- a/frontend/src/routes/login/+page.svelte +++ b/frontend/src/routes/login/+page.svelte @@ -39,7 +39,7 @@
- diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index 30299dc..13f4139 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -7,19 +7,28 @@ import Loading from '$lib/components/Loading.svelte'; let targets = $state([]); + let trackingConfigs = $state([]); + let templateConfigs = $state([]); let showForm = $state(false); let editing = $state(null); let formType = $state<'telegram' | 'webhook'>('telegram'); const defaultForm = () => ({ name: '', bot_token: '', chat_id: '', url: '', headers: '', max_media_to_send: 50, max_media_per_group: 10, media_delay: 500, max_asset_size: 50, - disable_url_preview: false, send_large_photos_as_documents: false, ai_captions: false }); + disable_url_preview: false, send_large_photos_as_documents: false, ai_captions: false, + tracking_config_id: 0, template_config_id: 0 }); let form = $state(defaultForm()); let error = $state(''); let testResult = $state(''); let loaded = $state(false); onMount(load); - async function load() { try { targets = await api('/targets'); } catch {} finally { loaded = true; } } + async function load() { + try { + [targets, trackingConfigs, templateConfigs] = await Promise.all([ + api('/targets'), api('/tracking-configs'), api('/template-configs') + ]); + } catch {} finally { loaded = true; } + } function openNew() { form = defaultForm(); formType = 'telegram'; editing = null; showForm = true; } function edit(tgt: any) { @@ -31,6 +40,8 @@ media_delay: c.media_delay ?? 500, max_asset_size: c.max_asset_size ?? 50, disable_url_preview: c.disable_url_preview ?? false, send_large_photos_as_documents: c.send_large_photos_as_documents ?? false, ai_captions: c.ai_captions ?? false, + tracking_config_id: tgt.tracking_config_id ?? 0, + template_config_id: tgt.template_config_id ?? 0, }; editing = tgt.id; showForm = true; } @@ -45,10 +56,12 @@ disable_url_preview: form.disable_url_preview, send_large_photos_as_documents: form.send_large_photos_as_documents, ai_captions: form.ai_captions } : { url: form.url, headers: form.headers ? JSON.parse(form.headers) : {}, ai_captions: form.ai_captions }; + const trkId = form.tracking_config_id || null; + const tplId = form.template_config_id || null; if (editing) { - await api(`/targets/${editing}`, { method: 'PUT', body: JSON.stringify({ name: form.name, config }) }); + await api(`/targets/${editing}`, { method: 'PUT', body: JSON.stringify({ name: form.name, config, tracking_config_id: trkId, template_config_id: tplId }) }); } else { - await api('/targets', { method: 'POST', body: JSON.stringify({ type: formType, name: form.name, config }) }); + await api('/targets', { method: 'POST', body: JSON.stringify({ type: formType, name: form.name, config, tracking_config_id: trkId, template_config_id: tplId }) }); } showForm = false; editing = null; await load(); } catch (err: any) { error = err.message; } @@ -134,7 +147,25 @@
{/if} - + +
+
+ + +
+
+ + +
+
+ + diff --git a/frontend/src/routes/template-configs/+page.svelte b/frontend/src/routes/template-configs/+page.svelte new file mode 100644 index 0000000..0a11d84 --- /dev/null +++ b/frontend/src/routes/template-configs/+page.svelte @@ -0,0 +1,176 @@ + + + + + + +{#if !loaded}{:else} + +{#if showForm} + + {#if error}
{error}
{/if} +
+
+ + +
+ + {#each templateSlots as group} +
+ {t(`templateConfig.${group.group}`)} +
+ {#each group.slots as slot} +
+ + +
+ {/each} +
+
+ {/each} + + +
+
+{/if} + +{#if configs.length === 0 && !showForm} +

{t('templateConfig.noConfigs')}

+{:else} +
+ {#each configs as config} + +
+
+

{config.name}

+
{config.message_assets_added?.slice(0, 120)}...
+ {#if previewResult && previewId === config.id} +
+

{previewSlot}:

+
{previewResult}
+
+ {/if} +
+
+ + + +
+
+
+ {/each} +
+{/if} + +{/if} diff --git a/frontend/src/routes/templates/+page.svelte b/frontend/src/routes/templates/+page.svelte deleted file mode 100644 index f905660..0000000 --- a/frontend/src/routes/templates/+page.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - -{#if !loaded}{:else} - -{#if showForm} - - {#if error}
{error}
{/if} -
-
-
- - -
-
- - -
-
-
- - -

- {t('templates.variables')}: {'{{ album_name }}'}, {'{{ added_count }}'}, {'{{ removed_count }}'}, {'{{ people }}'}, {'{{ change_type }}'}, {'{{ album_url }}'}, {'{{ added_assets }}'}, {'{{ old_name }}'}, {'{{ new_name }}'} -

-
- -
-
-{/if} - -{#if templates.length === 0 && !showForm} -

{t('templates.noTemplates')}

-{:else} -
- {#each templates as tmpl} - -
-
-
-

{tmpl.name}

- {#if tmpl.event_type} - {tmpl.event_type} - {/if} -
-
{tmpl.body.slice(0, 200)}{tmpl.body.length > 200 ? '...' : ''}
- {#if preview && previewId === tmpl.id && !showForm} -
-
{preview}
-
- {/if} -
-
- - - -
-
-
- {/each} -
-{/if} - -{/if} diff --git a/frontend/src/routes/trackers/+page.svelte b/frontend/src/routes/trackers/+page.svelte index 7c62454..2902720 100644 --- a/frontend/src/routes/trackers/+page.svelte +++ b/frontend/src/routes/trackers/+page.svelte @@ -14,11 +14,8 @@ let showForm = $state(false); let editing = $state(null); const defaultForm = () => ({ - name: '', server_id: 0, album_ids: [] as string[], event_types: ['assets_added'], + name: '', server_id: 0, album_ids: [] as string[], target_ids: [] as number[], scan_interval: 60, - track_images: true, track_videos: true, notify_favorites_only: false, - include_people: true, include_asset_details: false, - max_assets_to_show: 5, assets_order_by: 'none', assets_order: 'descending', }); let form = $state(defaultForm()); let error = $state(''); @@ -33,11 +30,7 @@ async function edit(trk: any) { form = { name: trk.name, server_id: trk.server_id, album_ids: [...trk.album_ids], - event_types: [...trk.event_types], target_ids: [...trk.target_ids], scan_interval: trk.scan_interval, - track_images: trk.track_images ?? true, track_videos: trk.track_videos ?? true, - notify_favorites_only: trk.notify_favorites_only ?? false, include_people: trk.include_people ?? true, - include_asset_details: trk.include_asset_details ?? false, max_assets_to_show: trk.max_assets_to_show ?? 5, - assets_order_by: trk.assets_order_by ?? 'none', assets_order: trk.assets_order ?? 'descending', + target_ids: [...trk.target_ids], scan_interval: trk.scan_interval, }; editing = trk.id; showForm = true; if (form.server_id) await loadAlbums(); @@ -103,57 +96,8 @@
{/if}
- -
- {#each ['assets_added', 'assets_removed', 'album_renamed', 'album_sharing_changed', 'changed'] as evt} - - {/each} -
-
- - -
- - - - - -
- - -
-
- - -
-
- - -
-
- -
-
- - -
-
- - -
+ +
{#if targets.length > 0} @@ -191,10 +135,13 @@ {tracker.enabled ? t('trackers.active') : t('trackers.paused')}
-

{tracker.album_ids.length} {t('trackers.albums_count')} · {t('trackers.every')} {tracker.scan_interval}s · {tracker.event_types.join(', ')}

+

{tracker.album_ids.length} {t('trackers.albums_count')} · {t('trackers.every')} {tracker.scan_interval}s · {tracker.target_ids.length} target(s)

+ + + diff --git a/frontend/src/routes/tracking-configs/+page.svelte b/frontend/src/routes/tracking-configs/+page.svelte new file mode 100644 index 0000000..aeb6ff3 --- /dev/null +++ b/frontend/src/routes/tracking-configs/+page.svelte @@ -0,0 +1,204 @@ + + + + + + +{#if !loaded}{:else} + +{#if showForm} + + {#if error}
{error}
{/if} +
+
+ + +
+ + +
+ {t('trackingConfig.eventTracking')} +
+ + + + + + + +
+
+ + +
+ {t('trackingConfig.assetDisplay')} +
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ {t('trackingConfig.periodicSummary')} + + {#if form.periodic_enabled} +
+
+
+
+
+ {/if} +
+ + +
+ {t('trackingConfig.scheduledAssets')} + + {#if form.scheduled_enabled} +
+
+
+
+
+
+
+
+ +
+ {/if} +
+ + +
+ {t('trackingConfig.memoryMode')} + + {#if form.memory_enabled} +
+
+
+
+
+
+
+
+ +
+ {/if} +
+ + +
+
+{/if} + +{#if configs.length === 0 && !showForm} +

{t('trackingConfig.noConfigs')}

+{:else} +
+ {#each configs as config} + +
+
+

{config.name}

+

+ {[config.track_assets_added && 'added', config.track_assets_removed && 'removed', config.track_album_renamed && 'renamed', config.track_album_deleted && 'deleted'].filter(Boolean).join(', ')} + {config.periodic_enabled ? ' · periodic' : ''} + {config.scheduled_enabled ? ' · scheduled' : ''} + {config.memory_enabled ? ' · memory' : ''} +

+
+
+ + +
+
+
+ {/each} +
+{/if} + +{/if} diff --git a/packages/server/.gitignore b/packages/server/.gitignore new file mode 100644 index 0000000..c18dd8d --- /dev/null +++ b/packages/server/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/packages/server/src/immich_watcher_server/api/trackers.py b/packages/server/src/immich_watcher_server/api/trackers.py index dd8b349..5780c5c 100644 --- a/packages/server/src/immich_watcher_server/api/trackers.py +++ b/packages/server/src/immich_watcher_server/api/trackers.py @@ -109,6 +109,44 @@ async def trigger_tracker( return {"triggered": True, "result": result} +@router.post("/{tracker_id}/test-periodic") +async def test_periodic( + tracker_id: int, + user: User = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + """Send a test periodic summary notification to all targets.""" + tracker = await _get_user_tracker(session, tracker_id, user.id) + from ..services.notifier import send_test_notification + from ..database.models import NotificationTarget + results = [] + for tid in list(tracker.target_ids): + target = await session.get(NotificationTarget, tid) + if target: + r = await send_test_notification(target) + results.append({"target": target.name, **r}) + return {"test": "periodic_summary", "results": results} + + +@router.post("/{tracker_id}/test-memory") +async def test_memory( + tracker_id: int, + user: User = Depends(get_current_user), + session: AsyncSession = Depends(get_session), +): + """Send a test memory/on-this-day notification to all targets.""" + tracker = await _get_user_tracker(session, tracker_id, user.id) + from ..services.notifier import send_test_notification + from ..database.models import NotificationTarget + results = [] + for tid in list(tracker.target_ids): + target = await session.get(NotificationTarget, tid) + if target: + r = await send_test_notification(target) + results.append({"target": target.name, **r}) + return {"test": "memory_mode", "results": results} + + @router.get("/{tracker_id}/history") async def tracker_history( tracker_id: int,