| ${esc(r.detail || "—")} |
${r.message_id ? esc(r.message_id.slice(0, 16)) + "…" : "—"} |
`).join("")}`
: `⟐
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 ? `
| # | Time | Action | Entity | Detail | IP |
${rows.map(r => `
| ${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 || "—")} |
`).join("")}
`
: `☰
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 = ``;
} 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 = ``;
}
}
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 | # | Outcome | Error class | Error | Provider 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:
`| Time | Event | Detail |
${(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();
};