Harden account import/continuous behavior and finish multi-account UI polish
This commit is contained in:
+133
-13
@@ -11,6 +11,7 @@ const state = {
|
||||
};
|
||||
|
||||
const ACTIVE_ACCOUNT_STORAGE_KEY = "telegramScraper.activeAccount";
|
||||
const jobStreams = new Map();
|
||||
|
||||
/* ── API helper ─────────────────────────────────────── */
|
||||
|
||||
@@ -70,6 +71,38 @@ function confirmAction(message) {
|
||||
return window.confirm(message);
|
||||
}
|
||||
|
||||
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 isAccountAuthorized(auth) {
|
||||
const status = auth?.auth_status || auth || {};
|
||||
return (
|
||||
@@ -203,7 +236,15 @@ function renderAccountPanel(accountId) {
|
||||
container.appendChild(node);
|
||||
}
|
||||
|
||||
function closeAllJobStreams() {
|
||||
for (const [jobId, stream] of jobStreams) {
|
||||
stream.close();
|
||||
}
|
||||
jobStreams.clear();
|
||||
}
|
||||
|
||||
function switchAccount(accountId) {
|
||||
closeAllJobStreams();
|
||||
state.activeAccount = accountId;
|
||||
saveActiveAccount(accountId);
|
||||
|
||||
@@ -257,6 +298,12 @@ function renderChannels(accountId, channels) {
|
||||
const template = document.getElementById("channel-row-template");
|
||||
tbody.innerHTML = "";
|
||||
|
||||
if (!channels.length) {
|
||||
const row = document.createElement("tr");
|
||||
row.innerHTML = '<td colspan="5"><div class="empty-state">No tracked channels yet. Add an ID or @username to start scraping this account.</div></td>';
|
||||
tbody.appendChild(row);
|
||||
}
|
||||
|
||||
channels.forEach((channel) => {
|
||||
const node = template.content.firstElementChild.cloneNode(true);
|
||||
node.querySelector(".channel-name").textContent = channel.name;
|
||||
@@ -317,12 +364,13 @@ function renderJobs(accountId, jobs) {
|
||||
root.innerHTML = "";
|
||||
|
||||
if (!jobs.length) {
|
||||
root.innerHTML = '<div class="muted">No jobs yet.</div>';
|
||||
root.innerHTML = '<div class="empty-state">No jobs yet. Start a scrape or export to see progress here.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
jobs.forEach((job) => {
|
||||
const node = template.content.firstElementChild.cloneNode(true);
|
||||
node.dataset.jobId = job.job_id;
|
||||
node.querySelector(".job-title").textContent = job.title;
|
||||
node.querySelector(".job-status").textContent = job.status;
|
||||
node.querySelector(".job-time").textContent = [displayTime(job.started_at), displayTime(job.finished_at)]
|
||||
@@ -330,9 +378,42 @@ function renderJobs(accountId, jobs) {
|
||||
.join(" -> ");
|
||||
node.querySelector(".job-logs").textContent = job.logs || job.error || "No logs.";
|
||||
root.appendChild(node);
|
||||
subscribeJobStream(accountId, job.job_id, job.status);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRenderedJob(accountId, job) {
|
||||
const panel = document.getElementById(`panel-${accountId}`);
|
||||
const node = panel?.querySelector(`[data-job-id="${job.job_id}"]`);
|
||||
if (!node) return;
|
||||
node.querySelector(".job-status").textContent = job.status;
|
||||
node.querySelector(".job-time").textContent = [displayTime(job.started_at), displayTime(job.finished_at)]
|
||||
.filter((item) => item !== "-")
|
||||
.join(" -> ");
|
||||
node.querySelector(".job-logs").textContent = job.logs || job.error || "No logs.";
|
||||
}
|
||||
|
||||
function subscribeJobStream(accountId, jobId, status) {
|
||||
if (!["queued", "running"].includes(status) || jobStreams.has(jobId) || !window.EventSource) {
|
||||
return;
|
||||
}
|
||||
const stream = new EventSource(`/api/jobs/${encodeURIComponent(jobId)}/events`);
|
||||
jobStreams.set(jobId, stream);
|
||||
stream.onmessage = (event) => {
|
||||
const job = JSON.parse(event.data);
|
||||
updateRenderedJob(accountId, job);
|
||||
if (["completed", "failed"].includes(job.status)) {
|
||||
stream.close();
|
||||
jobStreams.delete(jobId);
|
||||
refreshAccount(accountId);
|
||||
}
|
||||
};
|
||||
stream.onerror = () => {
|
||||
stream.close();
|
||||
jobStreams.delete(jobId);
|
||||
};
|
||||
}
|
||||
|
||||
function renderContinuous(accountId, data) {
|
||||
const panel = document.getElementById(`panel-${accountId}`);
|
||||
if (!panel) return;
|
||||
@@ -447,9 +528,18 @@ function renderSummary(accountId, data) {
|
||||
const panel = document.getElementById(`panel-${accountId}`);
|
||||
if (!panel) return;
|
||||
const d = data.dashboard || data.state || {};
|
||||
const health = data.health || {};
|
||||
panel.querySelector(".forwarding-count").textContent = String((d.forwarding_rules || []).length);
|
||||
const toggle = panel.querySelector(".scrape-media-label");
|
||||
toggle.textContent = d.scrape_media ? "ON" : "OFF";
|
||||
const healthOk = health.api_credentials && health.session_ready && health.data_dir_exists;
|
||||
panel.querySelector(".account-health-label").textContent = healthOk ? "Ready" : "Check";
|
||||
panel.querySelector(".account-health-label").style.color = healthOk ? "var(--ok)" : "var(--warn)";
|
||||
panel.querySelector(".account-health-detail").textContent = [
|
||||
`${health.message_count || 0} messages`,
|
||||
`${health.media_count || 0} media`,
|
||||
health.active_job ? `active: ${health.active_job.status}` : "idle",
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
/* ── Account data loading ───────────────────────────── */
|
||||
@@ -550,6 +640,16 @@ function renderSettingsAccounts() {
|
||||
document.getElementById("settings-dialog").close();
|
||||
});
|
||||
|
||||
node.querySelector(".account-export-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
const payload = await api(`/api/accounts/${acc.id}/export`);
|
||||
downloadJson(`telegram-scraper-account-${acc.id}.json`, payload);
|
||||
showToast(`Exported ${acc.label || acc.id}.`, "success");
|
||||
} catch (err) {
|
||||
showToast(`Failed to export account: ${err.message}`, "error");
|
||||
}
|
||||
});
|
||||
|
||||
node.querySelector(".account-remove-btn").addEventListener("click", async () => {
|
||||
if (!confirmAction(`Remove account "${acc.label || acc.id}"? All its data will be deleted.`)) return;
|
||||
try {
|
||||
@@ -561,7 +661,7 @@ function renderSettingsAccounts() {
|
||||
}
|
||||
await loadAccounts();
|
||||
} catch (err) {
|
||||
alert(`Failed to remove account: ${err.message}`);
|
||||
showToast(`Failed to remove account: ${err.message}`, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -677,13 +777,13 @@ async function toggleMedia(value) {
|
||||
async function main() {
|
||||
// ── Sidebar action buttons ──
|
||||
document.getElementById("scrape-all-btn").addEventListener("click", async () => {
|
||||
try { await scrapeAll(); } catch (err) { alert("Scrape error: " + err.message); }
|
||||
try { await scrapeAll(); showToast("Scrape job queued.", "success"); } catch (err) { showToast("Scrape error: " + err.message, "error"); }
|
||||
});
|
||||
document.getElementById("export-all-btn").addEventListener("click", async () => {
|
||||
try { await exportAll(); } catch (err) { alert("Export error: " + err.message); }
|
||||
try { await exportAll(); showToast("Export job queued.", "success"); } catch (err) { showToast("Export error: " + err.message, "error"); }
|
||||
});
|
||||
document.getElementById("refresh-dialogs-btn").addEventListener("click", async () => {
|
||||
try { await refreshDialogs(); } catch (err) { alert("Dialog refresh error: " + err.message); }
|
||||
try { await refreshDialogs(); showToast("Dialog refresh queued.", "success"); } catch (err) { showToast("Dialog refresh error: " + err.message, "error"); }
|
||||
});
|
||||
|
||||
// ── Settings dialog ──
|
||||
@@ -725,7 +825,27 @@ async function main() {
|
||||
switchAccount(accountId);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Failed to add account: " + err.message);
|
||||
showToast("Failed to add account: " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("import-account-file").addEventListener("change", async (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();
|
||||
switchAccount(accountId);
|
||||
showToast(`Imported ${accountId}.`, "success");
|
||||
} catch (err) {
|
||||
showToast(`Failed to import account: ${err.message}`, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -736,7 +856,7 @@ async function main() {
|
||||
try {
|
||||
await saveCredentials(state.activeAccount);
|
||||
} catch (err) {
|
||||
alert("Failed to save credentials: " + err.message);
|
||||
showToast("Failed to save credentials: " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -747,7 +867,7 @@ async function main() {
|
||||
if (!state.activeAccount) return;
|
||||
await startQrLogin(state.activeAccount);
|
||||
} catch (err) {
|
||||
alert("QR login error: " + err.message);
|
||||
showToast("QR login error: " + err.message, "error");
|
||||
} finally {
|
||||
setBusy(event.currentTarget, false);
|
||||
}
|
||||
@@ -760,7 +880,7 @@ async function main() {
|
||||
try {
|
||||
await requestPhoneCode(state.activeAccount);
|
||||
} catch (err) {
|
||||
alert("Phone code request error: " + err.message);
|
||||
showToast("Phone code request error: " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -771,7 +891,7 @@ async function main() {
|
||||
try {
|
||||
await submitPhoneCode(state.activeAccount);
|
||||
} catch (err) {
|
||||
alert("Code submit error: " + err.message);
|
||||
showToast("Code submit error: " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -782,14 +902,14 @@ async function main() {
|
||||
try {
|
||||
await submitPassword(state.activeAccount);
|
||||
} catch (err) {
|
||||
alert("Password error: " + err.message);
|
||||
showToast("Password error: " + err.message, "error");
|
||||
}
|
||||
});
|
||||
|
||||
// ── 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); }
|
||||
try { await toggleMedia(checked); showToast("Media setting updated.", "success"); } catch (err) { showToast("Media toggle error: " + err.message, "error"); }
|
||||
});
|
||||
|
||||
// ── Load accounts ──
|
||||
@@ -805,5 +925,5 @@ async function main() {
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
alert(error.message);
|
||||
showToast(error.message, "error");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user