469 lines
16 KiB
JavaScript
469 lines
16 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 relativeTime(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return "-";
|
|
const seconds = Math.max(0, Math.floor((Date.now() - date.getTime()) / 1000));
|
|
if (seconds < 10) return "now";
|
|
if (seconds < 60) return `${seconds}s ago`;
|
|
const minutes = Math.floor(seconds / 60);
|
|
if (minutes < 60) return `${minutes}m ago`;
|
|
const hours = Math.floor(minutes / 60);
|
|
if (hours < 24) return `${hours}h ago`;
|
|
return `${Math.floor(hours / 24)}d ago`;
|
|
}
|
|
|
|
function displayTime(value) {
|
|
if (!value) return "-";
|
|
const date = new Date(value);
|
|
if (Number.isNaN(date.getTime())) return formatDate(value);
|
|
return date.toLocaleString(undefined, {
|
|
year: "numeric",
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
second: "2-digit",
|
|
});
|
|
}
|
|
|
|
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 saved"
|
|
: "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">No jobs yet.</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 = [displayTime(job.started_at), displayTime(job.finished_at)]
|
|
.filter((item) => item !== "-")
|
|
.join(" -> ");
|
|
node.querySelector(".job-logs").textContent = job.logs || job.error || "No logs.";
|
|
root.appendChild(node);
|
|
});
|
|
}
|
|
|
|
function renderContinuousChannelPicker(channels, config) {
|
|
const root = document.getElementById("continuous-channel-list");
|
|
const runAll = Boolean(config.run_all_tracked);
|
|
const selected = new Set(config.channels || []);
|
|
root.innerHTML = "";
|
|
|
|
if (!channels.length) {
|
|
root.innerHTML = '<div class="muted">No tracked channels.</div>';
|
|
return;
|
|
}
|
|
|
|
channels.forEach((channel) => {
|
|
const label = document.createElement("label");
|
|
label.className = "checkbox-row";
|
|
label.classList.toggle("disabled", runAll);
|
|
|
|
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.disabled = runAll;
|
|
|
|
const text = document.createElement("span");
|
|
text.textContent = `${channel.name} (${channel.channel_id})`;
|
|
|
|
label.append(checkbox, text);
|
|
root.appendChild(label);
|
|
});
|
|
}
|
|
|
|
function classifyLog(message, level) {
|
|
if (level) return level;
|
|
const normalized = message.toLowerCase();
|
|
if (normalized.includes("failed") || normalized.includes("error") || normalized.includes("traceback")) return "error";
|
|
if (normalized.includes("sleep") || normalized.includes("no channels")) return "warn";
|
|
if (normalized.includes("finished") || normalized.includes("completed")) return "success";
|
|
if (normalized.includes("starting") || normalized.includes("started")) return "info";
|
|
return "debug";
|
|
}
|
|
|
|
function normalizeLogEntry(entry) {
|
|
if (typeof entry === "object" && entry !== null) {
|
|
return {
|
|
timestamp: entry.timestamp || null,
|
|
level: classifyLog(entry.message || "", entry.level),
|
|
message: entry.message || "",
|
|
};
|
|
}
|
|
const line = String(entry || "");
|
|
const match = line.match(/^\[(\d{2}:\d{2}:\d{2})\]\s?(.*)$/);
|
|
return {
|
|
timestamp: null,
|
|
legacyTime: match?.[1] || "",
|
|
level: classifyLog(match?.[2] || line),
|
|
message: match?.[2] || line,
|
|
};
|
|
}
|
|
|
|
function renderContinuousLogs(status) {
|
|
const root = document.getElementById("continuous-logs");
|
|
const entries = (status.log_entries || status.logs || []).map(normalizeLogEntry);
|
|
root.innerHTML = "";
|
|
|
|
if (!entries.length) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "log-line";
|
|
empty.innerHTML = '<span class="log-time">-</span><span class="log-age">-</span><span class="log-level">INFO</span><span class="log-message">No logs yet.</span>';
|
|
root.appendChild(empty);
|
|
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);
|
|
root.appendChild(row);
|
|
});
|
|
}
|
|
|
|
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);
|
|
renderContinuousChannelPicker(state.dashboard?.channels || [], config);
|
|
|
|
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);
|
|
}
|
|
|
|
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();
|
|
|
|
const settingsDialog = document.getElementById("settings-dialog");
|
|
document.getElementById("open-settings-btn").addEventListener("click", () => {
|
|
settingsDialog.showModal();
|
|
});
|
|
document.getElementById("close-settings-btn").addEventListener("click", () => {
|
|
settingsDialog.close();
|
|
});
|
|
|
|
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 channels = Array.from(document.querySelectorAll(".continuous-channel-checkbox:checked")).map(
|
|
(item) => item.value,
|
|
);
|
|
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,
|
|
});
|
|
});
|
|
|
|
await refreshDashboard();
|
|
window.setInterval(refreshDashboard, 5000);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error);
|
|
alert(error.message);
|
|
});
|