Files
telegram-scraper/webui/app.js
T

321 lines
11 KiB
JavaScript

const state = {
dashboard: null,
auth: null,
continuous: null,
};
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 formatDate(value) {
if (!value) return "—";
return value.replace("T", " ").replace("+00:00", " UTC");
}
function setBusy(button, busy) {
if (!button) return;
button.disabled = busy;
}
function renderAuth(auth) {
document.getElementById("auth-status").textContent = auth.status;
document.getElementById("auth-details").textContent = auth.details || "";
}
function toggleHidden(id, hidden) {
document.getElementById(id).classList.toggle("hidden", hidden);
}
function renderAuthPanel(authData) {
state.auth = authData;
renderAuth(authData.auth_status);
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 сохранён"
: "API Hash";
const showQr = Boolean(authData.qr_image);
toggleHidden("qr-wrap", !showQr);
if (showQr) {
document.getElementById("qr-image").src = authData.qr_image;
}
toggleHidden("code-form", authData.phase !== "code_required");
toggleHidden("password-form", authData.phase !== "password_required");
}
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 renderChannels(channels) {
const tbody = document.getElementById("channels-table");
tbody.innerHTML = "";
const template = document.getElementById("channel-row-template");
channels.forEach((channel) => {
const node = template.content.firstElementChild.cloneNode(true);
node.querySelector(".channel-name").textContent = channel.name;
node.querySelector(".channel-id").textContent = channel.channel_id;
node.querySelector(".message-count").textContent = String(channel.message_count);
node.querySelector(".media-count").textContent = String(channel.media_count);
node.querySelector(".last-date").textContent = channel.last_date || "—";
node.querySelector(".scrape-btn").addEventListener("click", async () => {
await api("/api/jobs/scrape", {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
await refreshDashboard();
});
node.querySelector(".export-btn").addEventListener("click", async () => {
await api("/api/jobs/export-channel", {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
await refreshDashboard();
});
node.querySelector(".export-view-btn").addEventListener("click", () => {
window.location.href = `/viewer?channel=${encodeURIComponent(channel.channel_id)}`;
});
node.querySelector(".media-btn").addEventListener("click", async () => {
await api("/api/jobs/rescrape-media", {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
await refreshDashboard();
});
node.querySelector(".remove-btn").addEventListener("click", async () => {
await api("/api/channels/remove", {
method: "POST",
body: JSON.stringify({ channel_id: channel.channel_id }),
});
await refreshDashboard();
});
tbody.appendChild(node);
});
}
function renderJobs(jobs) {
const root = document.getElementById("jobs-list");
root.innerHTML = "";
const template = document.getElementById("job-template");
if (!jobs.length) {
root.innerHTML = '<div class="muted">Пока задач нет.</div>';
return;
}
jobs.forEach((job) => {
const node = template.content.firstElementChild.cloneNode(true);
node.querySelector(".job-title").textContent = job.title;
node.querySelector(".job-status").textContent = job.status;
node.querySelector(".job-time").textContent = [job.started_at, job.finished_at].filter(Boolean).join(" -> ");
node.querySelector(".job-logs").textContent = job.logs || job.error || "Без логов";
root.appendChild(node);
});
}
function renderContinuous(data) {
state.continuous = data;
const config = data.config || {};
const status = data.status || {};
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);
document.getElementById("continuous-channels").value = (config.channels || []).join(", ");
document.getElementById("continuous-status").textContent = status.running ? "running" : "stopped";
document.getElementById("continuous-last-iteration").textContent = status.last_iteration_at || "—";
document.getElementById("continuous-last-error").textContent = status.last_error || "—";
document.getElementById("continuous-logs").textContent = (status.logs || []).join("\n") || "Логов пока нет.";
}
function setupTabs() {
const tabs = document.querySelectorAll(".content-tab");
const panels = document.querySelectorAll(".tab-panel");
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));
});
});
}
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);
}
async function main() {
setupTabs();
document.getElementById("scrape-all-btn").addEventListener("click", async (event) => {
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) => {
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) => {
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();
});
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();
});
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();
} finally {
setBusy(event.currentTarget, false);
}
});
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();
});
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();
});
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 channelsRaw = document.getElementById("continuous-channels").value.trim();
const channels = channelsRaw ? channelsRaw.split(",").map((item) => item.trim()).filter(Boolean) : [];
await api("/api/continuous", {
method: "POST",
body: JSON.stringify({
enabled,
interval_minutes: intervalMinutes,
run_all_tracked: runAllTracked,
channels,
}),
});
await refreshDashboard();
});
await refreshDashboard();
window.setInterval(refreshDashboard, 5000);
}
main().catch((error) => {
console.error(error);
alert(error.message);
});