const settingsState = { accounts: [], activeAccount: null, dashboard: null, auth: null, continuous: null, }; const ACTIVE_ACCOUNT_STORAGE_KEY = 'telegramScraper.activeAccount'; async function api(path, options = {}) { const response = await fetch(path, { headers: { 'Content-Type': 'application/json' }, ...options, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || 'Request failed'); } return data; } function showToast(message, type = 'info') { let root = document.getElementById('toast-root'); if (!root) { root = document.createElement('div'); root.id = 'toast-root'; root.className = 'toast-root'; document.body.appendChild(root); } const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; root.appendChild(toast); window.setTimeout(() => { toast.classList.add('toast-hide'); window.setTimeout(() => toast.remove(), 250); }, 3600); } function downloadJson(filename, payload) { const blob = new Blob([JSON.stringify(payload, null, 2) + '\n'], { type: 'application/json', }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); URL.revokeObjectURL(url); } function displayTime(value) { if (!value) return '-'; const date = new Date(value); if (Number.isNaN(date.getTime())) return String(value).replace('T', ' ').replace('+00:00', ' UTC'); return date.toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); } function isAccountAuthorized(auth) { const status = auth?.auth_status || auth || {}; return ( auth?.phase === 'authorized' || auth?.status === 'authorized' || status.status === 'ready' || status.status === 'authorized' ); } function accountLabel(account) { return account?.label || account?.id || 'No account'; } function setActiveAccount(accountId) { settingsState.activeAccount = accountId || null; if (accountId) { localStorage.setItem(ACTIVE_ACCOUNT_STORAGE_KEY, accountId); } else { localStorage.removeItem(ACTIVE_ACCOUNT_STORAGE_KEY); } } function chooseInitialAccount() { const requested = new URLSearchParams(window.location.search).get('account'); if (requested && settingsState.accounts.some((account) => account.id === requested)) { return requested; } const saved = localStorage.getItem(ACTIVE_ACCOUNT_STORAGE_KEY); if (saved && settingsState.accounts.some((account) => account.id === saved)) { return saved; } return settingsState.accounts[0]?.id || null; } function renderAccountList() { const root = document.getElementById('settings-account-list'); root.innerHTML = ''; if (!settingsState.accounts.length) { root.innerHTML = '
No accounts yet.
'; return; } settingsState.accounts.forEach((account) => { const button = document.createElement('button'); button.className = 'settings-account-button'; button.type = 'button'; button.classList.toggle('active', account.id === settingsState.activeAccount); const name = document.createElement('span'); name.className = 'settings-account-name'; name.textContent = accountLabel(account); const status = document.createElement('span'); status.className = isAccountAuthorized(account.auth) ? 'settings-account-status ok' : 'settings-account-status'; status.textContent = isAccountAuthorized(account.auth) ? 'Authorized' : 'Needs auth'; button.append(name, status); button.addEventListener('click', () => loadAccount(account.id)); root.appendChild(button); }); } function renderEmptyState() { const hasAccount = Boolean(settingsState.activeAccount); document.getElementById('settings-empty').classList.toggle('hidden', hasAccount); document.getElementById('settings-export-btn').disabled = !hasAccount; document.getElementById('settings-delete-btn').disabled = !hasAccount; document .getElementById('settings-credentials-form') .querySelectorAll('input, button') .forEach((node) => { node.disabled = !hasAccount; }); document.getElementById('settings-start-qr-btn').disabled = !hasAccount; document .getElementById('settings-phone-form') .querySelectorAll('input, button') .forEach((node) => { node.disabled = !hasAccount; }); document.getElementById('settings-scrape-media-toggle').disabled = !hasAccount; } function renderAccountData() { const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount); const dashboard = settingsState.dashboard || {}; const health = dashboard.health || {}; const auth = settingsState.auth || account?.auth || {}; const continuous = settingsState.continuous || {}; const continuousStatus = continuous.status || {}; const continuousConfig = continuous.config || {}; document.getElementById('settings-title').textContent = account ? accountLabel(account) : 'No account selected'; document.getElementById('settings-subtitle').textContent = account ? `Account ID: ${account.id}` : 'Create or import an account to manage settings.'; document.getElementById('settings-auth-label').textContent = account ? `${accountLabel(account)} auth status` : 'No account selected.'; document.getElementById('health-credentials').textContent = health.api_credentials ? 'Saved' : 'Missing'; document.getElementById('health-credentials').style.color = health.api_credentials ? 'var(--ok)' : 'var(--warn)'; document.getElementById('health-session').textContent = health.session_ready || isAccountAuthorized(auth) ? 'Ready' : 'Missing'; document.getElementById('health-session').style.color = health.session_ready || isAccountAuthorized(auth) ? 'var(--ok)' : 'var(--warn)'; document.getElementById('health-continuous').textContent = continuousStatus.running ? 'Running' : continuousConfig.enabled ? 'Enabled' : 'Stopped'; document.getElementById('health-continuous').style.color = continuousStatus.running ? 'var(--ok)' : 'var(--dim)'; document.getElementById('health-last-scrape').textContent = displayTime( health.last_scrape || continuousStatus.last_iteration_at, ); document.getElementById('settings-scrape-media-toggle').checked = Boolean(dashboard.dashboard?.scrape_media); document.getElementById('settings-channel-count').textContent = String(health.channel_count || 0); document.getElementById('settings-message-count').textContent = String(health.message_count || 0); document.getElementById('settings-media-count').textContent = String(health.media_count || 0); renderEmptyState(); renderAccountList(); } async function loadAccounts() { const payload = await api('/api/accounts'); settingsState.accounts = payload.accounts || []; setActiveAccount(chooseInitialAccount()); renderAccountList(); if (settingsState.activeAccount) { await loadAccount(settingsState.activeAccount, { preserveUrl: true }); } else { settingsState.dashboard = null; settingsState.auth = null; settingsState.continuous = null; renderAccountData(); } } async function loadAccount(accountId, options = {}) { setActiveAccount(accountId); const [dashboard, auth, continuous] = await Promise.all([ api(`/api/accounts/${encodeURIComponent(accountId)}`), api(`/api/accounts/${encodeURIComponent(accountId)}/auth`).catch(() => ({})), api(`/api/accounts/${encodeURIComponent(accountId)}/continuous`).catch(() => ({})), ]); settingsState.dashboard = dashboard; settingsState.auth = auth; settingsState.continuous = continuous; if (!options.preserveUrl) { history.replaceState(null, '', `/settings?account=${encodeURIComponent(accountId)}`); } renderAccountData(); } async function createAccount(event) { event.preventDefault(); const accountId = document.getElementById('settings-add-account-id').value.trim(); const label = document.getElementById('settings-add-account-label').value.trim(); const apiId = document.getElementById('settings-add-account-api-id').value.trim(); const apiHash = document.getElementById('settings-add-account-api-hash').value.trim(); if (!accountId) return; await api('/api/accounts', { method: 'POST', body: JSON.stringify({ account_id: accountId, label: label || accountId, api_id: apiId ? parseInt(apiId) : undefined, api_hash: apiHash || undefined, }), }); event.currentTarget.reset(); await loadAccounts(); await loadAccount(accountId); showToast(`Added ${accountId}.`, 'success'); } async function importAccount(event) { const file = event.currentTarget.files?.[0]; if (!file) return; try { const payload = JSON.parse(await file.text()); const accountId = payload.account_id || payload.id; if (!accountId) throw new Error('account_id is missing in import file'); await api('/api/accounts/import', { method: 'POST', body: JSON.stringify(payload), }); event.currentTarget.value = ''; await loadAccounts(); await loadAccount(accountId); showToast(`Imported ${accountId}.`, 'success'); } catch (err) { showToast(`Failed to import account: ${err.message}`, 'error'); } } async function saveCredentials(event) { event.preventDefault(); if (!settingsState.activeAccount) return; const apiId = document.getElementById('settings-api-id-input').value.trim(); const apiHash = document.getElementById('settings-api-hash-input').value.trim(); if (!apiId || !apiHash) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/credentials`, { method: 'POST', body: JSON.stringify({ api_id: apiId, api_hash: apiHash }), }); await loadAccount(settingsState.activeAccount); showToast('Credentials saved.', 'success'); } async function startQrLogin() { if (!settingsState.activeAccount) return; const data = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/qr/start`, { method: 'POST', body: JSON.stringify({}), }); if (data.qr_image) { document.getElementById('settings-qr-image').src = data.qr_image; document.getElementById('settings-qr-wrap').classList.remove('hidden'); } await loadAccount(settingsState.activeAccount); } async function requestPhoneCode(event) { event.preventDefault(); if (!settingsState.activeAccount) return; const phone = document.getElementById('settings-phone-input').value.trim(); if (!phone) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/request`, { method: 'POST', body: JSON.stringify({ phone }), }); document.getElementById('settings-code-form').classList.remove('hidden'); await loadAccount(settingsState.activeAccount); } async function submitPhoneCode(event) { event.preventDefault(); if (!settingsState.activeAccount) return; const code = document.getElementById('settings-code-input').value.trim(); if (!code) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/phone/submit`, { method: 'POST', body: JSON.stringify({ code }), }); document.getElementById('settings-code-input').value = ''; await loadAccount(settingsState.activeAccount); } async function submitPassword(event) { event.preventDefault(); if (!settingsState.activeAccount) return; const password = document.getElementById('settings-password-input').value.trim(); if (!password) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/auth/password`, { method: 'POST', body: JSON.stringify({ password }), }); document.getElementById('settings-password-input').value = ''; await loadAccount(settingsState.activeAccount); } async function exportAccount() { if (!settingsState.activeAccount) return; const payload = await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/export`); downloadJson(`telegram-scraper-account-${settingsState.activeAccount}.json`, payload); showToast('Account exported.', 'success'); } async function deleteAccount() { if (!settingsState.activeAccount) return; const account = settingsState.accounts.find((item) => item.id === settingsState.activeAccount); if (!window.confirm(`Remove account "${accountLabel(account)}"? All its account data will be deleted.`)) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}`, { method: 'DELETE' }); setActiveAccount(null); await loadAccounts(); showToast('Account deleted.', 'success'); } async function toggleMedia(event) { if (!settingsState.activeAccount) return; await api(`/api/accounts/${encodeURIComponent(settingsState.activeAccount)}/settings/media`, { method: 'POST', body: JSON.stringify({ value: event.currentTarget.checked }), }); await loadAccount(settingsState.activeAccount); } function bindEvents() { document.getElementById('settings-add-account-form').addEventListener('submit', async (event) => { try { await createAccount(event); } catch (err) { showToast(`Failed to add account: ${err.message}`, 'error'); } }); document.getElementById('settings-import-account-file').addEventListener('change', importAccount); document.getElementById('settings-credentials-form').addEventListener('submit', async (event) => { try { await saveCredentials(event); } catch (err) { showToast(`Failed to save credentials: ${err.message}`, 'error'); } }); document.getElementById('settings-start-qr-btn').addEventListener('click', async () => { try { await startQrLogin(); } catch (err) { showToast(`QR login failed: ${err.message}`, 'error'); } }); document.getElementById('settings-phone-form').addEventListener('submit', async (event) => { try { await requestPhoneCode(event); } catch (err) { showToast(`Phone login failed: ${err.message}`, 'error'); } }); document.getElementById('settings-code-form').addEventListener('submit', async (event) => { try { await submitPhoneCode(event); } catch (err) { showToast(`Code submit failed: ${err.message}`, 'error'); } }); document.getElementById('settings-password-form').addEventListener('submit', async (event) => { try { await submitPassword(event); } catch (err) { showToast(`Password submit failed: ${err.message}`, 'error'); } }); document.getElementById('settings-export-btn').addEventListener('click', async () => { try { await exportAccount(); } catch (err) { showToast(`Export failed: ${err.message}`, 'error'); } }); document.getElementById('settings-delete-btn').addEventListener('click', async () => { try { await deleteAccount(); } catch (err) { showToast(`Delete failed: ${err.message}`, 'error'); } }); document.getElementById('settings-scrape-media-toggle').addEventListener('change', async (event) => { try { await toggleMedia(event); } catch (err) { showToast(`Media setting failed: ${err.message}`, 'error'); } }); } bindEvents(); loadAccounts().catch((err) => { console.error(err); showToast(`Failed to load settings: ${err.message}`, 'error'); });