feat: test menu dropdown, split text/media messages, target settings, provider URL links
- Replace 3 test buttons with unified dropdown menu (basic/periodic/scheduled/memory) - Send text message first, then assets as reply (not combined caption+media) - Pass all target config settings to Telegram client (disable_url_preview, max_media, chunk_delay, etc.) - Real data test notifications for periodic/scheduled/memory (fetch from Immich) - Provider card URL is now a clickable hyperlink - Localized test type labels (EN/RU) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,7 +59,11 @@
|
||||
"oldestFirst": "Oldest first",
|
||||
"loadingEvents": "Loading events...",
|
||||
"asset": "asset",
|
||||
"assets": "assets"
|
||||
"assets": "assets",
|
||||
"eventActivity": "Event Activity",
|
||||
"last14days": "Last 14 days",
|
||||
"events": "events",
|
||||
"noChartData": "No event data yet"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Providers",
|
||||
@@ -126,7 +130,11 @@
|
||||
"batchDuration": "Batch duration (seconds)",
|
||||
"linkedTargets": "targets",
|
||||
"noLinkedTargets": "No targets linked. Add a target below.",
|
||||
"addTarget": "Add target"
|
||||
"addTarget": "Add target",
|
||||
"testBasic": "Send test message",
|
||||
"testPeriodic": "Test periodic summary",
|
||||
"testScheduled": "Test scheduled assets",
|
||||
"testMemory": "Test memory / On This Day"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Templates",
|
||||
|
||||
@@ -59,7 +59,11 @@
|
||||
"oldestFirst": "Сначала старые",
|
||||
"loadingEvents": "Загрузка событий...",
|
||||
"asset": "файл",
|
||||
"assets": "файлов"
|
||||
"assets": "файлов",
|
||||
"eventActivity": "Активность событий",
|
||||
"last14days": "Последние 14 дней",
|
||||
"events": "событий",
|
||||
"noChartData": "Нет данных о событиях"
|
||||
},
|
||||
"providers": {
|
||||
"title": "Провайдеры",
|
||||
@@ -126,7 +130,11 @@
|
||||
"batchDuration": "Длительность пакета (секунды)",
|
||||
"linkedTargets": "получатели",
|
||||
"noLinkedTargets": "Нет привязанных получателей. Добавьте получателя ниже.",
|
||||
"addTarget": "Добавить получателя"
|
||||
"addTarget": "Добавить получателя",
|
||||
"testBasic": "Отправить тестовое сообщение",
|
||||
"testPeriodic": "Тест периодической сводки",
|
||||
"testScheduled": "Тест запланированных фото",
|
||||
"testMemory": "Тест воспоминаний"
|
||||
},
|
||||
"templates": {
|
||||
"title": "Шаблоны",
|
||||
|
||||
@@ -6,9 +6,11 @@
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Loading from '$lib/components/Loading.svelte';
|
||||
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
||||
import EventChart from '$lib/components/EventChart.svelte';
|
||||
|
||||
let status = $state<any>(null);
|
||||
let providers = $state<any[]>([]);
|
||||
let chartDays = $state<any[]>([]);
|
||||
let loaded = $state(false);
|
||||
let error = $state('');
|
||||
|
||||
@@ -33,7 +35,7 @@
|
||||
function calcPageSize(): number {
|
||||
if (typeof window === 'undefined') return 8;
|
||||
const EVENT_ROW_HEIGHT = 50; // px per event row (content + gap)
|
||||
const FIXED_OVERHEAD = 390; // header + stats + events header + filters + paginator + padding
|
||||
const FIXED_OVERHEAD = 600; // header + stats + chart + events header + filters + paginator + padding
|
||||
const available = window.innerHeight - FIXED_OVERHEAD;
|
||||
return Math.max(3, Math.floor(available / EVENT_ROW_HEIGHT));
|
||||
}
|
||||
@@ -108,10 +110,14 @@
|
||||
|
||||
async function loadInitial() {
|
||||
try {
|
||||
[status, providers] = await Promise.all([
|
||||
const [statusRes, providersRes, chartRes] = await Promise.all([
|
||||
api<any>(`/status?limit=${eventsLimit}`),
|
||||
api<any[]>('/providers'),
|
||||
api<any>('/status/chart'),
|
||||
]);
|
||||
status = statusRes;
|
||||
providers = providersRes;
|
||||
chartDays = chartRes.days || [];
|
||||
setTimeout(() => {
|
||||
animateCount(0, status.providers, (v) => displayProviders = v);
|
||||
animateCount(0, status.trackers.active, (v) => displayActive = v);
|
||||
@@ -200,6 +206,8 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<EventChart days={chartDays} />
|
||||
|
||||
<h3 class="text-base font-semibold mb-3 flex items-center gap-2">
|
||||
<MdiIcon name="mdiPulse" size={18} />
|
||||
{t('dashboard.recentEvents')}
|
||||
|
||||
@@ -161,7 +161,9 @@
|
||||
<p class="font-medium">{provider.name}</p>
|
||||
<span class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{provider.type}</span>
|
||||
</div>
|
||||
<p class="text-xs text-[var(--color-muted-foreground)] font-mono">{provider.config?.url || ''}</p>
|
||||
{#if provider.config?.url}
|
||||
<a href={provider.config.url} target="_blank" rel="noopener" class="text-xs text-[var(--color-muted-foreground)] font-mono hover:text-[var(--color-primary)] hover:underline">{provider.config.url}</a>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
|
||||
@@ -176,13 +176,23 @@
|
||||
} catch (err: any) { error = err.message; snackError(err.message); }
|
||||
confirmDelete = null;
|
||||
}
|
||||
let testMenuOpen = $state<string | null>(null);
|
||||
let testMenuStyle = $state('');
|
||||
|
||||
const testTypes = [
|
||||
{ key: 'basic', icon: 'mdiSend', labelKey: 'trackers.testBasic' },
|
||||
{ key: 'periodic', icon: 'mdiCalendarClock', labelKey: 'trackers.testPeriodic' },
|
||||
{ key: 'scheduled', icon: 'mdiImageMultiple', labelKey: 'trackers.testScheduled' },
|
||||
{ key: 'memory', icon: 'mdiHistory', labelKey: 'trackers.testMemory' },
|
||||
];
|
||||
|
||||
async function testTrackerTarget(trackerId: number, ttId: number, testType: string) {
|
||||
testMenuOpen = null;
|
||||
const key = `${ttId}_${testType}`;
|
||||
if (ttTesting[key]) return;
|
||||
ttTesting = { ...ttTesting, [key]: testType };
|
||||
try {
|
||||
const endpoint = testType === 'basic' ? 'test' : `test-${testType}`;
|
||||
await api(`/trackers/${trackerId}/targets/${ttId}/${endpoint}?locale=${getLocale()}`, { method: 'POST' });
|
||||
await api(`/trackers/${trackerId}/targets/${ttId}/test/${testType}?locale=${getLocale()}`, { method: 'POST' });
|
||||
snackSuccess(t('snack.targetTestSent'));
|
||||
} catch (err: any) {
|
||||
snackError(err.message);
|
||||
@@ -190,6 +200,13 @@
|
||||
ttTesting = { ...ttTesting, [key]: '' };
|
||||
}
|
||||
}
|
||||
|
||||
function openTestMenu(ttId: number, event: MouseEvent) {
|
||||
const btn = event.currentTarget as HTMLElement;
|
||||
const rect = btn.getBoundingClientRect();
|
||||
testMenuStyle = `position:fixed; z-index:9999; top:${rect.bottom + 4}px; right:${window.innerWidth - rect.right}px;`;
|
||||
testMenuOpen = testMenuOpen === String(ttId) ? null : String(ttId);
|
||||
}
|
||||
function toggleCollection(collectionId: string) { form.collection_ids = form.collection_ids.includes(collectionId) ? form.collection_ids.filter(id => id !== collectionId) : [...form.collection_ids, collectionId]; }
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
@@ -390,15 +407,11 @@
|
||||
<option value={0}>— {t('templateConfig.title')} —</option>
|
||||
{#each templateConfigs as tc}<option value={tc.id}>{tc.name}</option>{/each}
|
||||
</select>
|
||||
<IconButton icon="mdiSend" size={14} title={t('common.test')}
|
||||
onclick={() => testTrackerTarget(tracker.id, tt.id, 'basic')}
|
||||
disabled={!!ttTesting[`${tt.id}_basic`]} />
|
||||
<IconButton icon="mdiCalendarClock" size={14} title={t('trackingConfig.periodicSummary')}
|
||||
onclick={() => testTrackerTarget(tracker.id, tt.id, 'periodic')}
|
||||
disabled={!!ttTesting[`${tt.id}_periodic`]} />
|
||||
<IconButton icon="mdiHistory" size={14} title={t('trackingConfig.memoryMode')}
|
||||
onclick={() => testTrackerTarget(tracker.id, tt.id, 'memory')}
|
||||
disabled={!!ttTesting[`${tt.id}_memory`]} />
|
||||
<div class="relative">
|
||||
<IconButton icon="mdiDotsVertical" size={14} title={t('common.test')}
|
||||
onclick={(e: MouseEvent) => openTestMenu(tt.id, e)}
|
||||
disabled={Object.keys(ttTesting).some(k => k.startsWith(`${tt.id}_`) && ttTesting[k])} />
|
||||
</div>
|
||||
<IconButton icon={tt.enabled ? 'mdiPause' : 'mdiPlay'} size={14}
|
||||
title={tt.enabled ? t('trackers.pause') : t('trackers.resume')}
|
||||
onclick={() => updateTargetLink(tracker.id, tt, 'enabled', !tt.enabled)} />
|
||||
@@ -442,6 +455,29 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if testMenuOpen}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div style="position:fixed; top:0; left:0; right:0; bottom:0; z-index:9998;"
|
||||
onclick={() => testMenuOpen = null}
|
||||
onkeydown={(e) => { if (e.key === 'Escape') testMenuOpen = null; }}>
|
||||
</div>
|
||||
<div style="{testMenuStyle} background:var(--color-card); border:1px solid var(--color-border); border-radius:0.5rem; box-shadow:0 10px 25px rgba(0,0,0,0.3); padding:0.25rem; min-width:10rem;">
|
||||
{#each testTypes as tt}
|
||||
{@const trackerId = trackers.find(t => t.tracker_targets?.some((x: any) => String(x.id) === testMenuOpen))?.id}
|
||||
<button
|
||||
onclick={() => trackerId && testTrackerTarget(trackerId, Number(testMenuOpen), tt.key)}
|
||||
disabled={!!ttTesting[`${testMenuOpen}_${tt.key}`]}
|
||||
class="flex items-center gap-2 w-full px-3 py-1.5 text-sm rounded hover:bg-[var(--color-muted)] transition-colors disabled:opacity-50 text-left">
|
||||
<MdiIcon name={tt.icon} size={14} />
|
||||
{t(tt.labelKey)}
|
||||
{#if ttTesting[`${testMenuOpen}_${tt.key}`]}
|
||||
<span class="ml-auto text-xs text-[var(--color-muted-foreground)]">...</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if linkWarning}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div style="position:fixed; top:0; left:0; right:0; bottom:0; z-index:9998; background:rgba(0,0,0,0.5);"
|
||||
|
||||
Reference in New Issue
Block a user