window.pageBoot = async (me) => { let tab = "attempts"; let campaigns = []; let st = { attempts: 1, events: 1, audit: 1, dlq: 1 }; const PS = 50; const OUTCOMES = ["", "submitted", "accepted", "temp_failed", "perm_failed", "skipped"]; const OUT_BADGE = { submitted: "b-blue", accepted: "b-green", temp_failed: "b-amber", perm_failed: "b-red", skipped: "b-gray", sent: "b-green", suppressed: "b-violet", bounced: "b-red" }; const sel = (id, opts, val) => `"; async function loadCampaigns() { try { const d = await GET("/campaigns/list?page_size=100"); campaigns = d.campaigns || []; } catch { campaigns = []; } } function renderFilters() { const f = $("#filters"); const campOpts = [{ v: "", t: "All campaigns" }].concat( campaigns.map(c => ({ v: String(c.id), t: `#${c.id} — ${c.name}`.slice(0, 60) }))); if (tab === "attempts") { f.innerHTML = sel("fCamp", campOpts, st._camp || "") + sel("fOutcome", OUTCOMES.map(o => ({ v: o, t: o ? o.replace("_", " ") : "All outcomes" })), st._out || "") + `` + ``; $("#fGo").onclick = applyFilters; $("#fSearch").onkeydown = e => { if (e.key === "Enter") applyFilters(); }; } else if (tab === "events") { f.innerHTML = sel("fCamp", campOpts, st._camp || "") + sel("fEvType", ["", "queued", "submitted", "accepted", "failed", "retry_scheduled", "suppressed", "unsubscribed", "bounced", "dlq"].map(o => ({ v: o, t: o ? o.replace("_", " ") : "All event types" })), st._ev || "") + ``; $("#fGo").onclick = applyFilters; } else if (tab === "audit") { f.innerHTML = sel("fAction", ["", "auth.login", "auth.register", "campaign.create", "campaign.start", "campaign.pause", "campaign.resume", "campaign.cancel", "recipients.import", "recipients.manual_add", "subjects.import", "templates.upload", "attachments.upload", "phones.import", "suppression.add", "gmail.connect", "gmail.disconnect", "gmail.import_config", "settings.save_batch", "settings.reset"].map(o => ({ v: o, t: o ? o.replace(/\./g, " › ") : "All actions" })), st._act || "") + ``; $("#fGo").onclick = applyFilters; } else { f.innerHTML = `Jobs that exhausted all retries and were moved to the dead-letter queue (dlq:send).`; } } function applyFilters() { if (tab === "attempts") { st._camp = $("#fCamp").value; st._out = $("#fOutcome").value; st._q = $("#fSearch").value.trim(); } else if (tab === "events") { st._camp = $("#fCamp").value; st._ev = $("#fEvType").value; } else if (tab === "audit") { st._act = $("#fAction").value; } st[tab] = 1; load(); } function pagerHTML(total, page, key) { const pages = Math.max(1, Math.ceil(total / PS)); if (pages <= 1) return ""; return ` Page ${page} of ${pages} · ${total} records `; } function bindPager(key) { $$("#pager [data-pg]").forEach(b => b.onclick = () => { st[key] = parseInt(b.dataset.pg, 10); load(); }); } function badge(v) { return `${esc(v || "—")}`; } async function load() { const box = $("#listing"); box.innerHTML = `
`; try { if (tab === "attempts") { const p = new URLSearchParams({ campaign_id: st._camp || 0, outcome: st._out || "", search: st._q || "", page: st.attempts, page_size: PS }); const d = await GET("/logs/list?" + p); const rows = d.logs || []; box.innerHTML = rows.length ? `
${rows.map(r => ``).join("")}
#TimeEmailOutcomeAttemptError classError / provider responseMessage ID
${r.id} ${fmtDT(r.created_at)} ${esc(r.email || "")} ${badge(r.outcome)} ${r.attempt_number} ${r.error_class ? `${esc(r.error_class)}` : "—"} ${esc((r.error_message || r.provider_response || "").slice(0, 120) || "—")} ${r.message_id ? esc(r.message_id.slice(0, 18)) + "…" : "—"}
` : `
≡

No send attempts match these filters yet.

`; $("#pager").innerHTML = pagerHTML(d.total, d.page, "attempts"); bindPager("attempts"); $$("#listing [data-hist]").forEach(b => b.onclick = () => showHistory(parseInt(b.dataset.hist, 10))); } else if (tab === "events") { const p = new URLSearchParams({ campaign_id: st._camp || 0, event_type: st._ev || "", page: st.events, page_size: PS }); const d = await GET("/logs/events?" + p); const rows = d.events || []; box.innerHTML = rows.length ? `
${rows.map(r => ``).join("")}
#TimeCampaignEmailEventDetailMessage ID
${r.id} ${fmtDT(r.created_at)} #${r.campaign_id} ${esc(r.email || "")} ${badge(r.event_type)} ${esc(r.detail || "—")} ${r.message_id ? esc(r.message_id.slice(0, 16)) + "…" : "—"}
` : `
⟐

No email events yet. Events appear as campaign messages are queued, submitted and accepted.

`; $("#pager").innerHTML = pagerHTML(d.total, d.page, "events"); bindPager("events"); } else if (tab === "audit") { const p = new URLSearchParams({ action: st._act || "", page: st.audit, page_size: PS }); const d = await GET("/logs/audit?" + p); const rows = d.audit || []; box.innerHTML = rows.length ? `
${rows.map(r => ``).join("")}
#TimeActionEntityDetailIP
${r.id} ${fmtDT(r.created_at)} ${esc(r.action)} ${esc(r.entity || "—")}${r.entity_id ? ` #${r.entity_id}` : ""} ${esc(JSON.stringify(r.detail || {}).slice(0, 140))} ${esc(r.ip_address || "—")}
` : `
☰

No audit entries yet. Administrative actions are recorded here.

`; $("#pager").innerHTML = pagerHTML(d.total, d.page, "audit"); bindPager("audit"); } else { const d = await GET(`/logs/dlq?page=${st.dlq}&page_size=${PS}`); if (d.error) { box.innerHTML = `
⚠
${esc(d.error)}
`; } else { const items = d.dlq || []; box.innerHTML = items.length ? items.map((it, i) => { const idx = (d.page - 1) * PS + i + 1; const email = it.recipient_email || it.email || it.args?.recipient_email || "?"; const reason = it.reason || it.error || (it.exc_message || ""); return `
#${idx} · ${esc(email)} ${it.campaign_id ? `· campaign #${it.campaign_id}` : ""}
dead-lettered
${reason ? `
Reason: ${esc(String(reason).slice(0, 300))}
` : ""}
${esc(JSON.stringify(it, null, 2).slice(0, 3000))}
`; }).join("") : `
✓

Dead-letter queue is empty. No jobs have exhausted their retries.

`; $("#pager").innerHTML = pagerHTML(d.total, d.page, "dlq"); bindPager("dlq"); } } } catch (e) { box.innerHTML = `
✕
${esc(e.message)}
`; } } async function showHistory(crId) { let d; try { d = await GET(`/logs/recipient/${crId}/history`); } catch (e) { toast(e.message, "err"); return; } const r = d.recipient || {}; const node = el("div", {}, el("dl", { class: "kv" }, el("dt", {}, "Email"), el("dd", {}, r.email || "—"), el("dt", {}, "Seq"), el("dd", {}, String(r.seq ?? "—")), el("dt", {}, "Final status"), el("dd", { html: badge(r.status) }), el("dt", {}, "Invoice number"), el("dd", {}, r.invoice_number || "—"), el("dt", {}, "Amount"), el("dd", {}, r.amount != null ? fmtMoney(r.amount) : "—"), el("dt", {}, "Message ID"), el("dd", {}, r.message_id || "—"), el("dt", {}, "Last error"), el("dd", {}, r.last_error || "—")), el("div", { class: "hist-sec" }, el("h4", {}, `Send attempts (${(d.attempts || []).length})`), (d.attempts || []).length ? el("div", { class: "tbl-wrap" }, el("table", { class: "tbl", html: `Time#OutcomeError classErrorProvider response ${(d.attempts || []).map(a => ` ${fmtDT(a.created_at)}${a.attempt_number} ${badge(a.outcome)}${esc(a.error_class || "—")} ${esc((a.error_message || "—").slice(0, 120))} ${esc((a.provider_response || "—").slice(0, 120))} `).join("")}` })) : el("p", { class: "muted small" }, "No attempts recorded.")), el("div", { class: "hist-sec" }, el("h4", {}, `Events (${(d.events || []).length})`), (d.events || []).length ? el("div", { class: "tbl-wrap" }, el("table", { class: "tbl", html: `TimeEventDetail ${(d.events || []).map(e => ` ${fmtDT(e.created_at)}${badge(e.event_type)} ${esc(e.detail || "—")} `).join("")}` })) : el("p", { class: "muted small" }, "No events recorded."))); openModal(`Recipient history — ${r.email || "#" + crId}`, node, { wide: true }); } $$("#tabs .tab").forEach(t => t.onclick = () => { $$("#tabs .tab").forEach(x => x.classList.remove("active")); t.classList.add("active"); tab = t.dataset.tab; renderFilters(); load(); }); $("#btnExport").onclick = () => { const p = new URLSearchParams(); if (tab === "attempts" || tab === "events") { if (st._camp) p.set("campaign_id", st._camp); } if (tab === "attempts" && st._out) p.set("outcome", st._out); const qs = p.toString(); window.location = "/logs/export" + (qs ? "?" + qs : ""); toast("CSV export downloading…", "ok"); }; await loadCampaigns(); renderFilters(); load(); };