From b170c2b7924c0a7c2163386bac1eabf82d248497 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Thu, 7 May 2026 22:34:24 +0300 Subject: [PATCH] feat(frontend): smoother event refresh, localized crumbs, template config deep-link MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Auto-refresh ticker is now silent: skips ``eventsLoading`` so the loading placeholder no longer flashes, uses ``(event.id)`` key on the events ``{#each}`` so unchanged rows reuse their DOM nodes, and short-circuits the array reassignment when the visible page is identical to what we already rendered. No-op refreshes leave the list completely untouched. - ``PageHeader`` crumbs (Routing · Notification, Operators · Bots, …) were hard-coded literals. Moved to a new ``crumbs`` i18n namespace with 9 keys; updated all 15 call sites to ``t('crumbs.*')`` so they switch with the language. - Tracker form's Immich feature-discovery banner now exposes both ``Open Tracking Config`` and ``Open Template Config``. Added the ``?edit=`` auto-open hook to ``/template-configs`` (mirrors the existing one on ``/tracking-configs``) so the new link lands users directly on the editor. --- frontend/src/lib/i18n/en.json | 12 ++++++ frontend/src/lib/i18n/ru.json | 12 ++++++ frontend/src/routes/+page.svelte | 43 ++++++++++++++++--- frontend/src/routes/actions/+page.svelte | 2 +- frontend/src/routes/bots/EmailBotTab.svelte | 2 +- frontend/src/routes/bots/MatrixBotTab.svelte | 2 +- .../src/routes/bots/TelegramBotTab.svelte | 2 +- .../src/routes/command-configs/+page.svelte | 2 +- .../command-template-configs/+page.svelte | 2 +- .../src/routes/command-trackers/+page.svelte | 2 +- .../routes/notification-trackers/+page.svelte | 2 +- .../notification-trackers/TrackerForm.svelte | 23 +++++++--- frontend/src/routes/providers/+page.svelte | 2 +- frontend/src/routes/settings/+page.svelte | 2 +- .../src/routes/settings/backup/+page.svelte | 2 +- frontend/src/routes/targets/+page.svelte | 2 +- .../src/routes/template-configs/+page.svelte | 22 +++++++++- .../src/routes/tracking-configs/+page.svelte | 2 +- frontend/src/routes/users/+page.svelte | 2 +- 19 files changed, 111 insertions(+), 29 deletions(-) diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json index 097af6a..6634386 100644 --- a/frontend/src/lib/i18n/en.json +++ b/frontend/src/lib/i18n/en.json @@ -3,6 +3,17 @@ "name": "Notify Bridge", "tagline": "Service notifications" }, + "crumbs": { + "routingNotification": "Routing · Notification", + "routingCommands": "Routing · Commands", + "routingTargets": "Routing · Targets", + "routingAutomation": "Routing · Automation", + "operatorsBots": "Operators · Bots", + "systemAccess": "System · Access", + "systemConfiguration": "System · Configuration", + "systemMaintenance": "System · Maintenance", + "serviceConnections": "Service · Connections" + }, "nav": { "sectionOverview": "Overview", "sectionRouting": "Routing", @@ -342,6 +353,7 @@ "checkingLinks": "Checking links...", "featureDiscovery": "Configure periodic summaries, scheduled photo picks, memories, and quiet hours in the default Tracking Config.", "openTrackingConfig": "Open Tracking Config", + "openTemplateConfig": "Open Template Config", "linkReplace": "Replace", "linkReplacing": "Replacing...", "linkReplaceFailed": "Failed to replace link for \"{name}\"", diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json index ed1d5c3..5390c94 100644 --- a/frontend/src/lib/i18n/ru.json +++ b/frontend/src/lib/i18n/ru.json @@ -3,6 +3,17 @@ "name": "Notify Bridge", "tagline": "Уведомления о сервисах" }, + "crumbs": { + "routingNotification": "Маршрутизация · Уведомления", + "routingCommands": "Маршрутизация · Команды", + "routingTargets": "Маршрутизация · Цели", + "routingAutomation": "Маршрутизация · Автоматизация", + "operatorsBots": "Операторы · Боты", + "systemAccess": "Система · Доступ", + "systemConfiguration": "Система · Настройки", + "systemMaintenance": "Система · Обслуживание", + "serviceConnections": "Сервис · Подключения" + }, "nav": { "sectionOverview": "Обзор", "sectionRouting": "Маршрутизация", @@ -342,6 +353,7 @@ "checkingLinks": "Проверка ссылок...", "featureDiscovery": "Периодические сводки, запланированные подборки, воспоминания и тихие часы настраиваются в привязанной конфигурации отслеживания.", "openTrackingConfig": "Открыть конфигурацию отслеживания", + "openTemplateConfig": "Открыть конфигурацию шаблона", "linkReplace": "Пересоздать", "linkReplacing": "Пересоздание...", "linkReplaceFailed": "Не удалось пересоздать ссылку для «{name}»", diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 7fc8fe4..0acd48c 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -108,7 +108,7 @@ // visibility flip via ``visibilitychange`` below. const tick = () => { if (typeof document !== 'undefined' && document.hidden) return; - loadEvents(); + loadEvents({ silent: true }); loadChart(); }; const handle = setInterval(tick, refreshSeconds * 1000); @@ -163,22 +163,53 @@ return params; } - async function loadEvents() { - eventsLoading = true; + /** Reload the events panel. + * + * ``silent`` is set by the auto-refresh ticker so the loading + * placeholder doesn't flash and the row list isn't disturbed when + * nothing actually changed. We diff the new payload against the + * current ``status`` and reuse the existing ``recent_events`` array + * reference when the ID list is identical — that lets Svelte's keyed + * ``{#each}`` skip its diff entirely instead of patching every row. + */ + async function loadEvents(opts: { silent?: boolean } = {}) { + if (!opts.silent) eventsLoading = true; try { const params = buildFilterParams(); params.set('sort', filterSort); params.set('limit', String(eventsLimit)); params.set('offset', String(eventsOffset)); const qs = params.toString(); - status = await api(`/status${qs ? '?' + qs : ''}`); + const next = await api(`/status${qs ? '?' + qs : ''}`); + + if (opts.silent && status && _sameEventIds(status.recent_events, next.recent_events)) { + // Nothing changed in the visible page. Update only the + // out-of-band counts so the header and pager stay accurate; + // keep the existing array reference so no row re-renders. + status = { + ...status, + providers: next.providers, + trackers: next.trackers, + targets: next.targets, + total_events: next.total_events, + command_trackers: next.command_trackers, + }; + return; + } + status = next; } catch (err: unknown) { error = err instanceof Error ? err.message : t('common.error'); } finally { - eventsLoading = false; + if (!opts.silent) eventsLoading = false; } } + function _sameEventIds(a: { id: number }[], b: { id: number }[]): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i].id !== b[i].id) return false; + return true; + } + async function loadChart() { try { const params = buildFilterParams(); @@ -653,7 +684,7 @@ {:else}
- {#each status.recent_events as event, i} + {#each status.recent_events as event, i (event.id)}
{/if} diff --git a/frontend/src/routes/providers/+page.svelte b/frontend/src/routes/providers/+page.svelte index 3c61c03..ddc24e9 100644 --- a/frontend/src/routes/providers/+page.svelte +++ b/frontend/src/routes/providers/+page.svelte @@ -198,7 +198,7 @@ title={t('providers.title')} emphasis={t('providers.titleEmphasis')} description={t('providers.description')} - crumb="Service · Connections" + crumb={t('crumbs.serviceConnections')} count={providers.length} countLabel={t('dashboard.providersShort')} pills={headerPills} diff --git a/frontend/src/routes/settings/+page.svelte b/frontend/src/routes/settings/+page.svelte index a07b59a..62a5369 100644 --- a/frontend/src/routes/settings/+page.svelte +++ b/frontend/src/routes/settings/+page.svelte @@ -101,7 +101,7 @@ title={t('settings.title')} emphasis={t('settings.titleEmphasis')} description={t('settings.description')} - crumb="System · Configuration" + crumb={t('crumbs.systemConfiguration')} /> {#if !loaded} diff --git a/frontend/src/routes/settings/backup/+page.svelte b/frontend/src/routes/settings/backup/+page.svelte index 320fe68..952deca 100644 --- a/frontend/src/routes/settings/backup/+page.svelte +++ b/frontend/src/routes/settings/backup/+page.svelte @@ -296,7 +296,7 @@ title={t('backup.title')} emphasis={t('backup.titleEmphasis')} description={t('backup.description')} - crumb="System · Maintenance" + crumb={t('crumbs.systemMaintenance')} /> {#if !loaded} diff --git a/frontend/src/routes/targets/+page.svelte b/frontend/src/routes/targets/+page.svelte index c3f366e..7523ee2 100644 --- a/frontend/src/routes/targets/+page.svelte +++ b/frontend/src/routes/targets/+page.svelte @@ -453,7 +453,7 @@ title={activeType ? activeType.charAt(0).toUpperCase() + activeType.slice(1) : t('targets.title')} emphasis={activeType ? t('targets.titleEmphasis') : t('targets.titleEmphasisAll')} description={activeType ? t(TYPE_DESC_KEYS[activeType]) : t('targets.description')} - crumb="Routing · Targets" + crumb={t('crumbs.routingTargets')} count={targets.length} countLabel={t('dashboard.targetsShort')} pills={headerPills} diff --git a/frontend/src/routes/template-configs/+page.svelte b/frontend/src/routes/template-configs/+page.svelte index 651f9f2..b7af313 100644 --- a/frontend/src/routes/template-configs/+page.svelte +++ b/frontend/src/routes/template-configs/+page.svelte @@ -261,7 +261,25 @@ supportedLocalesCache.fetch(), ]); } catch (err: any) { error = err.message || t('common.loadError'); snackError(error); } - finally { loaded = true; highlightFromUrl(); handleDeepLink(); } + finally { loaded = true; highlightFromUrl(); _openEditFromUrl(); handleDeepLink(); } + } + + // Cross-page deep-link: ``/template-configs?edit=`` auto-opens that + // config in edit mode. Mirrors the same hook on tracking-configs so the + // Notification Tracker form can link directly to the editor instead of + // the generic list. Strips the param afterwards so a browser refresh + // doesn't re-open the modal. + function _openEditFromUrl() { + if (typeof window === 'undefined') return; + const params = new URLSearchParams(window.location.search); + const editId = params.get('edit'); + if (!editId) return; + const match = allTemplateConfigs.find(c => String(c.id) === editId); + if (match) edit(match); + params.delete('edit'); + const qs = params.toString(); + const cleanUrl = window.location.pathname + (qs ? '?' + qs : ''); + window.history.replaceState(null, '', cleanUrl); } /** @@ -429,7 +447,7 @@ title={t('templateConfig.title')} emphasis={t('templateConfig.titleEmphasis')} description={t('templateConfig.description')} - crumb="Routing · Notification" + crumb={t('crumbs.routingNotification')} count={configs.length} countLabel={t('templateConfig.countLabel')} pills={headerPills} diff --git a/frontend/src/routes/tracking-configs/+page.svelte b/frontend/src/routes/tracking-configs/+page.svelte index 6cabb1d..cfcab6e 100644 --- a/frontend/src/routes/tracking-configs/+page.svelte +++ b/frontend/src/routes/tracking-configs/+page.svelte @@ -276,7 +276,7 @@ title={t('trackingConfig.title')} emphasis={t('trackingConfig.titleEmphasis')} description={t('trackingConfig.description')} - crumb="Routing · Notification" + crumb={t('crumbs.routingNotification')} count={configs.length} countLabel={t('trackingConfig.countLabel')} pills={headerPills} diff --git a/frontend/src/routes/users/+page.svelte b/frontend/src/routes/users/+page.svelte index 0e89168..582c9f1 100644 --- a/frontend/src/routes/users/+page.svelte +++ b/frontend/src/routes/users/+page.svelte @@ -93,7 +93,7 @@ title={t('users.title')} emphasis={t('users.titleEmphasis')} description={t('users.description')} - crumb="System · Access" + crumb={t('crumbs.systemAccess')} count={users.length} countLabel={t('users.countLabel')} >