diff --git a/frontend/src/lib/components/NavIcon.svelte b/frontend/src/lib/components/NavIcon.svelte
new file mode 100644
index 0000000..a485d2f
--- /dev/null
+++ b/frontend/src/lib/components/NavIcon.svelte
@@ -0,0 +1,92 @@
+
+
+{#if name === 'mdiViewDashboard'}
+
+{:else if name === 'mdiServer'}
+
+{:else if name === 'mdiBellOutline' || name === 'mdiBell'}
+
+{:else if name === 'mdiConsoleLine'}
+
+{:else if name === 'mdiRobotOutline' || name === 'mdiRobot'}
+
+{:else if name === 'mdiTarget'}
+
+{:else if name === 'mdiCogOutline' || name === 'mdiCog'}
+
+{:else if name === 'mdiRadar'}
+
+{:else if name === 'mdiFileDocumentEdit'}
+
+{:else if name === 'mdiCodeBracesBox'}
+
+{:else if name === 'mdiPlayCircleOutline'}
+
+{:else if name === 'mdiSendCircle' || name === 'mdiSend'}
+
+{:else if name === 'mdiEmailOutline'}
+
+{:else if name === 'mdiMatrix'}
+
+{:else if name === 'mdiWebhook'}
+
+{:else if name === 'mdiChat'}
+
+{:else if name === 'mdiSlack'}
+
+{:else if name === 'mdiBullhorn'}
+
+{:else if name === 'mdiBackupRestore'}
+
+{:else if name === 'mdiAccountGroup'}
+
+{:else if name === 'mdiChevronRight'}
+
+{:else if name === 'mdiChevronLeft'}
+
+{:else if name === 'mdiChevronDown'}
+
+{:else if name === 'mdiMagnify'}
+
+{:else if name === 'mdiLogout'}
+
+{:else if name === 'mdiKeyVariant'}
+
+{:else if name === 'mdiApi'}
+
+{:else if name === 'mdiWeatherNight'}
+
+{:else if name === 'mdiWeatherSunny'}
+
+{:else if name === 'mdiDesktopTowerMonitor'}
+
+{:else if name === 'mdiFilterOff'}
+
+{:else if name === 'mdiDotsHorizontal'}
+
+{:else if name === 'mdiPulse'}
+
+{:else if name === 'mdiPlus'}
+
+{:else if name === 'mdiArrowRight'}
+
+{:else}
+
+{/if}
diff --git a/frontend/src/lib/i18n/en.json b/frontend/src/lib/i18n/en.json
index 346722d..9288386 100644
--- a/frontend/src/lib/i18n/en.json
+++ b/frontend/src/lib/i18n/en.json
@@ -103,7 +103,39 @@
"last14days": "Last 14 days",
"event": "event",
"events": "events",
- "noChartData": "No event data yet"
+ "noChartData": "No event data yet",
+ "live": "Live",
+ "attention": "Attention",
+ "heroPrefix": "Tonight,",
+ "heroEmphasis": "everything",
+ "heroSuffix": "is flowing.",
+ "heroSummary": "{providers} providers listening, {armed} of {total} trackers armed, {throughput} events dispatched across {targets} targets in 24h.",
+ "throughput24h": "throughput · 24h",
+ "eventsShort": "events",
+ "armedShort": "armed",
+ "providersShort": "providers",
+ "targetsShort": "targets",
+ "trackersShort": "trackers",
+ "streamTitle": "Signal",
+ "streamEmphasis": "stream",
+ "eventsLabel": "events",
+ "onWatchTitle": "On",
+ "onWatchEmphasis": "watch",
+ "noProviders": "No providers yet.",
+ "addProvider": "Add provider",
+ "addProviderHint": "Connect a service to start tracking",
+ "pulseTitle": "Pulse",
+ "pulseEmphasis": "· last 14 days",
+ "pulseSub": "Events grouped by day",
+ "wiresTitle": "Active",
+ "wiresEmphasis": "wires",
+ "wiresSub": "routes",
+ "composeTitle": "Pick a source. Choose a channel.",
+ "composeEmphasis": "Compose the wire.",
+ "composeSub": "Walk from provider → tracker → template → target. Or paste a webhook URL and we'll infer the rest.",
+ "viewTrackers": "View trackers",
+ "newTracker": "New tracker",
+ "eventsTotal": "Events"
},
"providers": {
"title": "Providers",
diff --git a/frontend/src/lib/i18n/ru.json b/frontend/src/lib/i18n/ru.json
index b5b8b67..bbaca58 100644
--- a/frontend/src/lib/i18n/ru.json
+++ b/frontend/src/lib/i18n/ru.json
@@ -103,7 +103,39 @@
"last14days": "Последние 14 дней",
"event": "событие",
"events": "событий",
- "noChartData": "Нет данных о событиях"
+ "noChartData": "Нет данных о событиях",
+ "live": "В эфире",
+ "attention": "Внимание",
+ "heroPrefix": "Сегодня",
+ "heroEmphasis": "всё",
+ "heroSuffix": "идёт по плану.",
+ "heroSummary": "{providers} провайдеров на связи, {armed} из {total} трекеров активны, {throughput} событий доставлено в {targets} каналов за сутки.",
+ "throughput24h": "пропускная способность · 24ч",
+ "eventsShort": "событий",
+ "armedShort": "активны",
+ "providersShort": "провайдеров",
+ "targetsShort": "каналов",
+ "trackersShort": "трекеров",
+ "streamTitle": "Поток",
+ "streamEmphasis": "сигналов",
+ "eventsLabel": "событий",
+ "onWatchTitle": "На",
+ "onWatchEmphasis": "слежении",
+ "noProviders": "Пока нет провайдеров.",
+ "addProvider": "Добавить",
+ "addProviderHint": "Подключите сервис, чтобы начать слежение",
+ "pulseTitle": "Пульс",
+ "pulseEmphasis": "· 14 дней",
+ "pulseSub": "События по дням",
+ "wiresTitle": "Активные",
+ "wiresEmphasis": "линии",
+ "wiresSub": "маршрутов",
+ "composeTitle": "Выберите источник, выберите канал.",
+ "composeEmphasis": "Свяжите.",
+ "composeSub": "Проведите путь от провайдера → трекер → шаблон → цель. Или вставьте webhook URL — остальное мы определим сами.",
+ "viewTrackers": "К трекерам",
+ "newTracker": "Новый трекер",
+ "eventsTotal": "Событий"
},
"providers": {
"title": "Провайдеры",
diff --git a/frontend/src/routes/+layout.svelte b/frontend/src/routes/+layout.svelte
index 61b2e06..c904781 100644
--- a/frontend/src/routes/+layout.svelte
+++ b/frontend/src/routes/+layout.svelte
@@ -10,7 +10,7 @@
import { t, getLocale, setLocale } from '$lib/i18n';
import { getTheme, initTheme, setTheme, type Theme } from '$lib/theme.svelte';
import Modal from '$lib/components/Modal.svelte';
- import MdiIcon from '$lib/components/MdiIcon.svelte';
+ import NavIcon from '$lib/components/NavIcon.svelte';
import Snackbar from '$lib/components/Snackbar.svelte';
import SearchPalette from '$lib/components/SearchPalette.svelte';
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
@@ -359,7 +359,7 @@
{#if globalProviderFilter.provider}
-
+
{/if}
Notify Bridge
@@ -372,7 +372,7 @@
@@ -387,7 +387,7 @@
}}
class="provider-filter-btn flex items-center justify-center w-full py-1.5 rounded-lg text-sm transition-all duration-200"
title={globalProviderFilter.provider?.name || t('common.allProviders')}>
-
+
{:else}
@@ -400,7 +400,7 @@
@@ -440,7 +440,7 @@
{#if isActive(child.href)}
{/if}
-
+
{t(child.key)}
{#if child.countKey && navCounts[child.countKey]}
{navCounts[child.countKey]}
@@ -459,7 +459,7 @@
{#if isActive(entry.href)}
{/if}
-
+
{#if !collapsed}
{t(entry.key)}
{#if entry.countKey && navCounts[entry.countKey]}
@@ -483,12 +483,12 @@
@@ -498,7 +498,7 @@
{:else}
@@ -515,12 +515,12 @@
@@ -535,18 +535,18 @@
-
+
{/each}
@@ -567,7 +567,7 @@
-
+
{t(entry.key)}
@@ -576,7 +576,7 @@
class="flex flex-col items-center gap-1 p-3 rounded-lg transition-all duration-200 relative"
style="color: {isActive(child.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(child.href) ? 'var(--color-sidebar-active)' : 'transparent'};"
>
-
+
{t(child.key)}
{#if child.countKey && navCounts[child.countKey]}
{navCounts[child.countKey]}
@@ -590,7 +590,7 @@
class="flex items-center gap-2 p-3 rounded-lg transition-all duration-200 relative"
style="color: {isActive(entry.href) ? 'var(--color-primary)' : 'var(--color-muted-foreground)'}; background: {isActive(entry.href) ? 'var(--color-sidebar-active)' : 'transparent'};"
>
-
+
{t(entry.key)}
{#if entry.countKey && navCounts[entry.countKey]}
{navCounts[entry.countKey]}
@@ -602,7 +602,7 @@
@@ -614,7 +614,7 @@
{#key page.url.pathname}
-
+
{@render children()}
{/key}
diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte
index 389cd8e..f3e5ede 100644
--- a/frontend/src/routes/+page.svelte
+++ b/frontend/src/routes/+page.svelte
@@ -3,11 +3,14 @@
import { slide } from 'svelte/transition';
import { api, parseDate } from '$lib/api';
import { t } from '$lib/i18n';
- import { providersCache } from '$lib/stores/caches.svelte';
- import PageHeader from '$lib/components/PageHeader.svelte';
- import Card from '$lib/components/Card.svelte';
+ import {
+ providersCache,
+ notificationTrackersCache,
+ targetsCache,
+ } from '$lib/stores/caches.svelte';
import Loading from '$lib/components/Loading.svelte';
import MdiIcon from '$lib/components/MdiIcon.svelte';
+ import NavIcon from '$lib/components/NavIcon.svelte';
import EventChart from '$lib/components/EventChart.svelte';
import IconGridSelect from '$lib/components/IconGridSelect.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
@@ -39,6 +42,7 @@
let displayActive = $state(0);
let displayTotal = $state(0);
let displayTargets = $state(0);
+ let displayCommandTrackers = $state(0);
// Event filters
let filterEventType = $state('');
@@ -66,8 +70,8 @@
eventsOffset = 0;
await loadEvents();
await loadChart();
- } catch (err: any) {
- snackError(err.message || t('common.error'));
+ } catch (err: unknown) {
+ snackError(err instanceof Error ? err.message : t('common.error'));
} finally {
confirmClearEvents = false;
}
@@ -107,8 +111,8 @@
params.set('offset', String(eventsOffset));
const qs = params.toString();
status = await api
(`/status${qs ? '?' + qs : ''}`);
- } catch (err: any) {
- error = err.message || t('common.error');
+ } catch (err: unknown) {
+ error = err instanceof Error ? err.message : t('common.error');
} finally {
eventsLoading = false;
}
@@ -176,35 +180,104 @@
animateCount(0, status.command_trackers, (v) => displayCommandTrackers = v);
}
}, 200);
- } catch (err: any) {
- error = err.message || t('common.error');
+ } catch (err: unknown) {
+ error = err instanceof Error ? err.message : t('common.error');
} finally {
loaded = true;
}
}
- let displayCommandTrackers = $state(0);
-
const filteredProviderCount = $derived(globalProviderFilter.providerType
? providers.filter(p => p.type === globalProviderFilter.providerType).length
: displayProviders);
- const providerCard = $derived.by(() => {
- const gp = globalProviderFilter.provider;
- if (gp) {
- const desc = getDescriptor(gp.type);
- return { icon: providerDefaultIcon(gp), label: '', literalLabel: desc?.defaultName ?? gp.type, value: 0, literalValue: gp.name, color: '#0d9488' };
+ // === Provider deck — derive activity counts from recent events ===
+ const providerEventCounts = $derived.by(() => {
+ const counts = new Map();
+ if (!status) return counts;
+ for (const ev of status.recent_events) {
+ const k = ev.provider_name || '';
+ if (!k) continue;
+ counts.set(k, (counts.get(k) || 0) + (ev.assets_count || 1));
}
- return { icon: 'mdiServer', label: 'dashboard.providers', value: filteredProviderCount, color: '#0d9488' };
+ return counts;
+ });
+ const providerDeck = $derived.by(() => {
+ const max = Math.max(1, ...Array.from(providerEventCounts.values()));
+ return providers.map(p => {
+ const trackers = notificationTrackersCache.items.filter(t => t.provider_id === p.id);
+ const events = providerEventCounts.get(p.name) || 0;
+ return {
+ id: p.id,
+ name: p.name,
+ type: p.type,
+ icon: providerDefaultIcon(p),
+ trackerCount: trackers.length,
+ armedCount: trackers.filter(t => (t as { enabled?: boolean }).enabled !== false).length,
+ events,
+ share: events / max,
+ descriptor: getDescriptor(p.type),
+ };
+ }).sort((a, b) => b.events - a.events);
});
- interface StatCard { icon: string; label: string; literalLabel?: string; value: number; literalValue?: string; suffix?: string; color: string }
- const statCards = $derived(status ? [
- providerCard,
- { icon: 'mdiRadar', label: 'dashboard.activeTrackers', value: displayActive, suffix: ` / ${displayTotal}`, color: '#6366f1' },
- { icon: 'mdiTarget', label: 'dashboard.targets', value: displayTargets, color: '#f59e0b' },
- ...(status.command_trackers !== undefined ? [{ icon: 'mdiConsoleLine', label: 'nav.commandTrackers', value: displayCommandTrackers, color: '#8b5cf6' }] : []),
- ] : []);
+ // === Active wires — derive top tracker → target routes ===
+ const activeWires = $derived.by(() => {
+ const wires: Array<{
+ trackerName: string;
+ providerName: string;
+ providerType: string;
+ providerIcon: string;
+ targetName: string;
+ targetType: string;
+ targetIcon: string;
+ events: number;
+ }> = [];
+ const targetsById = new Map(targetsCache.items.map(tg => [tg.id, tg]));
+ const trackerEventCount = new Map();
+ if (status) {
+ for (const ev of status.recent_events) {
+ const k = ev.tracker_name || '';
+ if (!k) continue;
+ trackerEventCount.set(k, (trackerEventCount.get(k) || 0) + (ev.assets_count || 1));
+ }
+ }
+ for (const tracker of notificationTrackersCache.items) {
+ const provider = providers.find(p => p.id === tracker.provider_id);
+ if (!provider) continue;
+ const links = (tracker as { tracker_targets?: { target_id: number }[] }).tracker_targets || [];
+ for (const link of links) {
+ const target = targetsById.get(link.target_id);
+ if (!target) continue;
+ wires.push({
+ trackerName: tracker.name,
+ providerName: provider.name,
+ providerType: provider.type,
+ providerIcon: providerDefaultIcon(provider),
+ targetName: target.name,
+ targetType: target.type,
+ targetIcon: target.icon || `mdi${target.type[0].toUpperCase() + target.type.slice(1)}`,
+ events: trackerEventCount.get(tracker.name) || 0,
+ });
+ }
+ }
+ return wires.sort((a, b) => b.events - a.events).slice(0, 6);
+ });
+
+ // === Hero summary sentence === */
+ const heroSummary = $derived.by(() => {
+ if (!status) return null;
+ const failing = status.recent_events.filter(e => /fail|error/.test(e.event_type)).length;
+ return {
+ armed: status.trackers.active,
+ total: status.trackers.total,
+ providers: status.providers,
+ targets: status.targets,
+ throughput: status.total_events,
+ failing,
+ allOk: failing === 0,
+ };
+ });
function timeAgo(dateStr: string): string {
const diff = Date.now() - parseDate(dateStr).getTime();
@@ -216,6 +289,11 @@
return t('dashboard.daysAgo').replace('{n}', String(Math.floor(hours / 24)));
}
+ function timeShort(dateStr: string): string {
+ const d = parseDate(dateStr);
+ return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
+ }
+
const eventLabels: Record = {
assets_added: 'dashboard.assetsAdded',
assets_removed: 'dashboard.assetsRemoved',
@@ -234,181 +312,523 @@
scheduled_message: 'mdiCalendarClock',
action_success: 'mdiPlayCircle', action_partial: 'mdiAlertCircle', action_failed: 'mdiCloseCircle',
};
- const eventColors: Record = {
- assets_added: '#059669', assets_removed: '#ef4444',
- collection_renamed: '#6366f1', collection_deleted: '#dc2626', sharing_changed: '#f59e0b',
- scheduled_message: '#8b5cf6',
- action_success: '#0d9488', action_partial: '#f59e0b', action_failed: '#dc2626',
+
+ // Aurora gradient palette per event type — used for the avatar tile
+ const eventGradients: Record = {
+ assets_added: ['var(--color-mint)', 'var(--color-sky)'],
+ assets_removed: ['var(--color-coral)', 'var(--color-orchid)'],
+ collection_renamed: ['var(--color-primary)', 'var(--color-orchid)'],
+ collection_deleted: ['var(--color-coral)', 'var(--color-citrus)'],
+ sharing_changed: ['var(--color-citrus)', 'var(--color-coral)'],
+ scheduled_message: ['var(--color-sky)', 'var(--color-mint)'],
+ action_success: ['var(--color-mint)', 'var(--color-primary)'],
+ action_partial: ['var(--color-citrus)', 'var(--color-orchid)'],
+ action_failed: ['var(--color-coral)', 'var(--color-orchid)'],
};
+ const STAT_ACCENTS = [
+ 'var(--color-primary)',
+ 'var(--color-sky)',
+ 'var(--color-citrus)',
+ 'var(--color-orchid)',
+ ];
-
-
{#if !loaded}
{:else if error}
-
+
-
-{:else if status}
-
- {#each statCards as card, i}
-
-
-
-
-
-
-
-
{card.literalLabel || t(card.label)}
-
- {#if card.literalValue}{card.literalValue}{:else}{card.value}{/if}{#if card.suffix}{card.suffix}{/if}
-
-
+
+{:else if status && heroSummary}
+
+
+
+
+
+
+ {t('dashboard.title')}
+ ·
+
+
+ {heroSummary.allOk ? t('dashboard.live') : t('dashboard.attention')}
+
+
+
+ {t('dashboard.heroPrefix')} {t('dashboard.heroEmphasis')}
+ {t('dashboard.heroSuffix')}
+
+
+ {t('dashboard.heroSummary')
+ .replace('{providers}', String(heroSummary.providers))
+ .replace('{armed}', String(heroSummary.armed))
+ .replace('{total}', String(heroSummary.total))
+ .replace('{throughput}', String(heroSummary.throughput))
+ .replace('{targets}', String(heroSummary.targets))}
+
+
+
+
{t('dashboard.throughput24h')}
+
{heroSummary.throughput.toLocaleString()}{t('dashboard.eventsShort')}
+
+ {heroSummary.armed}/{heroSummary.total} {t('dashboard.armedShort')}
+ {heroSummary.providers} {t('dashboard.providersShort')}
+ {heroSummary.targets} {t('dashboard.targetsShort')}
+
+
+
+
+
+
+ {#snippet statCardSnippet(card: {icon: string; label: string; literalLabel?: string; value: number; literalValue?: string; suffix?: string; accent: string}, idx: number)}
+
+
+
+
+
+
+
+
{card.literalLabel || t(card.label)}
+
+ {#if card.literalValue}{card.literalValue}{:else}{card.value}{/if}{#if card.suffix}{card.suffix}{/if}
+
- {/each}
-
-
-
-
-
-
- {t('dashboard.recentEvents')}
- {#if status.total_events > 0}
- ({status.total_events})
- {/if}
-
- {#if status.total_events > 0}
-
- {/if}
-
-
-
-
-
-
-
-
- {#if !globalProviderFilter.id}
{/if}
-
-
-
-
-
- {#if chartVisible}
-
-
-
- {/if}
-
- {#snippet paginator()}
-
- {#if totalPages > 1}
-
- {#each Array.from({ length: totalPages }, (_, i) => i + 1) as page}
- {#if page === 1 || page === totalPages || (page >= currentPage - 1 && page <= currentPage + 1)}
-
- {:else if page === currentPage - 2 || page === currentPage + 2}
- ...
- {/if}
- {/each}
-
- {/if}
-
{/snippet}
- {#if eventsLoading}
-
{t('dashboard.loadingEvents')}
- {:else if status.recent_events.length === 0}
-
-
-
-
{t('dashboard.noEvents')}
-
-
- {:else}
-
- {#each status.recent_events as event, i}
-
-
- {#if i < status.recent_events.length - 1}
{/if}
-
-
-
-
-
-
- {event.collection_name}
- {t(eventLabels[event.event_type] || event.event_type)}
- {#if event.assets_count > 0}
- {event.assets_count} {event.assets_count === 1 ? t('dashboard.asset') : t('dashboard.assets')}
- {/if}
-
-
{timeAgo(event.created_at)}
-
- {#if event.provider_name || event.tracker_name}
-
- {#if event.provider_name}
- {event.provider_name}
- {/if}
- {#if event.tracker_name}
- {event.tracker_name}
- {/if}
-
- {/if}
-
-
- {/each}
+ {#snippet statCards()}
+
+ {#if globalProviderFilter.provider}
+ {@render statCardSnippet({
+ icon: providerDefaultIcon(globalProviderFilter.provider),
+ label: '',
+ literalLabel: getDescriptor(globalProviderFilter.provider.type)?.defaultName ?? globalProviderFilter.provider.type,
+ value: 0,
+ literalValue: globalProviderFilter.provider.name,
+ accent: STAT_ACCENTS[0],
+ }, 0)}
+ {:else}
+ {@render statCardSnippet({
+ icon: 'mdiServer',
+ label: 'dashboard.providers',
+ value: filteredProviderCount,
+ accent: STAT_ACCENTS[0],
+ }, 0)}
+ {/if}
+ {@render statCardSnippet({
+ icon: 'mdiRadar',
+ label: 'dashboard.activeTrackers',
+ value: displayActive,
+ suffix: ` / ${displayTotal}`,
+ accent: STAT_ACCENTS[1],
+ }, 1)}
+ {@render statCardSnippet({
+ icon: 'mdiTarget',
+ label: 'dashboard.targets',
+ value: displayTargets,
+ accent: STAT_ACCENTS[2],
+ }, 2)}
+ {#if status?.command_trackers !== undefined}
+ {@render statCardSnippet({
+ icon: 'mdiConsoleLine',
+ label: 'nav.commandTrackers',
+ value: displayCommandTrackers,
+ accent: STAT_ACCENTS[3],
+ }, 3)}
+ {:else}
+ {@render statCardSnippet({
+ icon: 'mdiPulse',
+ label: 'dashboard.eventsTotal',
+ value: heroSummary?.throughput ?? 0,
+ accent: STAT_ACCENTS[3],
+ }, 3)}
+ {/if}
+ {/snippet}
+ {@render statCards()}
-
-
- {@render paginator()}
-
+
+
+
+
+
+
+
+
+
+
+
+ {#if !globalProviderFilter.id}
{/if}
+
+
+
+ {#snippet paginator()}
+
+ {#if totalPages > 1}
+
+ {#each Array.from({ length: totalPages }, (_, i) => i + 1) as p}
+ {#if p === 1 || p === totalPages || (p >= currentPage - 1 && p <= currentPage + 1)}
+
+ {:else if p === currentPage - 2 || p === currentPage + 2}
+ …
+ {/if}
+ {/each}
+
+ {/if}
+
+
+ {/snippet}
+
+ {#if eventsLoading}
+ {t('dashboard.loadingEvents')}
+ {:else if status.recent_events.length === 0}
+
+
+
{t('dashboard.noEvents')}
+
+ {:else}
+
+ {#each status.recent_events as event, i}
+
+
+
+
+
+
+ {event.collection_name}
+ {t(eventLabels[event.event_type] || event.event_type)}
+ {#if event.assets_count > 0}
+ {event.assets_count} {event.assets_count === 1 ? t('dashboard.asset') : t('dashboard.assets')}
+ {/if}
+
+ {#if event.provider_name || event.tracker_name}
+
+ {#if event.tracker_name}
+ {event.tracker_name}
+ →
+ {/if}
+ {#if event.provider_name}
+ {event.provider_name}
+ {/if}
+
+ {/if}
+
+
+ {timeShort(event.created_at)}
+ {timeAgo(event.created_at)}
+
+
+ {/each}
+
+
+
+ {/if}
+
+
+
+
+
+
+ {#if providerDeck.length === 0}
+
+ {:else}
+
+ {/if}
+
+
+
+
+
+
+ {#if chartVisible}
+
+
+
+ {/if}
+
+
+
+ {#if activeWires.length > 0}
+
+
+
+ {#each activeWires as wire}
+
+
+
+
+
{wire.trackerName}
+
{wire.providerName}
+
+
+
+ {wire.events > 0 ? wire.events : '—'}
+
+
+
+
{wire.targetName}
+
{wire.targetType}
+
+
+
+
+ {/each}
+
+
{/if}
+
+
+
+
+
+ {t('dashboard.composeTitle')} {t('dashboard.composeEmphasis')}
+
+
{t('dashboard.composeSub')}
+
+
+
{/if}
confirmClearEvents = false} />