Files
forust 26b6344f7a
ci / lint (push) Successful in 12s
ci / validate (push) Successful in 7s
ci / publish (push) Successful in 8s
Format web UI assets
2026-06-28 00:55:11 +02:00

432 lines
15 KiB
JavaScript

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 = '<div class="empty-state">No accounts yet.</div>';
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');
});