feat(frontend): smoother event refresh, localized crumbs, template config deep-link
- 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=<id>`` auto-open hook to ``/template-configs`` (mirrors the
existing one on ``/tracking-configs``) so the new link lands users
directly on the editor.
This commit is contained in:
@@ -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}\"",
|
||||
|
||||
@@ -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}»",
|
||||
|
||||
@@ -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<DashboardStatus>(`/status${qs ? '?' + qs : ''}`);
|
||||
const next = await api<DashboardStatus>(`/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 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="signal-list stagger-children">
|
||||
{#each status.recent_events as event, i}
|
||||
{#each status.recent_events as event, i (event.id)}
|
||||
<button type="button" class="signal-row signal-row--clickable"
|
||||
style="animation-delay: {i * 60}ms;"
|
||||
onclick={() => selectedEvent = event}
|
||||
|
||||
@@ -199,7 +199,7 @@
|
||||
title={t('actions.title')}
|
||||
emphasis={t('actions.titleEmphasis')}
|
||||
description={t('actions.description')}
|
||||
crumb="Routing · Automation"
|
||||
crumb={t('crumbs.routingAutomation')}
|
||||
count={actions.length}
|
||||
countLabel={t('actions.countLabel')}
|
||||
pills={headerPills}
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
title={t('emailBot.title')}
|
||||
emphasis={t('emailBot.titleEmphasis')}
|
||||
description={t('emailBot.description')}
|
||||
crumb="Operators · Bots"
|
||||
crumb={t('crumbs.operatorsBots')}
|
||||
count={emailBots.length}
|
||||
countLabel={t('emailBot.countLabel')}
|
||||
>
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
title={t('matrixBot.title')}
|
||||
emphasis={t('matrixBot.titleEmphasis')}
|
||||
description={t('matrixBot.description')}
|
||||
crumb="Operators · Bots"
|
||||
crumb={t('crumbs.operatorsBots')}
|
||||
count={matrixBots.length}
|
||||
countLabel={t('matrixBot.countLabel')}
|
||||
>
|
||||
|
||||
@@ -303,7 +303,7 @@
|
||||
title={t('telegramBot.title')}
|
||||
emphasis={t('telegramBot.titleEmphasis')}
|
||||
description={t('telegramBot.description')}
|
||||
crumb="Operators · Bots"
|
||||
crumb={t('crumbs.operatorsBots')}
|
||||
count={bots.length}
|
||||
countLabel={t('telegramBot.countLabel')}
|
||||
>
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
title={t('commandConfig.title')}
|
||||
emphasis={t('commandConfig.titleEmphasis')}
|
||||
description={t('commandConfig.description')}
|
||||
crumb="Routing · Commands"
|
||||
crumb={t('crumbs.routingCommands')}
|
||||
count={configs.length}
|
||||
countLabel={t('commandConfig.countLabel')}
|
||||
pills={headerPills}
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
title={t('cmdTemplateConfig.title')}
|
||||
emphasis={t('cmdTemplateConfig.titleEmphasis')}
|
||||
description={t('cmdTemplateConfig.description')}
|
||||
crumb="Routing · Commands"
|
||||
crumb={t('crumbs.routingCommands')}
|
||||
count={configs.length}
|
||||
countLabel={t('cmdTemplateConfig.countLabel')}
|
||||
pills={headerPills}
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
title={t('commandTracker.title')}
|
||||
emphasis={t('commandTracker.titleEmphasis')}
|
||||
description={t('commandTracker.description')}
|
||||
crumb="Routing · Commands"
|
||||
crumb={t('crumbs.routingCommands')}
|
||||
count={trackers.length}
|
||||
countLabel={t('dashboard.trackersShort')}
|
||||
pills={headerPills}
|
||||
|
||||
@@ -468,7 +468,7 @@
|
||||
title={t('notificationTracker.title')}
|
||||
emphasis={t('notificationTracker.titleEmphasis')}
|
||||
description={t('notificationTracker.description')}
|
||||
crumb="Routing · Notification"
|
||||
crumb={t('crumbs.routingNotification')}
|
||||
count={notificationTrackers.length}
|
||||
countLabel={t('dashboard.trackersShort')}
|
||||
pills={headerPills}
|
||||
|
||||
@@ -227,13 +227,22 @@
|
||||
<span style="color: var(--color-primary);"><MdiIcon name="mdiInformationOutline" size={16} /></span>
|
||||
<div class="flex-1 text-xs">
|
||||
<p style="color: var(--color-muted-foreground);">{t('notificationTracker.featureDiscovery')}</p>
|
||||
<a href={form.default_tracking_config_id
|
||||
? `/tracking-configs?edit=${form.default_tracking_config_id}`
|
||||
: '/tracking-configs'}
|
||||
class="inline-flex items-center gap-1 text-[var(--color-primary)] hover:underline mt-1">
|
||||
<MdiIcon name="mdiArrowRight" size={12} />
|
||||
{t('notificationTracker.openTrackingConfig')}
|
||||
</a>
|
||||
<div class="flex flex-wrap gap-x-4 gap-y-1 mt-1">
|
||||
<a href={form.default_tracking_config_id
|
||||
? `/tracking-configs?edit=${form.default_tracking_config_id}`
|
||||
: '/tracking-configs'}
|
||||
class="inline-flex items-center gap-1 text-[var(--color-primary)] hover:underline">
|
||||
<MdiIcon name="mdiArrowRight" size={12} />
|
||||
{t('notificationTracker.openTrackingConfig')}
|
||||
</a>
|
||||
<a href={form.default_template_config_id
|
||||
? `/template-configs?edit=${form.default_template_config_id}`
|
||||
: '/template-configs'}
|
||||
class="inline-flex items-center gap-1 text-[var(--color-primary)] hover:underline">
|
||||
<MdiIcon name="mdiArrowRight" size={12} />
|
||||
{t('notificationTracker.openTemplateConfig')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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=<id>`` 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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user