window.pageBoot = async (me) => {
const state = { accounts: [] };
const fmtDT = window.fmtDT || (s => s || "—");
function statusBadge(a){
if (a.cooldown_until && new Date(a.cooldown_until) > new Date())
return `cooldown`;
if (a.authorized) return `authorized`;
const m = {expired:"b-amber", revoked:"b-red", pending:"b-gray", error:"b-red"};
return `${esc(a.status)}`;
}
function render(){
const q = $("#accSearch").value.trim().toLowerCase();
const rows = state.accounts.filter(a => !q || a.email.toLowerCase().includes(q));
$("#accCount").textContent = String(state.accounts.length);
$("#accEmpty").style.display = state.accounts.length ? "none" : "";
$("#accTbl").style.display = state.accounts.length ? "" : "none";
$("#accBody").innerHTML = rows.map(a => `
|
${esc(a.email)}
refresh token: ${a.has_refresh_token ? "stored" : "none"}
${a.token_expires_at ? " · expires " + esc(fmtDT(a.token_expires_at)) : ""}
|
${statusBadge(a)} |
${esc((a.sending_method||"api").toUpperCase())} |
${a.daily_limit} / ${a.campaign_limit} |
${a.sends_today ?? 0} sent |
${a.last_success_at ? esc(fmtDT(a.last_success_at)) : "—"} |
${a.error_count ? `${a.error_count}
${esc(a.last_error||"")} ` : "0"} |
|
`).join("");
}
async function load(){
const d = await GET("/gmail/accounts");
state.accounts = d.accounts || [];
render();
}
$("#accSearch").addEventListener("input", render);
// ---- Connect flow (real Google OAuth start) ----
$("#btnConnect").addEventListener("click", async () => {
const btn = $("#btnConnect"); btn.disabled = true; btn.textContent = "Starting…";
try {
const d = await POST("/gmail/accounts/connect");
const url = d.authorization_url || d.url || d.auth_url;
if (!url) throw new Error("No authorization URL returned");
// Navigate the current tab to Google's consent screen (popup blockers
// kill window.open() after an await, so we use a direct navigation).
sessionStorage.setItem("oauth_start_url", url);
location.href = url;
} catch (e) {
toast("Connect failed: " + e.message, "err");
btn.disabled = false; btn.textContent = "Connect Gmail Account";
}
});
// ---- Import OAuth client config ----
$("#btnImportCfg").addEventListener("click", () => {
$("#cfgCard").style.display = $("#cfgCard").style.display === "none" ? "" : "none";
});
$("#cfgForm").addEventListener("submit", async (e) => {
e.preventDefault();
const f = $("#cfgFile").files[0];
if (!f) return;
const fd = new FormData(); fd.append("file", f);
try {
const d = await api("/gmail/accounts/import-config", { method: "POST", body: fd });
$("#cfgRedirect").textContent = d.redirect_uri;
toast(`Configuration imported (project: ${d.project_id}). You can now connect accounts.`, "ok");
$("#cfgForm").reset();
} catch (err) { toast("Import failed: " + err.message, "err"); }
});
// ---- Clear all ----
$("#btnClearAll").addEventListener("click", () => {
confirmedAction("Clear ALL Gmail accounts? This revokes tokens at Google and deletes all stored credentials for every connected account. Audit logs are preserved.", async () => {
const d = await POST("/gmail/accounts/clear-all");
toast(`Cleared ${d.removed} account(s); revoked at Google: ${d.revoked_at_google}`, "ok");
await load();
});
});
// ---- Row actions ----
$("#accBody").addEventListener("click", async (e) => {
const b = e.target.closest("button"); if (!b) return;
const id = b.dataset.test || b.dataset.edit || b.dataset.reauth || b.dataset.disc;
if (b.dataset.test) {
b.disabled = true; b.textContent = "Testing…";
try {
const d = await POST(`/gmail/accounts/${id}/test`);
if (d.ok) toast("Connection verified: " + (d.detail || "Gmail profile OK"), "ok");
else toast("Test failed: " + (d.detail || d.error || "unknown"), "err");
await load();
} finally { b.disabled = false; b.textContent = "Test"; }
}
if (b.dataset.reauth) {
try {
const d = await POST(`/gmail/accounts/${id}/reauthorize`);
if (d.authorization_url) {
// Direct navigation (no popup-blocker risk)
location.href = d.authorization_url;
}
} catch (err) { toast("Reauthorize failed: " + err.message, "err"); }
}
if (b.dataset.edit) {
const a = state.accounts.find(x => x.id == id);
const dm = el("select", {class:"inp"}, ...[["api","Gmail API (recommended)"],["smtp","SMTP XOAUTH2"]].map(([v,l]) =>
el("option", {value:v, selected:(a.sending_method||"api")===v}, l)));
const dl = el("input", {class:"inp", type:"number", value:a.daily_limit, min:1, max:2000});
const cl = el("input", {class:"inp", type:"number", value:a.campaign_limit, min:1, max:2000});
openModal("Account Limits — " + a.email, el("div", {class:"form"},
el("label", {}, "Sending method"), dm,
el("label", {}, "Daily send limit (conservative recommended)"), dl,
el("label", {}, "Per-campaign limit"), cl), {
buttons: [{label:"Save", kind:"btn", onClick: async (close) => {
const payload = { sending_method: dm.value, daily_limit: +dl.value, campaign_limit: +cl.value };
await api(`/gmail/accounts/${id}`, { method: "PATCH", body: payload });
toast("Limits updated", "ok"); close(); await load();
}}]
});
}
if (b.dataset.disc) {
confirmedAction(`Disconnect this account? The stored OAuth token will be revoked at Google and deleted locally.`, async () => {
const d = await api(`/gmail/accounts/${id}`, { method: "DELETE" });
toast(`Disconnected (revoked at Google: ${d.revoked_at_google})`, "ok");
await load();
});
}
});
// ---- OAuth return handling: show result + refresh list ----
const qParams = new URLSearchParams(location.search);
const connectedEmail = qParams.get("connected");
const oauthError = qParams.get("error");
if (connectedEmail || oauthError) {
history.replaceState(null, "", "/gmail-accounts");
}
if (connectedEmail) {
toast(`Gmail account connected: ${connectedEmail}`, "ok", 8000);
} else if (oauthError) {
toast(`Google authorization failed: ${oauthError}`, "err", 8000);
}
await load();
};