"use strict"; const $ = (sel) => document.querySelector(sel); const $$ = (sel) => Array.from(document.querySelectorAll(sel)); async function api(path, opts = {}) { const res = await fetch(path, { headers: { "Content-Type": "application/json" }, ...opts, }); let data = null; try { data = await res.json(); } catch (_) { /* no body */ } if (!res.ok) { const msg = (data && (data.error || data.message)) || res.statusText; throw new Error(msg); } return data; } function toast(message, kind = "") { const el = $("#toast"); el.textContent = message; el.className = "toast " + kind; setTimeout(() => el.classList.add("hidden"), 3000); } function fmtTime(iso) { if (!iso) return "—"; try { return new Date(iso).toLocaleString(locale()); } catch (_) { return iso; } } function fmtTs(ts) { if (!ts) return "—"; return new Date(ts * 1000).toLocaleString(locale()); } function escapeHtml(s) { return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); } /* ---------------- Language ---------------- */ const langSel = $("#lang-select"); langSel.value = LANG; langSel.addEventListener("change", async () => { setLang(langSel.value); refreshStatus(); refreshLogs(); if ($("#tab-feeds").classList.contains("active")) loadFeeds(); try { await api("/api/settings", { method: "PUT", body: JSON.stringify({ language: LANG }) }); } catch (_) { /* ignore */ } }); /* ---------------- Tabs ---------------- */ $$(".tab").forEach((tab) => { tab.addEventListener("click", () => { $$(".tab").forEach((t) => t.classList.remove("active")); $$(".tab-panel").forEach((p) => p.classList.remove("active")); tab.classList.add("active"); $("#tab-" + tab.dataset.tab).classList.add("active"); if (tab.dataset.tab === "feeds") loadFeeds(); if (tab.dataset.tab === "settings") loadSettings(); }); }); /* ---------------- Status / dashboard ---------------- */ async function refreshStatus() { try { const s = await api("/api/status"); $("#stat-feeds").textContent = `${s.feed_active}/${s.feed_total}`; $("#stat-topics").textContent = s.topics.length; $("#stat-sent").textContent = s.sent_total; $("#stat-history").textContent = s.history_count; const enabled = s.enabled; const pill = $("#engine-pill"); if (s.syncing) { pill.textContent = t("pill_syncing"); pill.className = "pill pill-on"; } else if (enabled) { pill.textContent = t("pill_running"); pill.className = "pill pill-on"; } else { pill.textContent = t("pill_paused"); pill.className = "pill pill-off"; } $("#btn-toggle").textContent = enabled ? t("pause") : t("resume"); $("#dash-engine").textContent = s.syncing ? t("st_syncing") : (enabled ? t("st_running") : t("st_paused")); $("#dash-last").textContent = fmtTime(s.last_sync) + (s.last_sync_ok === false ? t("err_suffix_sync_failed") : ""); $("#dash-next").textContent = enabled ? fmtTs(s.next_sync) : "—"; $("#dash-interval").textContent = s.sync_interval + " " + t("sec"); $("#dash-error").textContent = s.last_error || "—"; } catch (e) { $("#engine-pill").textContent = t("no_connection"); } } async function refreshLogs() { try { const { logs } = await api("/api/logs"); const box = $("#log"); box.innerHTML = logs.map((l) => { const time = new Date(l.time).toLocaleTimeString(locale()); return `
${time} ${escapeHtml(l.message)}
`; }).join(""); } catch (_) { /* ignore */ } } /* ---------------- Engine controls ---------------- */ $("#btn-sync").addEventListener("click", async () => { try { await api("/api/sync", { method: "POST" }); toast(t("sync_started"), "ok"); } catch (e) { toast(e.message, "err"); } setTimeout(refreshStatus, 500); setTimeout(refreshLogs, 1500); }); $("#btn-toggle").addEventListener("click", async () => { const s = await api("/api/status"); const action = s.enabled ? "pause" : "resume"; await api("/api/engine", { method: "POST", body: JSON.stringify({ action }) }); refreshStatus(); }); $("#btn-clear-history").addEventListener("click", async () => { if (!confirm(t("confirm_clear_history"))) return; await api("/api/history/clear", { method: "POST" }); toast(t("history_cleared"), "ok"); refreshStatus(); }); $("#btn-test").addEventListener("click", async () => { const topic = $("#test-topic").value.trim(); const message = $("#test-msg").value.trim(); if (!topic) { toast(t("need_topic"), "err"); return; } try { await api("/api/test-notify", { method: "POST", body: JSON.stringify({ topic, message }) }); toast(t("notify_sent"), "ok"); } catch (e) { toast(t("err_prefix") + e.message, "err"); } }); /* ---------------- OPML import / export ---------------- */ $("#btn-export-opml").addEventListener("click", () => { window.location.href = "/api/export/opml"; }); $("#btn-import-opml").addEventListener("click", () => $("#opml-file").click()); $("#opml-file").addEventListener("change", async (e) => { const file = e.target.files[0]; if (!file) return; const form = new FormData(); form.append("file", file); try { const res = await fetch("/api/import/opml", { method: "POST", body: form }); const data = await res.json(); if (!res.ok || !data.ok) throw new Error(data.error || res.statusText); toast(t("import_done").replace("{n}", data.added).replace("{total}", data.total), "ok"); loadFeeds(); refreshStatus(); } catch (err) { toast(t("err_prefix") + err.message, "err"); } finally { e.target.value = ""; } }); /* ---------------- Feeds ---------------- */ async function loadFeeds() { const { feeds } = await api("/api/feeds"); const list = $("#feeds-list"); if (!feeds.length) { list.innerHTML = `
${escapeHtml(t("feeds_empty"))}
`; return; } list.innerHTML = feeds.map(feedRow).join(""); $$(".feed-toggle").forEach((el) => el.addEventListener("change", onToggleFeed)); $$(".feed-edit").forEach((el) => el.addEventListener("click", onEditFeed)); $$(".feed-del").forEach((el) => el.addEventListener("click", onDeleteFeed)); } function feedRow(f) { const badges = [`→ ${escapeHtml(f.topic)}`, `${t("priority")} ${f.priority}`]; if (f.quiet_hours) badges.push(`${t("quiet")} ${escapeHtml(f.quiet_hours)}`); if (f.include_regex) badges.push(`include`); if (f.exclude_regex) badges.push(`exclude`); return `
${escapeHtml(f.name || t("no_name"))}
${escapeHtml(f.url)}
${badges.join("")}
`; } async function onToggleFeed(e) { const id = e.target.dataset.id; await api(`/api/feeds/${id}`, { method: "PUT", body: JSON.stringify({ enabled: e.target.checked }) }); toast(t("saved"), "ok"); refreshStatus(); } async function onEditFeed(e) { const id = e.target.dataset.id; const { feeds } = await api("/api/feeds"); const feed = feeds.find((f) => f.id === id); openModal(feed); } async function onDeleteFeed(e) { const id = e.target.dataset.id; if (!confirm(t("confirm_delete_feed"))) return; await api(`/api/feeds/${id}`, { method: "DELETE" }); toast(t("feed_deleted"), "ok"); loadFeeds(); refreshStatus(); } /* ---------------- Feed modal ---------------- */ const modal = $("#modal"); const feedForm = $("#feed-form"); function openModal(feed) { $("#modal-title").textContent = feed ? t("feed_edit") : t("feed_new"); $("#preview-result").innerHTML = ""; feedForm.reset(); const data = feed || { priority: 3, quiet_priority: 1, enabled: true }; for (const [k, v] of Object.entries(data)) { const el = feedForm.elements[k]; if (!el) continue; if (el.type === "checkbox") el.checked = !!v; else el.value = v ?? ""; } if (!feed) feedForm.elements["enabled"].checked = true; modal.classList.remove("hidden"); } function closeModal() { modal.classList.add("hidden"); } $("#btn-add-feed").addEventListener("click", () => openModal(null)); $("#modal-close").addEventListener("click", closeModal); modal.addEventListener("click", (e) => { if (e.target === modal) closeModal(); }); feedForm.addEventListener("submit", async (e) => { e.preventDefault(); const payload = collectForm(feedForm); const id = payload.id; delete payload.id; try { if (id) await api(`/api/feeds/${id}`, { method: "PUT", body: JSON.stringify(payload) }); else await api("/api/feeds", { method: "POST", body: JSON.stringify(payload) }); toast(t("feed_saved"), "ok"); closeModal(); loadFeeds(); refreshStatus(); } catch (err) { toast(t("err_prefix") + err.message, "err"); } }); $("#btn-preview").addEventListener("click", async () => { const url = feedForm.elements["url"].value.trim(); const box = $("#preview-result"); if (!url) { box.innerHTML = `${escapeHtml(t("need_url"))}`; return; } box.textContent = t("checking"); try { const r = await api("/api/feeds/preview", { method: "POST", body: JSON.stringify({ url }) }); if (r.error && !r.entries.length) { box.innerHTML = `${escapeHtml(t("err_prefix") + r.error)}`; return; } const items = r.entries.map((it) => `
  • ${escapeHtml(it.title)}
  • `).join(""); box.innerHTML = `${escapeHtml(t("preview_ok"))} «${escapeHtml(r.title)}», ` + `${escapeHtml(t("preview_entries"))} ${r.count}`; } catch (err) { box.innerHTML = `${escapeHtml(t("err_prefix") + err.message)}`; } }); function collectForm(form) { const out = {}; for (const el of form.elements) { if (!el.name) continue; if (el.type === "checkbox") out[el.name] = el.checked; else if (el.type === "number") out[el.name] = el.value === "" ? null : Number(el.value); else out[el.name] = el.value; } return out; } /* ---------------- Settings ---------------- */ async function loadSettings() { const s = await api("/api/settings"); const form = $("#settings-form"); for (const [k, v] of Object.entries(s)) { const el = form.elements[k]; if (!el) continue; if (el.type === "checkbox") el.checked = !!v; else el.value = v ?? ""; } } $("#settings-form").addEventListener("submit", async (e) => { e.preventDefault(); const payload = collectForm(e.target); try { await api("/api/settings", { method: "PUT", body: JSON.stringify(payload) }); toast(t("saved"), "ok"); refreshStatus(); } catch (err) { toast(t("err_prefix") + err.message, "err"); } }); /* ---------------- Init ---------------- */ (async () => { try { const s = await api("/api/settings"); if (s.language && s.language !== LANG) { setLang(s.language); langSel.value = s.language; } } catch (_) { /* ignore */ } refreshStatus(); refreshLogs(); })(); setInterval(refreshStatus, 5000); setInterval(refreshLogs, 5000);