window.pageBoot = async (me) => { /* ---------------- shared state ---------------- */ let OPT = null; // wizard options let mode = "list"; // list | monitor let editingId = null; // editing existing campaign const ST_BADGE = { draft: "b-gray", queued: "b-blue", running: "b-amber", paused: "b-violet", completed: "b-green", cancelled: "b-gray", failed: "b-red" }; const C = {}; // wizard state (CampaignIn mirror) const DEF = () => ({ name: "", recipient_list_id: null, subject_set_ids: [], template_ids: [], message_type: "html", attach_mode: "none", attach_pdf: false, attach_pdf_template_id: null, attachment_id: null, variable_config: {}, account_ids: [], rotation_strategy: "sequential", rotation_weights: {}, subject_rotation: "round_robin", template_sequence_mode: "repeat", phone_selection: "round_robin", max_per_account: 200, send_interval_seconds: 1.0, rate_per_minute: 60, max_retries: 3, retry_delay_seconds: 60, unsubscribe_enabled: true, preview_sample_size: 5, }); Object.assign(C, DEF()); let step = 1; const STEPS = [ "Campaign name", "Recipient list", "Subject lines", "Templates", "Attachments", "Variables & amounts", "Gmail accounts", "Rotation & weights", "Limits & pacing", "Company phone", "Unsubscribe", "Confirm & start", ]; const STEPS_KEYS = ["name", "recipients", "subjects", "templates", "attachments", "variables", "accounts", "rotation", "limits", "phone", "unsubscribe", "confirm"]; async function loadOptions() { const d = await GET("/campaigns/wizard/options"); OPT = d; return d; } function stBadge(s) { return `${esc(s || "—")}`; } /* ==================== LIST PANE ==================== */ let lst = { page: 1, status: "", q: "" }; async function loadList() { const box = $("#campTable"); box.innerHTML = `
`; try { const p = new URLSearchParams({ status: lst.status, search: lst.q, page: lst.page, page_size: 20 }); const d = await GET("/campaigns/list?" + p); const rows = d.campaigns || []; box.innerHTML = rows.length ? `
${rows.map(c => ``).join("")}
IDNameStatusRecipientsSentFailedSuppressedSkippedStartedActions
${c.id} ${esc(c.name)} ${stBadge(c.status)} ${c.total_recipients ?? 0} ${c.sent_count ?? 0} ${c.failed_count ?? 0} ${c.suppressed_count ?? 0} ${c.skipped_count ?? 0} ${fmtDT(c.started_at)} ${["draft", "paused"].includes(c.status) ? `` : ""}
` : `
✦

No campaigns yet. Use the wizard to create your first campaign.

`; const pages = Math.max(1, Math.ceil(d.total / 20)); $("#campPager").innerHTML = pages > 1 ? ` Page ${lst.page} of ${pages} · ${d.total} campaigns ` : ""; $$("#campPager [data-pg]").forEach(b => b.onclick = () => { lst.page = +b.dataset.pg; loadList(); }); $$("#campTable [data-mon]").forEach(b => b.onclick = () => openMonitor(+b.dataset.mon)); $$("#campTable [data-edit]").forEach(b => b.onclick = () => openWizard(+b.dataset.edit)); $$("#campTable [data-prev]").forEach(b => b.onclick = () => campaignPreview(+b.dataset.prev)); $$("#campTable [data-del]").forEach(b => b.onclick = () => confirmedAction(`Delete campaign #${b.dataset.del}? This removes the campaign and its recipient assignments. Logs are kept.`, async () => { await POST(`/campaigns/${b.dataset.del}/delete`); loadList(); }, { title: "Delete campaign", confirmLabel: "Delete" })); } catch (e) { box.innerHTML = `
✕
${esc(e.message)}
`; } } /* ==================== MONITOR PANE ==================== */ let MON = { id: null, es: null, page: 1, statusF: "", q: "" }; function openMonitor(cid) { MON.id = cid; setMode("monitor"); } function setMode(m) { mode = m; $$("#modeTabs .mode-tab").forEach(t => t.classList.toggle("active", t.dataset.mode === m)); $("#listPane").classList.toggle("hidden", m !== "list"); $("#monPane").classList.toggle("hidden", m !== "monitor"); if (m === "list") { stopStream(); } if (m === "monitor" && MON.id) { refreshMonSel(); startMonitor(); } } async function refreshMonSel() { try { const d = await GET("/campaigns/list?page_size=100"); $("#monSel").innerHTML = (d.campaigns || []).map(c => ``).join(""); } catch {} $("#monSel").onchange = () => { MON.id = +$("#monSel").value; startMonitor(); }; } function stopStream() { if (MON.es) { MON.es.close(); MON.es = null; } $("#liveInd").classList.add("hidden"); } async function startMonitor() { if (!MON.id) return; stopStream(); const body = $("#monBody"); body.innerHTML = `
`; await drawProgress(); // initial paint via REST await drawRecipients(); drawControls(); // SSE MON.es = new EventSource(`/campaigns/${MON.id}/stream`); MON.es.addEventListener("connected", () => $("#liveInd").classList.remove("hidden"));