diff --git a/webui/app.js b/webui/app.js
index 8af9b4d..e38c656 100644
--- a/webui/app.js
+++ b/webui/app.js
@@ -1,9 +1,17 @@
+/* ── State ───────────────────────────────────────────── */
+
const state = {
- dashboard: null,
- auth: null,
- continuous: null,
+ accounts: [],
+ activeAccount: null,
+ dashboards: {},
+ continuous: {},
+ channels: {},
+ jobs: {},
+ authStates: {},
};
+/* ── API helper ─────────────────────────────────────── */
+
async function api(path, options = {}) {
const response = await fetch(path, {
headers: { "Content-Type": "application/json" },
@@ -16,6 +24,8 @@ async function api(path, options = {}) {
return data;
}
+/* ── Format helpers ─────────────────────────────────── */
+
function formatDate(value) {
if (!value) return "-";
return value.replace("T", " ").replace("+00:00", " UTC");
@@ -58,47 +68,165 @@ function confirmAction(message) {
return window.confirm(message);
}
-function renderAuth(auth) {
- document.getElementById("auth-status").textContent = auth.status;
- document.getElementById("auth-details").textContent = auth.details || "";
+function isAccountAuthorized(auth) {
+ const status = auth?.auth_status || auth || {};
+ return (
+ auth?.phase === "authorized" ||
+ auth?.status === "authorized" ||
+ status.status === "ready" ||
+ status.status === "authorized"
+ );
}
-function toggleHidden(id, hidden) {
- document.getElementById(id).classList.toggle("hidden", hidden);
+/* ── Account tab management ─────────────────────────── */
+
+function renderAccountTabs(accounts) {
+ const container = document.getElementById("account-tabs");
+ const template = document.getElementById("account-tab-template");
+ container.innerHTML = "";
+
+ accounts.forEach((acc) => {
+ const node = template.content.firstElementChild.cloneNode(true);
+ node.dataset.accountId = acc.id;
+ node.querySelector(".account-tab-name").textContent = acc.label || acc.id;
+ const statusEl = node.querySelector(".account-tab-status");
+ if (acc.auth) {
+ const authOk = isAccountAuthorized(acc.auth);
+ statusEl.textContent = authOk ? "●" : "○";
+ statusEl.className = "account-tab-status " + (authOk ? "ok" : "");
+ }
+ if (acc.id === state.activeAccount) {
+ node.classList.add("active");
+ }
+ node.addEventListener("click", () => switchAccount(acc.id));
+ container.appendChild(node);
+ });
}
-function renderAuthPanel(authData) {
- state.auth = authData;
- renderAuth(authData.auth_status);
+function renderAccountPanel(accountId) {
+ const container = document.getElementById("account-panels");
+ const template = document.getElementById("account-panel-template");
- const apiId = authData.saved_credentials?.api_id ?? "";
- document.getElementById("api-id-input").value = apiId || "";
- document.getElementById("api-hash-input").placeholder = authData.saved_credentials?.api_hash_present
- ? "API Hash saved"
- : "API Hash";
+ // Don't duplicate
+ if (document.getElementById(`panel-${accountId}`)) return;
- const showQr = Boolean(authData.qr_image);
- toggleHidden("qr-wrap", !showQr);
- if (showQr) {
- document.getElementById("qr-image").src = authData.qr_image;
+ const node = template.content.firstElementChild.cloneNode(true);
+ node.id = `panel-${accountId}`;
+ node.dataset.accountId = accountId;
+
+ // Add/remove channel form
+ const addForm = node.querySelector(".add-channel-form");
+ addForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const channelId = addForm.querySelector(".add-channel-id").value.trim();
+ const name = addForm.querySelector(".add-channel-name").value.trim();
+ if (!channelId) return;
+ await api(`/api/accounts/${accountId}/channels/add`, {
+ method: "POST",
+ body: JSON.stringify({ channel_id: channelId, name }),
+ });
+ addForm.reset();
+ await refreshAccount(accountId);
+ });
+
+ // Continuous form
+ const contForm = node.querySelector(".continuous-form");
+ contForm.addEventListener("submit", async (event) => {
+ event.preventDefault();
+ const enabled = contForm.querySelector(".continuous-enabled").checked;
+ const intervalMinutes = contForm.querySelector(".continuous-interval").value;
+ const runAllTracked = contForm.querySelector(".continuous-all").checked;
+ const channels = Array.from(contForm.querySelectorAll(".continuous-channel-checkbox:checked")).map(
+ (item) => item.value
+ );
+ if (!runAllTracked && channels.length === 0 && enabled) {
+ if (!confirmAction("Continuous scraping enabled with no selected channels. Save anyway?")) return;
+ }
+ await api(`/api/accounts/${accountId}/continuous`, {
+ method: "POST",
+ body: JSON.stringify({ enabled, interval_minutes: intervalMinutes, run_all_tracked: runAllTracked, channels }),
+ });
+ await refreshAccount(accountId);
+ });
+
+ // "All tracked" checkbox toggles individual checkboxes
+ const contAll = contForm.querySelector(".continuous-all");
+ contAll.addEventListener("change", () => {
+ const checkboxes = contForm.querySelectorAll(".continuous-channel-checkbox");
+ checkboxes.forEach((cb) => { cb.disabled = contAll.checked; });
+ });
+
+ // Content tabs within account panel
+ const contentTabs = node.querySelectorAll(".content-tab");
+ contentTabs.forEach((tab) => {
+ tab.addEventListener("click", () => {
+ const targetId = tab.dataset.tabTarget;
+ contentTabs.forEach((t) => t.classList.toggle("active", t === tab));
+ node.querySelectorAll(".tab-panel").forEach((p) => {
+ p.classList.toggle("active", p.dataset.panel === targetId);
+ });
+ });
+ });
+
+ // Refresh jobs
+ node.querySelector(".refresh-jobs-btn").addEventListener("click", () => refreshAccount(accountId));
+
+ container.appendChild(node);
+}
+
+function switchAccount(accountId) {
+ if (state.activeAccount === accountId) return;
+ state.activeAccount = accountId;
+
+ // Update tabs
+ document.querySelectorAll(".account-tab").forEach((tab) => {
+ tab.classList.toggle("active", tab.dataset.accountId === accountId);
+ });
+
+ // Show/hide panels
+ document.querySelectorAll(".account-panel").forEach((panel) => {
+ panel.classList.toggle("hidden", panel.dataset.accountId !== accountId);
+ });
+
+ // Ensure panel exists
+ if (!document.getElementById(`panel-${accountId}`)) {
+ renderAccountPanel(accountId);
}
- toggleHidden("code-form", authData.phase !== "code_required");
- toggleHidden("password-form", authData.phase !== "password_required");
+ // Update sidebar
+ updateSidebarAccount();
+
+ // Refresh data
+ refreshAccount(accountId);
+
+ // Update settings auth section
+ updateAuthSection(accountId);
}
-function renderSummary(data) {
- document.getElementById("channel-count").textContent = String(data.state.channel_count);
- document.getElementById("forwarding-count").textContent = String(data.state.forwarding_rules.length);
- const toggle = document.getElementById("scrape-media-toggle");
- toggle.checked = Boolean(data.state.scrape_media);
- document.getElementById("scrape-media-label").textContent = toggle.checked ? "ON" : "OFF";
+function updateSidebarAccount() {
+ const acc = state.accounts.find((a) => a.id === state.activeAccount);
+ const nameEl = document.getElementById("active-account-name");
+ const statusEl = document.getElementById("active-account-status");
+ if (acc) {
+ nameEl.textContent = acc.label || acc.id;
+ const authOk = isAccountAuthorized(acc.auth);
+ statusEl.textContent = authOk ? "Authorized session" : "Needs login";
+ statusEl.style.color = authOk ? "var(--ok)" : "var(--danger)";
+ } else {
+ nameEl.textContent = "None";
+ statusEl.textContent = "Add an account in Settings";
+ statusEl.style.color = "var(--dim)";
+ }
}
-function renderChannels(channels) {
- const tbody = document.getElementById("channels-table");
- tbody.innerHTML = "";
+/* ── Dashboard rendering ────────────────────────────── */
+
+function renderChannels(accountId, channels) {
+ const panel = document.getElementById(`panel-${accountId}`);
+ if (!panel) return;
+ const tbody = panel.querySelector(".channels-table");
const template = document.getElementById("channel-row-template");
+ tbody.innerHTML = "";
channels.forEach((channel) => {
const node = template.content.firstElementChild.cloneNode(true);
@@ -109,50 +237,55 @@ function renderChannels(channels) {
node.querySelector(".last-date").textContent = channel.last_date || "-";
node.querySelector(".scrape-btn").addEventListener("click", async () => {
- await api("/api/jobs/scrape", {
+ await api(`/api/accounts/${accountId}/jobs/scrape`, {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
- await refreshDashboard();
+ await refreshAccount(accountId);
});
node.querySelector(".export-btn").addEventListener("click", async () => {
- await api("/api/jobs/export-channel", {
+ await api(`/api/accounts/${accountId}/jobs/export-channel`, {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
- await refreshDashboard();
+ await refreshAccount(accountId);
});
node.querySelector(".export-view-btn").addEventListener("click", () => {
- window.location.href = `/viewer?channel=${encodeURIComponent(channel.channel_id)}`;
+ window.location.href = `/viewer?channel=${encodeURIComponent(channel.channel_id)}&account=${encodeURIComponent(accountId)}`;
});
node.querySelector(".media-btn").addEventListener("click", async () => {
- await api("/api/jobs/rescrape-media", {
+ await api(`/api/accounts/${accountId}/jobs/rescrape-media`, {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
- await refreshDashboard();
+ await refreshAccount(accountId);
});
node.querySelector(".remove-btn").addEventListener("click", async () => {
if (!confirmAction(`Remove ${channel.name} from tracked channels?`)) return;
- await api("/api/channels/remove", {
+ await api(`/api/accounts/${accountId}/channels/remove`, {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
- await refreshDashboard();
+ await refreshAccount(accountId);
});
tbody.appendChild(node);
});
+
+ // Update channel count stat
+ panel.querySelector(".channel-count").textContent = String(channels.length);
}
-function renderJobs(jobs) {
- const root = document.getElementById("jobs-list");
- root.innerHTML = "";
+function renderJobs(accountId, jobs) {
+ const panel = document.getElementById(`panel-${accountId}`);
+ if (!panel) return;
+ const root = panel.querySelector(".jobs-list");
const template = document.getElementById("job-template");
+ root.innerHTML = "";
if (!jobs.length) {
root.innerHTML = '
No jobs yet.
';
@@ -171,18 +304,25 @@ function renderJobs(jobs) {
});
}
-function renderContinuousChannelPicker(channels, config) {
- const root = document.getElementById("continuous-channel-list");
+function renderContinuous(accountId, data) {
+ const panel = document.getElementById(`panel-${accountId}`);
+ if (!panel) return;
+ const config = data.config || {};
+ const status = data.status || {};
+
+ const contForm = panel.querySelector(".continuous-form");
+ contForm.querySelector(".continuous-enabled").checked = Boolean(config.enabled);
+ contForm.querySelector(".continuous-interval").value = config.interval_minutes || 1;
+ contForm.querySelector(".continuous-all").checked = Boolean(config.run_all_tracked);
+
+ // Channel picker
+ const chList = panel.querySelector(".continuous-channel-list");
+ const channels = state.channels[accountId] || [];
const runAll = Boolean(config.run_all_tracked);
const selected = new Set(config.channels || []);
- root.innerHTML = "";
+ chList.innerHTML = "";
- if (!channels.length) {
- root.innerHTML = 'No tracked channels.
';
- return;
- }
-
- channels.forEach((channel) => {
+ channels.forEach((ch) => {
const label = document.createElement("label");
label.className = "checkbox-row";
label.classList.toggle("disabled", runAll);
@@ -190,15 +330,59 @@ function renderContinuousChannelPicker(channels, config) {
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "continuous-channel-checkbox";
- checkbox.value = channel.channel_id;
- checkbox.checked = runAll || selected.has(channel.channel_id);
+ checkbox.value = ch.channel_id;
+ checkbox.checked = runAll || selected.has(ch.channel_id);
checkbox.disabled = runAll;
const text = document.createElement("span");
- text.textContent = `${channel.name} (${channel.channel_id})`;
+ text.textContent = `${ch.name} (${ch.channel_id})`;
label.append(checkbox, text);
- root.appendChild(label);
+ chList.appendChild(label);
+ });
+
+ // Meta
+ panel.querySelector(".continuous-status").textContent = status.running ? "running" : "stopped";
+ panel.querySelector(".continuous-last-iteration").textContent = status.last_iteration_at
+ ? `${relativeTime(status.last_iteration_at)} (${displayTime(status.last_iteration_at)})`
+ : "-";
+ panel.querySelector(".continuous-last-error").textContent = status.last_error || "-";
+
+ // Logs
+ const logsRoot = panel.querySelector(".continuous-logs");
+ const entries = (status.log_entries || status.logs || []).map(normalizeLogEntry);
+ logsRoot.innerHTML = "";
+
+ if (!entries.length) {
+ logsRoot.innerHTML = '-INFONo logs yet.
';
+ return;
+ }
+
+ entries.slice().reverse().forEach((entry) => {
+ const row = document.createElement("div");
+ row.className = `log-line ${entry.level}`;
+ const time = entry.timestamp ? displayTime(entry.timestamp).split(", ").pop() : entry.legacyTime || "-";
+ const absolute = entry.timestamp ? displayTime(entry.timestamp) : entry.legacyTime || "-";
+ row.title = absolute;
+
+ const timeNode = document.createElement("span");
+ timeNode.className = "log-time";
+ timeNode.textContent = time;
+
+ const ageNode = document.createElement("span");
+ ageNode.className = "log-age";
+ ageNode.textContent = entry.timestamp ? relativeTime(entry.timestamp) : "-";
+
+ const levelNode = document.createElement("span");
+ levelNode.className = "log-level";
+ levelNode.textContent = entry.level.toUpperCase();
+
+ const messageNode = document.createElement("span");
+ messageNode.className = "log-message";
+ messageNode.textContent = entry.message;
+
+ row.append(timeNode, ageNode, levelNode, messageNode);
+ logsRoot.appendChild(row);
});
}
@@ -230,246 +414,360 @@ function normalizeLogEntry(entry) {
};
}
-function renderContinuousLogs(status) {
- const root = document.getElementById("continuous-logs");
- const entries = (status.log_entries || status.logs || []).map(normalizeLogEntry);
- root.innerHTML = "";
+function renderSummary(accountId, data) {
+ const panel = document.getElementById(`panel-${accountId}`);
+ if (!panel) return;
+ const d = data.dashboard || data.state || {};
+ panel.querySelector(".forwarding-count").textContent = String((d.forwarding_rules || []).length);
+ const toggle = panel.querySelector(".scrape-media-label");
+ toggle.textContent = d.scrape_media ? "ON" : "OFF";
+}
- if (!entries.length) {
- const empty = document.createElement("div");
- empty.className = "log-line";
- empty.innerHTML = '--INFONo logs yet.';
- root.appendChild(empty);
+/* ── Account data loading ───────────────────────────── */
+
+async function refreshAccount(accountId) {
+ try {
+ const [dashboard, authData, continuousData] = await Promise.all([
+ api(`/api/accounts/${accountId}`),
+ api(`/api/accounts/${accountId}/auth`).catch(() => ({})),
+ api(`/api/accounts/${accountId}/continuous`).catch(() => ({})),
+ ]);
+
+ state.dashboards[accountId] = dashboard;
+ state.continuous[accountId] = continuousData;
+ state.authStates[accountId] = authData;
+ state.channels[accountId] = dashboard.channels || [];
+
+ renderSummary(accountId, dashboard);
+ renderChannels(accountId, dashboard.channels || []);
+ renderJobs(accountId, dashboard.jobs || []);
+ renderContinuous(accountId, continuousData);
+
+ // Update tab status indicator
+ const tab = document.querySelector(`.account-tab[data-account-id="${accountId}"]`);
+ if (tab) {
+ const statusEl = tab.querySelector(".account-tab-status");
+ const authOk = isAccountAuthorized(authData);
+ statusEl.textContent = authOk ? "●" : "○";
+ statusEl.className = "account-tab-status " + (authOk ? "ok" : "");
+ }
+ } catch (err) {
+ console.error(`Failed to refresh account ${accountId}:`, err);
+ }
+}
+
+async function loadAccounts() {
+ let accounts = [];
+ try {
+ const resp = await api("/api/accounts");
+ accounts = resp.accounts || [];
+ } catch {
+ // Legacy fallback — check /api/auth
+ try {
+ const legacy = await api("/api/auth");
+ if (legacy.saved_credentials?.api_id) {
+ accounts = [{ id: "default", label: "Default", auth: legacy.auth_status || legacy }];
+ }
+ } catch {
+ // No accounts at all
+ }
+ }
+
+ state.accounts = accounts;
+
+ if (accounts.length === 0) {
+ document.getElementById("no-accounts-msg").classList.remove("hidden");
return;
}
- entries.slice().reverse().forEach((entry) => {
- const row = document.createElement("div");
- row.className = `log-line ${entry.level}`;
- const time = entry.timestamp ? displayTime(entry.timestamp).split(", ").pop() : entry.legacyTime || "-";
- const absolute = entry.timestamp ? displayTime(entry.timestamp) : entry.legacyTime || "-";
- row.title = absolute;
+ document.getElementById("no-accounts-msg").classList.add("hidden");
- const timeNode = document.createElement("span");
- timeNode.className = "log-time";
- timeNode.textContent = time;
+ // Render tabs
+ renderAccountTabs(accounts);
- const ageNode = document.createElement("span");
- ageNode.className = "log-age";
- ageNode.textContent = entry.timestamp ? relativeTime(entry.timestamp) : "-";
+ // Render panels for each account
+ accounts.forEach((acc) => renderAccountPanel(acc.id));
- const levelNode = document.createElement("span");
- levelNode.className = "log-level";
- levelNode.textContent = entry.level.toUpperCase();
+ // Determine active account
+ const activeId = state.activeAccount || accounts[0].id;
+ switchAccount(activeId);
- const messageNode = document.createElement("span");
- messageNode.className = "log-message";
- messageNode.textContent = entry.message;
-
- row.append(timeNode, ageNode, levelNode, messageNode);
- root.appendChild(row);
- });
+ // Update settings
+ renderSettingsAccounts();
}
-function renderContinuous(data) {
- state.continuous = data;
- const config = data.config || {};
- const status = data.status || {};
+/* ── Settings: Accounts list ────────────────────────── */
- document.getElementById("continuous-enabled").checked = Boolean(config.enabled);
- document.getElementById("continuous-interval").value = config.interval_minutes || 1;
- document.getElementById("continuous-all").checked = Boolean(config.run_all_tracked);
- renderContinuousChannelPicker(state.dashboard?.channels || [], config);
+function renderSettingsAccounts() {
+ const container = document.getElementById("accounts-list");
+ const template = document.getElementById("account-list-item-template");
+ container.innerHTML = "";
- document.getElementById("continuous-status").textContent = status.running ? "running" : "stopped";
- document.getElementById("continuous-last-iteration").textContent = status.last_iteration_at
- ? `${relativeTime(status.last_iteration_at)} (${displayTime(status.last_iteration_at)})`
- : "-";
- document.getElementById("continuous-last-error").textContent = status.last_error || "-";
- renderContinuousLogs(status);
-}
+ state.accounts.forEach((acc) => {
+ const node = template.content.firstElementChild.cloneNode(true);
+ node.querySelector(".account-list-label").textContent = acc.label || acc.id;
+ node.querySelector(".account-list-id").textContent = acc.id;
-function setupTabs() {
- const tabs = document.querySelectorAll(".content-tab");
- const panels = document.querySelectorAll(".tab-panel");
+ const authStatus = node.querySelector(".account-list-auth-status");
+ const authOk = isAccountAuthorized(acc.auth);
+ authStatus.textContent = authOk ? "Authorized" : "Needs auth";
+ authStatus.style.color = authOk ? "var(--ok)" : "var(--danger)";
+ authStatus.style.fontSize = "0.78rem";
- tabs.forEach((tab) => {
- tab.addEventListener("click", () => {
- const targetId = tab.dataset.tabTarget;
- tabs.forEach((item) => item.classList.toggle("active", item === tab));
- panels.forEach((panel) => panel.classList.toggle("active", panel.id === targetId));
+ node.querySelector(".account-select-btn").addEventListener("click", () => {
+ switchAccount(acc.id);
+ document.getElementById("settings-dialog").close();
});
+
+ node.querySelector(".account-remove-btn").addEventListener("click", async () => {
+ if (!confirmAction(`Remove account "${acc.label || acc.id}"? All its data will be deleted.`)) return;
+ try {
+ await api(`/api/accounts/${acc.id}`, { method: "DELETE" });
+ await loadAccounts();
+ if (state.activeAccount === acc.id) {
+ state.activeAccount = null;
+ }
+ } catch (err) {
+ alert(`Failed to remove account: ${err.message}`);
+ }
+ });
+
+ container.appendChild(node);
});
}
-async function refreshDashboard() {
- const data = await api("/api/dashboard");
- const authData = await api("/api/auth");
- const continuousData = await api("/api/continuous");
- state.dashboard = data;
- renderAuthPanel(authData);
- renderSummary(data);
- renderChannels(data.channels);
- renderJobs(data.jobs);
- renderContinuous(continuousData);
+/* ── Auth (operates on active account) ───────────────── */
+
+function updateAuthSection(accountId) {
+ const label = document.getElementById("auth-account-label");
+ const acc = state.accounts.find((a) => a.id === accountId);
+ label.textContent = acc ? (acc.label || acc.id) : "-";
}
+async function saveCredentials(accountId) {
+ const apiId = document.getElementById("api-id-input").value.trim();
+ const apiHash = document.getElementById("api-hash-input").value.trim();
+ if (!apiId || !apiHash) return;
+ await api(`/api/accounts/${accountId}/auth/credentials`, {
+ method: "POST",
+ body: JSON.stringify({ api_id: apiId, api_hash: apiHash }),
+ });
+ await loadAccounts();
+}
+
+async function startQrLogin(accountId) {
+ const data = await api(`/api/accounts/${accountId}/auth/qr/start`, {
+ method: "POST",
+ body: JSON.stringify({}),
+ });
+ if (data.qr_image) {
+ document.getElementById("qr-image").src = data.qr_image;
+ document.getElementById("qr-wrap").classList.remove("hidden");
+ }
+ await loadAccounts();
+}
+
+async function requestPhoneCode(accountId) {
+ const phone = document.getElementById("phone-input").value.trim();
+ if (!phone) return;
+ await api(`/api/accounts/${accountId}/auth/phone/request`, {
+ method: "POST",
+ body: JSON.stringify({ phone }),
+ });
+ await loadAccounts();
+}
+
+async function submitPhoneCode(accountId) {
+ const code = document.getElementById("code-input").value.trim();
+ if (!code) return;
+ await api(`/api/accounts/${accountId}/auth/phone/submit`, {
+ method: "POST",
+ body: JSON.stringify({ code }),
+ });
+ document.getElementById("code-input").value = "";
+ await loadAccounts();
+}
+
+async function submitPassword(accountId) {
+ const password = document.getElementById("password-input").value.trim();
+ if (!password) return;
+ await api(`/api/accounts/${accountId}/auth/password`, {
+ method: "POST",
+ body: JSON.stringify({ password }),
+ });
+ document.getElementById("password-input").value = "";
+ await loadAccounts();
+}
+
+/* ── Scrape / Export all (for active account) ───────── */
+
+async function scrapeAll() {
+ if (!state.activeAccount) return;
+ if (!confirmAction("Queue scraping for all tracked channels?")) return;
+ await api(`/api/accounts/${state.activeAccount}/jobs/scrape`, {
+ method: "POST",
+ body: JSON.stringify({}),
+ });
+ await refreshAccount(state.activeAccount);
+}
+
+async function exportAll() {
+ if (!state.activeAccount) return;
+ if (!confirmAction("Queue export for all tracked channels?")) return;
+ await api(`/api/accounts/${state.activeAccount}/jobs/export`, {
+ method: "POST",
+ body: JSON.stringify({}),
+ });
+ await refreshAccount(state.activeAccount);
+}
+
+async function refreshDialogs() {
+ if (!state.activeAccount) return;
+ await api(`/api/accounts/${state.activeAccount}/jobs/refresh-dialogs`, {
+ method: "POST",
+ body: JSON.stringify({}),
+ });
+ await refreshAccount(state.activeAccount);
+}
+
+async function toggleMedia(value) {
+ if (!state.activeAccount) return;
+ await api(`/api/accounts/${state.activeAccount}/settings/media`, {
+ method: "POST",
+ body: JSON.stringify({ value }),
+ });
+ await refreshAccount(state.activeAccount);
+}
+
+/* ── Main ───────────────────────────────────────────── */
+
async function main() {
- setupTabs();
+ // ── Sidebar action buttons ──
+ document.getElementById("scrape-all-btn").addEventListener("click", async () => {
+ try { await scrapeAll(); } catch (err) { alert("Scrape error: " + err.message); }
+ });
+ document.getElementById("export-all-btn").addEventListener("click", async () => {
+ try { await exportAll(); } catch (err) { alert("Export error: " + err.message); }
+ });
+ document.getElementById("refresh-dialogs-btn").addEventListener("click", async () => {
+ try { await refreshDialogs(); } catch (err) { alert("Dialog refresh error: " + err.message); }
+ });
+ // ── Settings dialog ──
const settingsDialog = document.getElementById("settings-dialog");
document.getElementById("open-settings-btn").addEventListener("click", () => {
+ // Update auth section for active account
+ if (state.activeAccount) {
+ updateAuthSection(state.activeAccount);
+ }
+ renderSettingsAccounts();
settingsDialog.showModal();
});
document.getElementById("close-settings-btn").addEventListener("click", () => {
settingsDialog.close();
});
- document.getElementById("scrape-all-btn").addEventListener("click", async (event) => {
- if (!confirmAction("Queue scraping for all tracked channels?")) return;
- setBusy(event.currentTarget, true);
- try {
- await api("/api/jobs/scrape", { method: "POST", body: JSON.stringify({}) });
- await refreshDashboard();
- } finally {
- setBusy(event.currentTarget, false);
- }
- });
-
- document.getElementById("export-all-btn").addEventListener("click", async (event) => {
- if (!confirmAction("Queue export for all tracked channels?")) return;
- setBusy(event.currentTarget, true);
- try {
- await api("/api/jobs/export", { method: "POST", body: JSON.stringify({}) });
- await refreshDashboard();
- } finally {
- setBusy(event.currentTarget, false);
- }
- });
-
- document.getElementById("refresh-dialogs-btn").addEventListener("click", async (event) => {
- setBusy(event.currentTarget, true);
- try {
- await api("/api/jobs/refresh-dialogs", { method: "POST", body: JSON.stringify({}) });
- await refreshDashboard();
- } finally {
- setBusy(event.currentTarget, false);
- }
- });
-
- document.getElementById("refresh-jobs-btn").addEventListener("click", refreshDashboard);
-
- document.getElementById("scrape-media-toggle").addEventListener("change", async (event) => {
- const checked = event.currentTarget.checked;
- document.getElementById("scrape-media-label").textContent = checked ? "ON" : "OFF";
- await api("/api/settings/media", {
- method: "POST",
- body: JSON.stringify({ value: checked }),
- });
- await refreshDashboard();
- });
-
- document.getElementById("add-channel-form").addEventListener("submit", async (event) => {
+ // ── Add account form ──
+ document.getElementById("add-account-form").addEventListener("submit", async (event) => {
event.preventDefault();
- const channelId = document.getElementById("add-channel-id").value.trim();
- const name = document.getElementById("add-channel-name").value.trim();
- if (!channelId) return;
- await api("/api/channels/add", {
- method: "POST",
- body: JSON.stringify({ channel_id: channelId, name }),
- });
- event.currentTarget.reset();
- await refreshDashboard();
+ const accountId = document.getElementById("add-account-id").value.trim();
+ const label = document.getElementById("add-account-label").value.trim();
+ const apiId = document.getElementById("add-account-api-id").value.trim();
+ const apiHash = document.getElementById("add-account-api-hash").value.trim();
+ if (!accountId) return;
+
+ try {
+ 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.target.reset();
+ await loadAccounts();
+ if (!state.activeAccount) {
+ switchAccount(accountId);
+ }
+ } catch (err) {
+ alert("Failed to add account: " + err.message);
+ }
});
+ // ── Credentials form ──
document.getElementById("credentials-form").addEventListener("submit", async (event) => {
event.preventDefault();
- const apiId = document.getElementById("api-id-input").value.trim();
- const apiHash = document.getElementById("api-hash-input").value.trim();
- await api("/api/auth/credentials", {
- method: "POST",
- body: JSON.stringify({ api_id: apiId, api_hash: apiHash }),
- });
- await refreshDashboard();
+ if (!state.activeAccount) return;
+ try {
+ await saveCredentials(state.activeAccount);
+ } catch (err) {
+ alert("Failed to save credentials: " + err.message);
+ }
});
+ // ── QR login ──
document.getElementById("start-qr-btn").addEventListener("click", async (event) => {
setBusy(event.currentTarget, true);
try {
- await api("/api/auth/qr/start", {
- method: "POST",
- body: JSON.stringify({}),
- });
- await refreshDashboard();
+ if (!state.activeAccount) return;
+ await startQrLogin(state.activeAccount);
+ } catch (err) {
+ alert("QR login error: " + err.message);
} finally {
setBusy(event.currentTarget, false);
}
});
+ // ── Phone ──
document.getElementById("phone-form").addEventListener("submit", async (event) => {
event.preventDefault();
- const phone = document.getElementById("phone-input").value.trim();
- await api("/api/auth/phone/request", {
- method: "POST",
- body: JSON.stringify({ phone }),
- });
- await refreshDashboard();
+ if (!state.activeAccount) return;
+ try {
+ await requestPhoneCode(state.activeAccount);
+ } catch (err) {
+ alert("Phone code request error: " + err.message);
+ }
});
+ // ── Code ──
document.getElementById("code-form").addEventListener("submit", async (event) => {
event.preventDefault();
- const code = document.getElementById("code-input").value.trim();
- await api("/api/auth/phone/submit", {
- method: "POST",
- body: JSON.stringify({ code }),
- });
- event.currentTarget.reset();
- await refreshDashboard();
+ if (!state.activeAccount) return;
+ try {
+ await submitPhoneCode(state.activeAccount);
+ } catch (err) {
+ alert("Code submit error: " + err.message);
+ }
});
+ // ── Password ──
document.getElementById("password-form").addEventListener("submit", async (event) => {
event.preventDefault();
- const password = document.getElementById("password-input").value.trim();
- await api("/api/auth/password", {
- method: "POST",
- body: JSON.stringify({ password }),
- });
- event.currentTarget.reset();
- await refreshDashboard();
- });
-
- document.getElementById("continuous-form").addEventListener("submit", async (event) => {
- event.preventDefault();
- const enabled = document.getElementById("continuous-enabled").checked;
- const intervalMinutes = document.getElementById("continuous-interval").value;
- const runAllTracked = document.getElementById("continuous-all").checked;
- const channels = Array.from(document.querySelectorAll(".continuous-channel-checkbox:checked")).map(
- (item) => item.value,
- );
- if (!runAllTracked && channels.length === 0 && enabled) {
- if (!confirmAction("Continuous scraping is enabled with no selected channels. Save anyway?")) return;
+ if (!state.activeAccount) return;
+ try {
+ await submitPassword(state.activeAccount);
+ } catch (err) {
+ alert("Password error: " + err.message);
}
- await api("/api/continuous", {
- method: "POST",
- body: JSON.stringify({
- enabled,
- interval_minutes: intervalMinutes,
- run_all_tracked: runAllTracked,
- channels,
- }),
- });
- await refreshDashboard();
});
- document.getElementById("continuous-all").addEventListener("change", () => {
- renderContinuousChannelPicker(state.dashboard?.channels || [], {
- ...(state.continuous?.config || {}),
- run_all_tracked: document.getElementById("continuous-all").checked,
- });
+ // ── Media toggle ──
+ document.getElementById("scrape-media-toggle").addEventListener("change", async (event) => {
+ const checked = event.currentTarget.checked;
+ try { await toggleMedia(checked); } catch (err) { alert("Media toggle error: " + err.message); }
});
- await refreshDashboard();
- window.setInterval(refreshDashboard, 5000);
+ // ── Load accounts ──
+ await loadAccounts();
+
+ // ── Periodic refresh ──
+ window.setInterval(() => {
+ if (state.activeAccount) {
+ refreshAccount(state.activeAccount);
+ }
+ }, 5000);
}
main().catch((error) => {
diff --git a/webui/index.html b/webui/index.html
index faea273..f5be6ac 100644
--- a/webui/index.html
+++ b/webui/index.html
@@ -4,7 +4,7 @@
Telegram Scraper Control Panel
-
+
@@ -22,10 +22,10 @@
Health
-
- Telegram Status
- Checking...
-
+
+ Active Account
+ Loading...
+
@@ -38,127 +38,54 @@
-
-
-
+
+
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Channel |
+ Messages |
+ Media |
+ Last message |
+ Actions |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
@@ -224,6 +278,7 @@
|
+
@@ -235,6 +290,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+