4049efe186
- Fix bot card header overflow by replacing "Sync with Telegram" text button with icon button, add flex-wrap - Rename sync button label to "Sync Commands" - Remove decorative dashes from selector placeholders (— X — → X) - Show selected provider name/icon in dashboard stat card when global provider filter is active - Add selector placeholder convention to frontend-architecture.md
519 lines
24 KiB
Svelte
519 lines
24 KiB
Svelte
<script lang="ts">
|
|
import { slide } from 'svelte/transition';
|
|
import { api } from '$lib/api';
|
|
import { t, getLocale } from '$lib/i18n';
|
|
import { telegramBotsCache } from '$lib/stores/caches.svelte';
|
|
import PageHeader from '$lib/components/PageHeader.svelte';
|
|
import Card from '$lib/components/Card.svelte';
|
|
import IconPicker from '$lib/components/IconPicker.svelte';
|
|
import MdiIcon from '$lib/components/MdiIcon.svelte';
|
|
import EmptyState from '$lib/components/EmptyState.svelte';
|
|
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
|
|
import IconButton from '$lib/components/IconButton.svelte';
|
|
import EntitySelect from '$lib/components/EntitySelect.svelte';
|
|
import { snackSuccess, snackError, snackInfo } from '$lib/stores/snackbar.svelte';
|
|
import type { TelegramBot, TelegramChat } from '$lib/types';
|
|
|
|
interface CommandTrackerSummary { id: number; name: string; icon?: string; enabled: boolean }
|
|
interface ListenerEntry { listener_type: string; listener_id: number }
|
|
interface WebhookStatusInfo { url?: string; pending_update_count?: number; last_error_message?: string }
|
|
interface ApiResult { success: boolean; error?: string; verified?: boolean }
|
|
|
|
let { settings, onreload }: { settings: Record<string, string>; onreload: () => Promise<void> } = $props();
|
|
|
|
let bots = $derived(telegramBotsCache.items);
|
|
let showForm = $state(false);
|
|
let editing = $state<number | null>(null);
|
|
let form = $state({ name: '', icon: '', token: '' });
|
|
let error = $state('');
|
|
let submitting = $state(false);
|
|
let confirmDelete = $state<{ id: number; onconfirm: () => Promise<void> } | null>(null);
|
|
|
|
// Per-bot expandable sections
|
|
let chats = $state<Record<number, TelegramChat[]>>({});
|
|
let chatsLoading = $state<Record<number, boolean>>({});
|
|
let expandedSection = $state<Record<number, string>>({});
|
|
|
|
// Webhook status per bot
|
|
let webhookStatus = $state<Record<number, WebhookStatusInfo>>({});
|
|
|
|
let chatTesting = $state<Record<string, boolean>>({});
|
|
let modeChanging = $state<Record<number, boolean>>({});
|
|
|
|
// Listener status: command trackers using this bot
|
|
let botListenerStatus = $state<Record<number, CommandTrackerSummary[]>>({});
|
|
let botListenerLoading = $state<Record<number, boolean>>({});
|
|
|
|
function openNew() { form = { name: '', icon: '', token: '' }; editing = null; showForm = true; }
|
|
function editBot(bot: TelegramBot) { form = { name: bot.name, icon: bot.icon || '', token: '' }; editing = bot.id; showForm = true; }
|
|
|
|
async function saveBot(e: SubmitEvent) {
|
|
e.preventDefault(); error = ''; submitting = true;
|
|
try {
|
|
if (editing) {
|
|
await api(`/telegram-bots/${editing}`, { method: 'PUT', body: JSON.stringify({ name: form.name, icon: form.icon }) });
|
|
snackSuccess(t('snack.botUpdated'));
|
|
} else {
|
|
await api('/telegram-bots', { method: 'POST', body: JSON.stringify(form) });
|
|
snackSuccess(t('snack.botRegistered'));
|
|
}
|
|
form = { name: '', icon: '', token: '' }; showForm = false; editing = null; await onreload();
|
|
} catch (err: any) { error = err.message; snackError(err.message); }
|
|
finally { submitting = false; }
|
|
}
|
|
|
|
function remove(id: number) {
|
|
confirmDelete = {
|
|
id,
|
|
onconfirm: async () => {
|
|
try { await api(`/telegram-bots/${id}`, { method: 'DELETE' }); await onreload(); snackSuccess(t('snack.botDeleted')); }
|
|
catch (err: any) { error = err.message; snackError(err.message); }
|
|
finally { confirmDelete = null; }
|
|
}
|
|
};
|
|
}
|
|
|
|
function toggleSection(botId: number, section: string) {
|
|
if (expandedSection[botId] === section) {
|
|
expandedSection = { ...expandedSection, [botId]: '' };
|
|
return;
|
|
}
|
|
expandedSection = { ...expandedSection, [botId]: section };
|
|
if (section === 'chats') loadChats(botId);
|
|
}
|
|
|
|
async function loadChats(botId: number) {
|
|
chatsLoading = { ...chatsLoading, [botId]: true };
|
|
try { chats = { ...chats, [botId]: await api<TelegramChat[]>(`/telegram-bots/${botId}/chats`) }; } catch (e) { console.warn('Failed to load chats:', e); chats = { ...chats, [botId]: [] }; }
|
|
chatsLoading = { ...chatsLoading, [botId]: false };
|
|
}
|
|
|
|
async function discoverChats(botId: number) {
|
|
chatsLoading = { ...chatsLoading, [botId]: true };
|
|
try {
|
|
chats = { ...chats, [botId]: await api<TelegramChat[]>(`/telegram-bots/${botId}/chats/discover`, { method: 'POST' }) };
|
|
snackSuccess(t('telegramBot.chatsDiscovered'));
|
|
} catch (err: any) { snackError(err.message); }
|
|
chatsLoading = { ...chatsLoading, [botId]: false };
|
|
}
|
|
|
|
async function deleteChat(botId: number, chatDbId: number) {
|
|
try {
|
|
await api(`/telegram-bots/${botId}/chats/${chatDbId}`, { method: 'DELETE' });
|
|
chats[botId] = (chats[botId] || []).filter((c) => c.id !== chatDbId);
|
|
snackSuccess(t('telegramBot.chatDeleted'));
|
|
} catch (err: any) { snackError(err.message); }
|
|
}
|
|
|
|
const LANG_ITEMS = [
|
|
{ value: '', label: '—', icon: 'mdiTranslate', desc: 'Auto' },
|
|
{ value: 'en', label: 'EN', icon: 'mdiAlphaECircle', desc: 'English' },
|
|
{ value: 'ru', label: 'RU', icon: 'mdiAlphaRCircle', desc: 'Русский' },
|
|
{ value: 'uk', label: 'UK', icon: 'mdiAlphaUCircle', desc: 'Українська' },
|
|
{ value: 'de', label: 'DE', icon: 'mdiAlphaDCircle', desc: 'Deutsch' },
|
|
{ value: 'fr', label: 'FR', icon: 'mdiAlphaFCircle', desc: 'Français' },
|
|
{ value: 'es', label: 'ES', icon: 'mdiAlphaECircle', desc: 'Español' },
|
|
{ value: 'it', label: 'IT', icon: 'mdiAlphaICircle', desc: 'Italiano' },
|
|
{ value: 'pt', label: 'PT', icon: 'mdiAlphaPCircle', desc: 'Português' },
|
|
{ value: 'zh', label: 'ZH', icon: 'mdiAlphaZCircle', desc: '中文' },
|
|
{ value: 'ja', label: 'JA', icon: 'mdiAlphaJCircle', desc: '日本語' },
|
|
{ value: 'ko', label: 'KO', icon: 'mdiAlphaKCircle', desc: '한국어' },
|
|
{ value: 'pl', label: 'PL', icon: 'mdiAlphaPCircle', desc: 'Polski' },
|
|
{ value: 'nl', label: 'NL', icon: 'mdiAlphaNCircle', desc: 'Nederlands' },
|
|
{ value: 'tr', label: 'TR', icon: 'mdiAlphaTCircle', desc: 'Türkçe' },
|
|
{ value: 'ar', label: 'AR', icon: 'mdiAlphaACircle', desc: 'العربية' },
|
|
{ value: 'hi', label: 'HI', icon: 'mdiAlphaHCircle', desc: 'हिन्दी' },
|
|
];
|
|
|
|
async function updateChatLanguage(botId: number, chat: TelegramChat, lang: string) {
|
|
try {
|
|
await api(`/telegram-bots/${botId}/chats/${chat.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ language_override: lang }),
|
|
});
|
|
chats[botId] = (chats[botId] || []).map(c =>
|
|
c.id === chat.id ? { ...c, language_override: lang } : c
|
|
);
|
|
snackSuccess(t('telegramBot.languageUpdated'));
|
|
} catch (err: any) { snackError(err.message); }
|
|
}
|
|
|
|
async function toggleChatCommands(botId: number, chat: TelegramChat) {
|
|
const newVal = !chat.commands_enabled;
|
|
try {
|
|
await api(`/telegram-bots/${botId}/chats/${chat.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ commands_enabled: newVal }),
|
|
});
|
|
chats[botId] = (chats[botId] || []).map(c =>
|
|
c.id === chat.id ? { ...c, commands_enabled: newVal } : c
|
|
);
|
|
} catch (err: any) { snackError(err.message); }
|
|
}
|
|
|
|
async function loadListenerStatus(botId: number) {
|
|
botListenerLoading = { ...botListenerLoading, [botId]: true };
|
|
try {
|
|
const trackers = await api<CommandTrackerSummary[]>('/command-trackers');
|
|
const matched: CommandTrackerSummary[] = [];
|
|
for (const trk of trackers) {
|
|
try {
|
|
const listeners = await api<ListenerEntry[]>(`/command-trackers/${trk.id}/listeners`);
|
|
const hasBot = listeners.some((l) => l.listener_type === 'telegram_bot' && l.listener_id === botId);
|
|
if (hasBot) matched.push(trk);
|
|
} catch (e) { console.warn('Failed to load listeners for tracker:', e); }
|
|
}
|
|
botListenerStatus = { ...botListenerStatus, [botId]: matched };
|
|
} catch (e) { console.warn('Failed to load listener status:', e); botListenerStatus = { ...botListenerStatus, [botId]: [] }; }
|
|
botListenerLoading = { ...botListenerLoading, [botId]: false };
|
|
}
|
|
|
|
async function toggleListenerEnabled(botId: number, trk: CommandTrackerSummary) {
|
|
const endpoint = trk.enabled ? 'disable' : 'enable';
|
|
try {
|
|
await api(`/command-trackers/${trk.id}/${endpoint}`, { method: 'POST' });
|
|
botListenerStatus = {
|
|
...botListenerStatus,
|
|
[botId]: (botListenerStatus[botId] || []).map(t =>
|
|
t.id === trk.id ? { ...t, enabled: !t.enabled } : t
|
|
),
|
|
};
|
|
} catch (err: any) { snackError(err.message); }
|
|
}
|
|
|
|
async function syncCommands(botId: number) {
|
|
modeChanging = { ...modeChanging, [botId]: true };
|
|
try {
|
|
const res = await api<ApiResult>(`/telegram-bots/${botId}/sync-commands`, { method: 'POST' });
|
|
if (res.success) snackSuccess(t('telegramBot.commandsSynced'));
|
|
else snackError(res.error || 'Failed');
|
|
} catch (err: any) { snackError(err.message); }
|
|
modeChanging = { ...modeChanging, [botId]: false };
|
|
}
|
|
|
|
async function switchMode(botId: number, mode: string) {
|
|
modeChanging = { ...modeChanging, [botId]: true };
|
|
try {
|
|
await api(`/telegram-bots/${botId}`, { method: 'PUT', body: JSON.stringify({ update_mode: mode }) });
|
|
await onreload();
|
|
if (mode === 'webhook') {
|
|
await loadWebhookStatus(botId);
|
|
}
|
|
snackSuccess(t('snack.botUpdated'));
|
|
} catch (err: any) { snackError(err.message); }
|
|
modeChanging = { ...modeChanging, [botId]: false };
|
|
}
|
|
|
|
async function loadWebhookStatus(botId: number) {
|
|
try {
|
|
webhookStatus = { ...webhookStatus, [botId]: await api<WebhookStatusInfo>(`/telegram-bots/${botId}/webhook/status`) };
|
|
} catch (e) { console.warn('Failed to load webhook status:', e); webhookStatus = { ...webhookStatus, [botId]: {} }; }
|
|
}
|
|
|
|
async function registerWebhook(botId: number) {
|
|
modeChanging = { ...modeChanging, [botId]: true };
|
|
try {
|
|
const res = await api<ApiResult>(`/telegram-bots/${botId}/webhook/register`, { method: 'POST' });
|
|
if (res.success) {
|
|
snackSuccess(res.verified ? t('telegramBot.webhookVerified') : t('telegramBot.webhookRegistered'));
|
|
await loadWebhookStatus(botId);
|
|
} else {
|
|
snackError(res.error || 'Failed to register webhook');
|
|
}
|
|
} catch (err: any) { snackError(err.message); }
|
|
modeChanging = { ...modeChanging, [botId]: false };
|
|
}
|
|
|
|
async function unregisterWebhook(botId: number) {
|
|
modeChanging = { ...modeChanging, [botId]: true };
|
|
try {
|
|
const res = await api<ApiResult>(`/telegram-bots/${botId}/webhook/unregister`, { method: 'POST' });
|
|
if (res.success) { snackSuccess(t('telegramBot.webhookUnregistered')); await loadWebhookStatus(botId); }
|
|
else snackError(res.error || 'Failed');
|
|
} catch (err: any) { snackError(err.message); }
|
|
modeChanging = { ...modeChanging, [botId]: false };
|
|
}
|
|
|
|
function copyChatId(e: Event, chatId: string) {
|
|
e.stopPropagation();
|
|
if (navigator.clipboard?.writeText) {
|
|
navigator.clipboard.writeText(chatId);
|
|
} else {
|
|
// Fallback for non-HTTPS contexts
|
|
const ta = document.createElement('textarea');
|
|
ta.value = chatId;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
}
|
|
snackInfo(`${t('snack.copied')}: ${chatId}`);
|
|
}
|
|
|
|
async function testChat(e: Event, botId: number, chatId: string) {
|
|
e.stopPropagation();
|
|
const key = `${botId}_${chatId}`;
|
|
if (chatTesting[key]) return;
|
|
chatTesting = { ...chatTesting, [key]: true };
|
|
try {
|
|
const res = await api<ApiResult>(`/telegram-bots/${botId}/chats/${chatId}/test?locale=${getLocale()}`, { method: 'POST' });
|
|
if (res.success) snackSuccess(t('snack.targetTestSent'));
|
|
else snackError(res.error || 'Failed');
|
|
} catch (err: any) { snackError(err.message); }
|
|
chatTesting = { ...chatTesting, [key]: false };
|
|
}
|
|
|
|
function chatTypeLabel(type: string): string {
|
|
const map: Record<string, string> = {
|
|
private: t('telegramBot.private'),
|
|
group: t('telegramBot.group'),
|
|
supergroup: t('telegramBot.supergroup'),
|
|
channel: t('telegramBot.channel'),
|
|
};
|
|
return map[type] || type;
|
|
}
|
|
</script>
|
|
|
|
<PageHeader title={t('telegramBot.title')} description={t('telegramBot.description')}>
|
|
<button onclick={() => { showForm ? (showForm = false, editing = null) : openNew(); }}
|
|
class="px-3 py-1.5 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90">
|
|
{showForm ? t('common.cancel') : t('telegramBot.addBot')}
|
|
</button>
|
|
</PageHeader>
|
|
|
|
{#if showForm}
|
|
<Card class="mb-6">
|
|
{#if error}<div class="bg-[var(--color-error-bg)] text-[var(--color-error-fg)] text-sm rounded-md p-3 mb-4">{error}</div>{/if}
|
|
<form onsubmit={saveBot} class="space-y-3">
|
|
<div>
|
|
<label for="bot-name" class="block text-sm font-medium mb-1">{t('telegramBot.name')}</label>
|
|
<div class="flex gap-2">
|
|
<IconPicker value={form.icon} onselect={(v: string) => form.icon = v} />
|
|
<input id="bot-name" bind:value={form.name} required placeholder={t('telegramBot.namePlaceholder')}
|
|
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
|
</div>
|
|
</div>
|
|
{#if !editing}
|
|
<div>
|
|
<label for="bot-token" class="block text-sm font-medium mb-1">{t('telegramBot.token')}</label>
|
|
<input id="bot-token" bind:value={form.token} required placeholder={t('telegramBot.tokenPlaceholder')}
|
|
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)] font-mono" />
|
|
</div>
|
|
{/if}
|
|
<button type="submit" disabled={submitting}
|
|
class="px-4 py-2 bg-[var(--color-primary)] text-[var(--color-primary-foreground)] rounded-md text-sm font-medium hover:opacity-90 disabled:opacity-50">
|
|
{submitting ? t('common.loading') : (editing ? t('common.save') : t('telegramBot.addBot'))}
|
|
</button>
|
|
</form>
|
|
</Card>
|
|
{/if}
|
|
|
|
{#if bots.length === 0 && !showForm}
|
|
<Card>
|
|
<EmptyState icon="mdiRobot" message={t('telegramBot.noBots')} />
|
|
</Card>
|
|
{:else}
|
|
<div class="space-y-3 stagger-children">
|
|
{#each bots as bot}
|
|
<Card hover entityId={bot.id}>
|
|
<div class="flex items-center justify-between gap-2 flex-wrap">
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<span style="color: var(--color-primary);"><MdiIcon name={bot.icon || 'mdiRobot'} size={20} /></span>
|
|
<p class="font-medium">{bot.name}</p>
|
|
{#if bot.bot_username}
|
|
<span class="text-xs text-[var(--color-muted-foreground)]">@{bot.bot_username}</span>
|
|
{/if}
|
|
<!-- Mode badge -->
|
|
<span class="text-xs px-1.5 py-0.5 rounded font-mono {bot.update_mode === 'webhook'
|
|
? 'bg-blue-500/10 text-blue-500'
|
|
: 'bg-emerald-500/10 text-emerald-500'}">
|
|
{bot.update_mode === 'webhook' ? t('telegramBot.webhook') : t('telegramBot.polling')}
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-[var(--color-muted-foreground)] font-mono">{bot.token_preview}</p>
|
|
</div>
|
|
<div class="flex items-center gap-1 flex-shrink-0 flex-wrap">
|
|
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => editBot(bot)} />
|
|
<button onclick={() => toggleSection(bot.id, 'chats')}
|
|
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1 whitespace-nowrap">
|
|
{t('telegramBot.chats')} {expandedSection[bot.id] === 'chats' ? '▲' : '▼'}
|
|
</button>
|
|
<button onclick={() => { toggleSection(bot.id, 'listeners'); if (expandedSection[bot.id] === 'listeners') loadListenerStatus(bot.id); }}
|
|
class="text-xs text-[var(--color-muted-foreground)] hover:underline px-2 py-1 whitespace-nowrap">
|
|
{t('commandTracker.listeners')} {expandedSection[bot.id] === 'listeners' ? '▲' : '▼'}
|
|
</button>
|
|
<IconButton icon="mdiSync" title={t('telegramBot.syncCommands')} onclick={() => syncCommands(bot.id)} disabled={modeChanging[bot.id]} />
|
|
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(bot.id)} variant="danger" />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Chats section -->
|
|
{#if expandedSection[bot.id] === 'chats'}
|
|
<div class="mt-3 border-t border-[var(--color-border)] pt-3" in:slide>
|
|
{#if chatsLoading[bot.id]}
|
|
<p class="text-xs text-[var(--color-muted-foreground)]">{t('common.loading')}</p>
|
|
{:else if (chats[bot.id] || []).length === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)]">{t('telegramBot.noChats')}</p>
|
|
{:else}
|
|
{@const gridStyle = "display:grid; grid-template-columns:1fr 80px 40px 100px 50px 130px 60px; align-items:center; gap:0.5rem;"}
|
|
<!-- Header -->
|
|
<div style="{gridStyle} padding:0.25rem 0.5rem; border-bottom:1px solid var(--color-border);"
|
|
class="text-[0.65rem] font-semibold uppercase tracking-wide text-[var(--color-muted-foreground)]">
|
|
<span>{t('telegramBot.chatName')}</span>
|
|
<span style="text-align:center">{t('telegramBot.chatType')}</span>
|
|
<span style="text-align:center">{t('telegramBot.chatLang')}</span>
|
|
<span style="text-align:center">{t('telegramBot.langOverride')}</span>
|
|
<span style="text-align:center">{t('telegramBot.cmds')}</span>
|
|
<span style="text-align:center">{t('telegramBot.chatId')}</span>
|
|
<span></span>
|
|
</div>
|
|
<!-- Rows -->
|
|
{#each chats[bot.id] as chat}
|
|
<div style={gridStyle}
|
|
class="text-sm px-2 py-1.5 rounded hover:bg-[var(--color-muted)] cursor-pointer"
|
|
onclick={(e: MouseEvent) => copyChatId(e, chat.chat_id)}
|
|
title={t('telegramBot.clickToCopy')}
|
|
role="button" tabindex="0">
|
|
<span class="font-medium truncate">{chat.title || chat.username || 'Unknown'}</span>
|
|
<span style="text-align:center" class="text-xs px-1.5 py-0.5 rounded bg-[var(--color-muted)] text-[var(--color-muted-foreground)]">{chatTypeLabel(chat.type)}</span>
|
|
<span style="text-align:center" class="text-xs text-[var(--color-muted-foreground)]">{(chat.language_code || '—').toUpperCase()}</span>
|
|
<div style="justify-self:center" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
|
<EntitySelect
|
|
items={LANG_ITEMS}
|
|
value={chat.language_override || ''}
|
|
size="sm"
|
|
onselect={(val) => updateChatLanguage(bot.id, chat, String(val ?? ''))}
|
|
/>
|
|
</div>
|
|
<div style="justify-self:center" onclick={(e: MouseEvent) => e.stopPropagation()}>
|
|
<button
|
|
style="width:28px; height:16px; border-radius:8px; position:relative; transition:background-color 0.2s; background-color:{chat.commands_enabled ? 'var(--color-primary)' : 'var(--color-border)'};"
|
|
title={t('telegramBot.commandsToggle')}
|
|
onclick={() => toggleChatCommands(bot.id, chat)}>
|
|
<span style="position:absolute; top:2px; width:12px; height:12px; border-radius:50%; transition:left 0.2s; left:{chat.commands_enabled ? '14px' : '2px'}; background:{chat.commands_enabled ? 'white' : 'var(--color-muted-foreground)'};" ></span>
|
|
</button>
|
|
</div>
|
|
<span style="text-align:center" class="text-xs text-[var(--color-muted-foreground)] font-mono">{chat.chat_id}</span>
|
|
<div style="justify-self:end" class="flex items-center gap-1">
|
|
<IconButton icon="mdiSend" title="Test message" size={14}
|
|
onclick={(e: MouseEvent) => testChat(e, bot.id, chat.chat_id)}
|
|
disabled={chatTesting[`${bot.id}_${chat.chat_id}`]} />
|
|
<IconButton icon="mdiDelete" title={t('common.delete')} size={14}
|
|
onclick={(e: MouseEvent) => { e.stopPropagation(); deleteChat(bot.id, chat.id); }} variant="danger" />
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{/if}
|
|
<button onclick={() => discoverChats(bot.id)}
|
|
class="text-xs text-[var(--color-primary)] hover:underline mt-2 flex items-center gap-1">
|
|
<MdiIcon name="mdiSync" size={14} />
|
|
{t('telegramBot.discoverChats')}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
<!-- Listener Status section -->
|
|
{#if expandedSection[bot.id] === 'listeners'}
|
|
<div class="mt-3 border-t border-[var(--color-border)] pt-3 space-y-3" in:slide>
|
|
{#if botListenerLoading[bot.id]}
|
|
<p class="text-xs text-[var(--color-muted-foreground)]">{t('common.loading')}</p>
|
|
{:else if (botListenerStatus[bot.id] || []).length === 0}
|
|
<p class="text-xs text-[var(--color-muted-foreground)]">{t('commandTracker.noListeners')}</p>
|
|
{:else}
|
|
<div class="space-y-1">
|
|
{#each botListenerStatus[bot.id] as trk}
|
|
<div class="flex items-center justify-between text-sm px-2 py-1 rounded hover:bg-[var(--color-muted)]">
|
|
<div class="flex items-center gap-2">
|
|
<MdiIcon name={trk.icon || 'mdiConsoleLine'} size={14} />
|
|
<a href="/command-trackers" class="font-medium text-[var(--color-primary)] hover:underline">{trk.name}</a>
|
|
</div>
|
|
<button
|
|
style="width:28px; height:16px; border-radius:8px; position:relative; transition:background-color 0.2s; background-color:{trk.enabled ? 'var(--color-primary)' : 'var(--color-border)'};"
|
|
title={trk.enabled ? t('notificationTracker.pause') : t('notificationTracker.resume')}
|
|
onclick={() => toggleListenerEnabled(bot.id, trk)}>
|
|
<span style="position:absolute; top:2px; width:12px; height:12px; border-radius:50%; transition:left 0.2s; left:{trk.enabled ? '14px' : '2px'}; background:{trk.enabled ? 'white' : 'var(--color-muted-foreground)'};" ></span>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Update mode -->
|
|
<div class="border-t border-[var(--color-border)] pt-3">
|
|
<p class="text-xs font-medium mb-2">{t('telegramBot.updateMode')}</p>
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<div class="flex items-center rounded-md border border-[var(--color-border)] overflow-hidden">
|
|
<button onclick={() => switchMode(bot.id, 'polling')}
|
|
disabled={modeChanging[bot.id] || bot.update_mode === 'polling'}
|
|
class="px-3 py-1 text-xs transition-colors {bot.update_mode === 'polling'
|
|
? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]'
|
|
: 'hover:bg-[var(--color-muted)]'} disabled:opacity-70">
|
|
<MdiIcon name="mdiSync" size={14} />
|
|
{t('telegramBot.polling')}
|
|
</button>
|
|
<button onclick={() => switchMode(bot.id, 'webhook')}
|
|
disabled={modeChanging[bot.id] || bot.update_mode === 'webhook'}
|
|
class="px-3 py-1 text-xs transition-colors {bot.update_mode === 'webhook'
|
|
? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]'
|
|
: 'hover:bg-[var(--color-muted)]'} disabled:opacity-70">
|
|
<MdiIcon name="mdiWebhook" size={14} />
|
|
{t('telegramBot.webhook')}
|
|
</button>
|
|
</div>
|
|
|
|
{#if bot.update_mode === 'polling'}
|
|
<span class="text-xs text-emerald-500 flex items-center gap-1">
|
|
<MdiIcon name="mdiCheckCircle" size={14} />
|
|
{t('telegramBot.pollingActive')}
|
|
</span>
|
|
{/if}
|
|
|
|
{#if bot.update_mode === 'webhook'}
|
|
<button onclick={() => registerWebhook(bot.id)} disabled={modeChanging[bot.id]}
|
|
class="px-2 py-1 text-xs border border-[var(--color-border)] rounded-md hover:bg-[var(--color-muted)] disabled:opacity-50">
|
|
{t('telegramBot.registerWebhook')}
|
|
</button>
|
|
<button onclick={() => unregisterWebhook(bot.id)} disabled={modeChanging[bot.id]}
|
|
class="px-2 py-1 text-xs text-[var(--color-muted-foreground)] hover:underline disabled:opacity-50">
|
|
{t('telegramBot.unregisterWebhook')}
|
|
</button>
|
|
{#if webhookStatus[bot.id]}
|
|
{@const ws = webhookStatus[bot.id]}
|
|
<span class="text-xs font-mono {ws.url ? 'text-blue-500' : 'text-[var(--color-muted-foreground)]'}">
|
|
{ws.url ? t('telegramBot.webhookActive') : t('telegramBot.webhookNotSet')}
|
|
{#if (ws.pending_update_count ?? 0) > 0}
|
|
({ws.pending_update_count} {t('telegramBot.pendingUpdates')})
|
|
{/if}
|
|
</span>
|
|
{#if ws.last_error_message}
|
|
<span class="text-xs text-red-500">{t('telegramBot.webhookError')}: {ws.last_error_message}</span>
|
|
{/if}
|
|
{:else}
|
|
<button onclick={() => loadWebhookStatus(bot.id)}
|
|
class="text-xs text-[var(--color-primary)] hover:underline">
|
|
{t('telegramBot.webhookStatus')}
|
|
</button>
|
|
{/if}
|
|
{/if}
|
|
|
|
{#if !settings.external_url && bot.update_mode === 'webhook'}
|
|
<span class="text-xs text-amber-500 flex items-center gap-1">
|
|
<MdiIcon name="mdiAlert" size={14} />
|
|
{t('telegramBot.noExternalDomain')}
|
|
</span>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</Card>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<ConfirmModal open={confirmDelete !== null} message={t('telegramBot.confirmDelete')}
|
|
onconfirm={() => confirmDelete?.onconfirm()} oncancel={() => confirmDelete = null} />
|
|
|