| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312 |
- "use strict";
- // --- state -----------------------------------------------------------------
- let CONFIG = null;
- let messages = []; // current folder's messages
- const selected = new Set(); // selected localguids
- let jobPollTimer = null;
- // --- element refs ----------------------------------------------------------
- const $ = (id) => document.getElementById(id);
- const folderSelect = $("folder-select");
- const limitInput = $("limit-input");
- const msgBody = $("msg-body");
- const selectAll = $("select-all");
- const emptyNote = $("empty-note");
- const statusBar = $("status-bar");
- const moveBtn = $("move-btn");
- const moveMenu = $("move-menu");
- const overlay = $("overlay");
- // --- init ------------------------------------------------------------------
- async function init() {
- CONFIG = await fetchJSON("/api/config");
- for (const f of CONFIG.folders) {
- folderSelect.append(new Option(f, f));
- }
- folderSelect.value = CONFIG.default_folder;
- limitInput.value = CONFIG.default_limit;
- // Move dropdown targets
- moveMenu.innerHTML = "";
- for (const f of CONFIG.folders) {
- const item = document.createElement("button");
- item.className = "move-item";
- item.textContent = f;
- item.onclick = () => { hideMoveMenu(); runAction("move", f); };
- moveMenu.append(item);
- }
- wireEvents();
- await loadMessages();
- startJobPolling();
- }
- function wireEvents() {
- $("refresh-btn").onclick = loadMessages;
- folderSelect.onchange = () => { clearSelection(); loadMessages(); };
- limitInput.onchange = loadMessages;
- selectAll.onchange = () => toggleSelectAll(selectAll.checked);
- document.querySelectorAll(".btn.action[data-action]").forEach((btn) => {
- btn.onclick = () => runAction(btn.dataset.action);
- });
- moveBtn.onclick = (e) => {
- e.stopPropagation();
- moveMenu.classList.toggle("hidden");
- };
- document.addEventListener("click", hideMoveMenu);
- $("overlay-close").onclick = closeOverlay;
- overlay.onclick = (e) => { if (e.target === overlay) closeOverlay(); };
- $("ov-toggle").onclick = toggleBody;
- }
- // --- message list ----------------------------------------------------------
- async function loadMessages() {
- const folder = folderSelect.value;
- const limit = limitInput.value || CONFIG.default_limit;
- msgBody.innerHTML = `<tr><td colspan="5" class="loading">Loading…</td></tr>`;
- try {
- const data = await fetchJSON(
- `/api/messages?folder=${encodeURIComponent(folder)}&limit=${encodeURIComponent(limit)}`
- );
- messages = data.messages;
- renderRows();
- } catch (err) {
- msgBody.innerHTML = `<tr><td colspan="5" class="error">${escapeHTML(err.message)}</td></tr>`;
- }
- }
- function renderRows() {
- clearSelection();
- msgBody.innerHTML = "";
- emptyNote.classList.toggle("hidden", messages.length > 0);
- for (const m of messages) {
- const tr = document.createElement("tr");
- tr.dataset.localguid = m.localguid;
- const check = document.createElement("input");
- check.type = "checkbox";
- check.onclick = (e) => e.stopPropagation();
- check.onchange = () => toggleRow(m.localguid, check.checked, tr);
- const tdCheck = document.createElement("td");
- tdCheck.className = "col-check";
- tdCheck.append(check);
- tr.append(tdCheck);
- tr.append(cell(formatDate(m.date), "col-date"));
- tr.append(cell(m.from || "", "col-from"));
- tr.append(cell((m.rcpts || []).join(", "), "col-rcpt"));
- tr.append(cell(m.subject || "(no subject)", "col-subj"));
- tr.onclick = () => openOverlay(m);
- msgBody.append(tr);
- }
- updateActionButtons();
- }
- function cell(text, cls) {
- const td = document.createElement("td");
- td.className = cls;
- td.textContent = text;
- td.title = text;
- return td;
- }
- // --- selection -------------------------------------------------------------
- function toggleRow(localguid, on, tr) {
- if (on) selected.add(localguid); else selected.delete(localguid);
- tr.classList.toggle("selected", on);
- updateActionButtons();
- }
- function toggleSelectAll(on) {
- msgBody.querySelectorAll("tr").forEach((tr) => {
- const cb = tr.querySelector('input[type="checkbox"]');
- if (cb) { cb.checked = on; toggleRow(tr.dataset.localguid, on, tr); }
- });
- }
- function clearSelection() {
- selected.clear();
- selectAll.checked = false;
- updateActionButtons();
- }
- function updateActionButtons() {
- const has = selected.size > 0;
- document.querySelectorAll(".btn.action").forEach((b) => { b.disabled = !has; });
- }
- // --- actions ---------------------------------------------------------------
- async function runAction(action, targetfolder) {
- const chosen = messages.filter((m) => selected.has(m.localguid));
- if (chosen.length === 0) return;
- if (action === "delete" &&
- !confirm(`Delete ${chosen.length} message(s)? They move to "${CONFIG.deleted_folder}".`)) {
- return;
- }
- const payload = {
- action,
- folder: folderSelect.value,
- messages: chosen.map((m) => ({ localguid: m.localguid, guid: m.guid })),
- };
- if (action === "move") payload.targetfolder = targetfolder;
- try {
- await fetchJSON("/api/actions", { method: "POST", body: JSON.stringify(payload) });
- } catch (err) {
- alert("Action failed to queue: " + err.message);
- return;
- }
- // Optimistic: remove acted-on rows immediately.
- const gone = new Set(chosen.map((m) => m.localguid));
- messages = messages.filter((m) => !gone.has(m.localguid));
- renderRows();
- pollJobs();
- }
- // --- overlay ---------------------------------------------------------------
- async function openOverlay(m) {
- $("ov-subject").textContent = m.subject || "(no subject)";
- $("ov-meta").innerHTML = "";
- $("ov-text").textContent = "Loading…";
- $("ov-html").classList.add("hidden");
- $("ov-text").classList.remove("hidden");
- $("ov-toggle").classList.add("hidden");
- overlay.classList.remove("hidden");
- try {
- const data = await fetchJSON(`/api/message/${encodeURIComponent(m.guid)}`);
- renderOverlay(data);
- } catch (err) {
- $("ov-text").textContent = "Failed to load message: " + err.message;
- }
- }
- function renderOverlay(data) {
- const h = data.headers || {};
- const meta = [
- ["From", h.from], ["To", h.to], ["Cc", h.cc], ["Date", h.date],
- ].filter(([, v]) => v);
- $("ov-meta").innerHTML = meta
- .map(([k, v]) => `<div><span class="mk">${k}:</span> ${escapeHTML(v)}</div>`)
- .join("");
- const text = data.text || "";
- const html = data.html || "";
- $("ov-text").textContent = text || (html ? "(HTML message — use “Show HTML”.)" : "(empty body)");
- overlay.dataset.html = html;
- overlay.dataset.text = text;
- const toggle = $("ov-toggle");
- if (html) {
- toggle.classList.remove("hidden");
- toggle.textContent = "Show HTML";
- $("ov-html").classList.add("hidden");
- $("ov-text").classList.remove("hidden");
- } else {
- toggle.classList.add("hidden");
- }
- }
- function toggleBody() {
- const showingHtml = !$("ov-html").classList.contains("hidden");
- const toggle = $("ov-toggle");
- if (showingHtml) {
- $("ov-html").classList.add("hidden");
- $("ov-text").classList.remove("hidden");
- toggle.textContent = "Show HTML";
- } else {
- // srcdoc + sandbox keeps scripts and remote loads from running.
- $("ov-html").srcdoc = overlay.dataset.html || "";
- $("ov-html").classList.remove("hidden");
- $("ov-text").classList.add("hidden");
- toggle.textContent = "Show text";
- }
- }
- function closeOverlay() {
- overlay.classList.add("hidden");
- $("ov-html").srcdoc = "";
- }
- // --- job status polling ----------------------------------------------------
- function startJobPolling() { pollJobs(); }
- async function pollJobs() {
- let data;
- try {
- data = await fetchJSON("/api/jobs");
- } catch {
- return;
- }
- const jobs = data.jobs || [];
- const active = jobs.filter((j) => j.status === "pending" || j.status === "running");
- renderStatus(jobs, active);
- clearTimeout(jobPollTimer);
- if (active.length > 0) {
- jobPollTimer = setTimeout(pollJobs, 3000);
- }
- }
- function renderStatus(jobs, active) {
- if (active.length === 0) {
- // Show the most recent finished job briefly, else hide.
- const last = jobs[0];
- if (last && (last.status === "failed")) {
- statusBar.className = "status-bar error";
- statusBar.textContent = `Job #${last.id} (${labelFor(last.action)}) failed: ${last.error || ""}`;
- statusBar.classList.remove("hidden");
- } else {
- statusBar.classList.add("hidden");
- }
- return;
- }
- statusBar.className = "status-bar";
- statusBar.textContent = active
- .map((j) => `${labelFor(j.action)}: ${j.processed}/${j.total} (${j.status})`)
- .join(" • ");
- statusBar.classList.remove("hidden");
- }
- // --- misc helpers ----------------------------------------------------------
- function hideMoveMenu() { moveMenu.classList.add("hidden"); }
- function labelFor(action) {
- return {
- release: "Release",
- report_release: "Report & Release",
- delete: "Delete",
- move: "Move",
- }[action] || action;
- }
- function formatDate(d) {
- if (!d) return "";
- return d.replace("T", " ").slice(0, 19);
- }
- async function fetchJSON(url, opts = {}) {
- const res = await fetch(url, {
- headers: { "Content-Type": "application/json" },
- ...opts,
- });
- if (res.status === 401) { window.location = "/login"; throw new Error("Not signed in"); }
- const data = await res.json().catch(() => ({}));
- if (!res.ok) throw new Error(data.error || data.detail || `HTTP ${res.status}`);
- return data;
- }
- function escapeHTML(s) {
- return String(s).replace(/[&<>"']/g, (c) =>
- ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
- }
- init();
|