toast(`Campaign finished — status: ${(JSON.parse(ev.data) || {}).status || "?"}`, "info", 6000); drawControls(); }); MON.es.onerror = () => { $("#liveInd").classList.add("hidden"); }; } async function drawProgress() { try { const p = await GET(`/campaigns/${MON.id}/progress`); paintProgress(p); } catch (e) { $("#monBody").innerHTML = `
✕
${esc(e.message)}
`; } } function paintProgress(p) { let box = $("#progBox"); if (!box) { box = el("div", { id: "progBox" }); $("#monBody").prepend(box); } const meterSegs = Math.min(p.total || 0, 400); const seg = (cls, n) => ``; const sum = p.sent + p.perm_failed + p.suppressed + p.skipped; box.innerHTML = `
#${p.campaign_id} ${stBadge(p.status)} ${p.started_at ? "started " + fmtDT(p.started_at) : "not started"} ${p.finished_at ? `· finished ${fmtDT(p.finished_at)}` : ""}
${p.progress_pct}% complete
${meterSegs("sent", p.sent)}${meterSegs("fail", p.perm_failed)}${meterSegs("supp", p.suppressed)}${meterSegs("skip", p.skipped)}${meterSegs("proc", (p.total || 0) - sum)}
${p.sent}
Accepted & sent
${p.temp_failed}
Temp failed
${p.perm_failed}
Perm failed
${p.suppressed}
Suppressed
${p.skipped}
Skipped
${p.pending}
Pending
${p.queued}
Queued
${p.total}
Total
${p.jobs_total} send jobs · delivery attempts are retried with backoff; temporary failures never masquerade as success.
`; drawControls(); } function drawControls() { const ctl = $("#monCtl"); if (!ctl) return; fetch(`/campaigns/${MON.id}`).then(r => r.json()).then(c => { ctl.innerHTML = ""; const mk = (label, cls, fn, confirmMsg) => {