Improve webui style and add api docs

This commit is contained in:
2026-05-24 22:18:41 +02:00
parent 6851922a9e
commit a78a708757
8 changed files with 1144 additions and 415 deletions
+160 -12
View File
@@ -17,10 +17,38 @@ async function api(path, options = {}) {
}
function formatDate(value) {
if (!value) return "";
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;
@@ -42,7 +70,7 @@ function renderAuthPanel(authData) {
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 saved"
: "API Hash";
const showQr = Boolean(authData.qr_image);
@@ -74,7 +102,7 @@ function renderChannels(channels) {
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(".last-date").textContent = channel.last_date || "-";
node.querySelector(".scrape-btn").addEventListener("click", async () => {
await api("/api/jobs/scrape", {
@@ -122,7 +150,7 @@ function renderJobs(jobs) {
const template = document.getElementById("job-template");
if (!jobs.length) {
root.innerHTML = '<div class="muted">Пока задач нет.</div>';
root.innerHTML = '<div class="muted">No jobs yet.</div>';
return;
}
@@ -130,12 +158,114 @@ function renderJobs(jobs) {
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 || "Без логов";
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 || {};
@@ -144,12 +274,14 @@ function renderContinuous(data) {
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(", ");
renderContinuousChannelPicker(state.dashboard?.channels || [], config);
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") || "Логов пока нет.";
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() {
@@ -180,6 +312,14 @@ async function refreshDashboard() {
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 {
@@ -296,8 +436,9 @@ async function main() {
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) : [];
const channels = Array.from(document.querySelectorAll(".continuous-channel-checkbox:checked")).map(
(item) => item.value,
);
await api("/api/continuous", {
method: "POST",
body: JSON.stringify({
@@ -310,6 +451,13 @@ async function main() {
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);
}