/* ════════════════════════════════════════════════════════════════ Derez — User Portal ════════════════════════════════════════════════════════════════ */ /* ─── API Base & Endpoints ───────────────────────────────────── */ const API_BASE = "https://n8n.derez.ai/webhook"; const AUTH_URL = `${API_BASE}/account/login`; // POST (sign in), DELETE (sign out) const AGENTS_URL = `${API_BASE}/agent/list`; // GET ?email= const AGENT_INFO_URL = `${API_BASE}/agent/info`; // GET ?uuid= | POST {uuid,key,value} const KEYS_URL = `${API_BASE}/agent/key`; // GET ?uuid= const PASSWORD_URL = `${API_BASE}/agent/password`; // POST {uuid,email,password} const RESTART_URL = `${API_BASE}/agent/service/restart`; // POST {uuid,email,action:"restart"} const BACKUP_URL = `${API_BASE}/agent/service/backup`; // GET ?uuid= | POST {uuid} | PUT restore const CONTRACT_URL = `${API_BASE}/account/contract`; // GET ?uuid= const ACCOUNT_PASSWORD_URL = `${API_BASE}/account/password/change`; // POST {email,password} const PW_RESET_URL = `${API_BASE}/account/password/init_change`; // POST {email} const SIGNUP_URL = `${API_BASE}/etc/signup`; // POST {email,password,agent,location} const COUPON_URL = `${API_BASE}/etc/coupon`; // POST {code} const CHAT_URL = `${API_BASE}/etc/chat`; // POST {message,sessionId,email} const REFERRAL_URL = `${API_BASE}/etc/referral`; // POST {referrer,friend_name,friend_email,uuid} // WIZZARD=false is written via AGENT_INFO_URL (POST { uuid, key, value }) const CREDITS_URL = `${API_BASE}/payment/credits`; // POST {key} const CONTRACT_EXTEND_URL = "https://derez.ai"; // ← replace with Stripe payment link const CONTRACT_CANCEL_URL = "https://derez.ai"; // ← replace with Stripe cancellation link /* ─── State ─────────────────────────────────────────────────── */ let currentEmail = null; let currentSession = null; let couponApplied = false; let agents = []; let activeUUID = null; let activeAgentInfo = null; let chatId = null; // generated on first panel open let talkOpen = false; // Wizard state let wizardPhase = null; // 'skills' | 'integrations' | 'fields' | 'review' let wizardSkillsData = null; // cached from skills/index.json let wizardIntegrationsData = null; // cached from integrations/index.json let wizardSkillsCache = {}; // { filename: full skill object (with prompt) } let wizardIntegrationsCache = {}; // { filename: full integration object (with prompt) } let wizardSelectedSkills = new Set(); let wizardSelectedIntegrations = new Set(); let wizardFieldValues = {}; // { stepIndex: { fieldKey: value } } let wizardIntegrationStep = 0; // current index in fields phase let wizardSelectedIntegrationList = []; // ordered list of selected integration objects /* ─── Debug logger ─────────────────────────────────────────── */ function dbg(label, data) { console.log(`[Derez] ${label}`); console.log(data); } /* ─── Response normalisers ──────────────────────────────────── */ // n8n can wrap items as {json:{…}} or double-wrap arrays [[…]] function normalizeAgents(raw) { if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) { // { data: [...] } envelope — recurse into the array if (Array.isArray(raw.data)) return normalizeAgents(raw.data); // Plain single agent object — wrap it return [raw]; } if (!Array.isArray(raw)) return []; // unwrap double-wrapped array: [[{…}]] → [{…}] if (raw.length > 0 && Array.isArray(raw[0])) return normalizeAgents(raw[0]); // unwrap n8n {json:{…}} envelope if ( raw.length > 0 && raw[0] !== null && typeof raw[0] === "object" && "json" in raw[0] ) { return raw.map((item) => item.json); } return raw; } /* ═══════════════════════════════════════════════════════════════ BOOT ═══════════════════════════════════════════════════════════════ */ document.addEventListener("DOMContentLoaded", () => { const sessionId = getCookie("al_session"); if (sessionId) { currentSession = sessionId; currentEmail = getCookie("al_email") || ""; showApp(); loadAgents(); } else { showAuth(); } // Close confirm on backdrop click document.getElementById("confirm-backdrop").addEventListener("click", (e) => { if (e.target === e.currentTarget) confirmClose(false); }); // Close user panel when clicking outside of it document.addEventListener("click", (e) => { const wrap = document.getElementById("user-panel-wrap"); if (wrap && !wrap.contains(e.target)) closeUserPanel(); }); }); /* ═══════════════════════════════════════════════════════════════ AUTH — tab switch ═══════════════════════════════════════════════════════════════ */ function switchAuthTab(tab) { document .querySelectorAll(".auth-switch-btn") .forEach((b) => b.classList.toggle("active", b.dataset.tab === tab)); document.getElementById("signin-form").style.display = tab === "signin" ? "" : "none"; document.getElementById("signup-form").style.display = tab === "signup" ? "" : "none"; // Clear stale error/success messages document.getElementById("signin-error").style.display = "none"; document.getElementById("signup-error").style.display = "none"; document.getElementById("signup-success").style.display = "none"; const _rpw = document.getElementById("reset-pw-wrap"); if (_rpw) _rpw.style.display = "none"; } /* ═══════════════════════════════════════════════════════════════ AUTH — sign in Sends the email to the agents webhook; a 200 response means the account exists and we proceed to the app. ═══════════════════════════════════════════════════════════════ */ async function doSignIn() { const email = document.getElementById("signin-email").value.trim(); const password = document.getElementById("signin-password").value; hideAuthError("signin-error"); if (!isValidEmail(email)) { showAuthError("signin-error", "Please enter a valid email address."); return; } if (!password) { showAuthError("signin-error", "Please enter your password."); return; } const btn = document.getElementById("signin-btn"); btnLoad(btn, "Signing in\u2026"); try { dbg("\u2192 Sign In", AUTH_URL); const res = await fetch(AUTH_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!res.ok) throw new Error( "Account not found or incorrect password. Please try again.", ); const data = await res.json(); dbg("\u2190 Sign In", data); const raw = Array.isArray(data) ? data[0] : data; // unwrap n8n {json:{...}} envelope if present const item = raw && typeof raw === "object" && "json" in raw ? raw.json : raw; const sessionId = item?.sessionid; if (!sessionId) throw new Error("Authentication failed. Please try again."); currentSession = sessionId; currentEmail = email; setCookie("al_session", sessionId, 30); setCookie("al_email", email, 30); showApp(); loadAgents(); } catch (err) { showAuthError("signin-error", err.message); showResetOption(); } finally { btnReset(btn); } } /* ═══════════════════════════════════════════════════════════════ AUTH — sign up ═══════════════════════════════════════════════════════════════ */ async function doSignUp() { const email = document.getElementById("signup-email").value.trim(); const password = document.getElementById("signup-password").value; const agent = document.getElementById("signup-agent").value; const location = document.getElementById("signup-location").value; hideAuthError("signup-error"); document.getElementById("signup-success").style.display = "none"; if (!isValidEmail(email)) { showAuthError("signup-error", "Please enter a valid email address."); return; } if (password.length < 8) { showAuthError("signup-error", "Password must be at least 8 characters."); return; } const btn = document.getElementById("signup-btn"); btnLoad(btn, "Hiring agent\u2026"); // Open a blank tab NOW while we still have the user-gesture context. // Browsers block window.open called after an await, so we must do this // synchronously before any async work begins. const stripeTab = window.open("", "_blank"); try { // Step 1: authenticate to get a session dbg("\u2192 Hire Agent \u2013 auth", AUTH_URL); const authRes = await fetch(AUTH_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password }), }); if (!authRes.ok) throw new Error("Account creation failed. Please try again later."); const authData = await authRes.json(); dbg("\u2190 Hire Agent \u2013 auth", authData); const authRaw = Array.isArray(authData) ? authData[0] : authData; // unwrap n8n {json:{...}} envelope if present const authItem = authRaw && typeof authRaw === "object" && "json" in authRaw ? authRaw.json : authRaw; const sessionId = authItem?.sessionid; if (!sessionId) throw new Error("Account creation failed. Please try again."); currentSession = sessionId; currentEmail = email; setCookie("al_session", sessionId, 30); setCookie("al_email", email, 30); // Step 2: trigger agent creation dbg("\u2192 Hire Agent \u2013 create", SIGNUP_URL); const createRes = await fetch(SIGNUP_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email, password, agent, location }), }); if (!createRes.ok) { throw new Error("Agent provisioning failed. Please contact support."); } const createData = await createRes.json(); dbg("\u2190 Hire Agent \u2013 create", createData); const createRaw = Array.isArray(createData) ? createData[0] : createData; const createItem = createRaw && typeof createRaw === "object" && "json" in createRaw ? createRaw.json : createRaw; // Navigate the pre-opened tab to the Stripe checkout URL const checkoutUrl = createItem?.checkout_url || createItem?.checkoutUrl || createItem?.url; if (checkoutUrl && checkoutUrl.startsWith("http")) { stripeTab.location.href = checkoutUrl; } else { stripeTab.close(); } // Enter the app and load agents now that the agent has been created showApp(); loadAgents(); } catch (err) { stripeTab?.close(); showAuthError("signup-error", err.message); } finally { btnReset(btn); } } /* ═══════════════════════════════════════════════════════════════ AUTH — sign out ═══════════════════════════════════════════════════════════════ */ /* ─── Coupon ────────────────────────────────────────────────── */ function handleCouponKey(e) { if (e.key === "Enter") { e.preventDefault(); applyCoupon(); } } async function applyCoupon() { const input = document.getElementById("coupon-input"); const spinner = document.getElementById("coupon-spinner"); const errorEl = document.getElementById("coupon-error"); const code = input.value.trim(); if (!code) return; input.disabled = true; input.classList.remove("coupon-input-error"); errorEl.style.display = "none"; spinner.style.display = ""; try { const res = await apiFetch(COUPON_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); const text = (await res.text()).trim(); if (text === "OK") { // Valid coupon — hide the box, show success couponApplied = true; document.getElementById("coupon-box").style.display = "none"; document.getElementById("coupon-success").style.display = ""; } else { input.classList.add("coupon-input-error"); errorEl.style.display = ""; input.disabled = false; } } catch (err) { input.classList.add("coupon-input-error"); errorEl.textContent = "Could not validate coupon. Try again."; errorEl.style.display = ""; input.disabled = false; } finally { spinner.style.display = "none"; } } function doSignOut() { // Invalidate server session (fire-and-forget) if (currentSession) { fetch(AUTH_URL, { method: "DELETE", headers: { "Content-Type": "application/json", "X-Session-Id": currentSession, }, body: JSON.stringify({ sessionid: currentSession }), }).catch(() => {}); } deleteCookie("al_session"); deleteCookie("al_email"); localStorage.removeItem("al_email"); // clean up legacy storage currentSession = null; currentEmail = null; agents = []; activeUUID = null; chatId = null; talkOpen = false; const tp = document.getElementById("talk-panel"); if (tp) { tp.classList.remove("open"); } const chev = document.getElementById("talk-chevron"); if (chev) { chev.style.transform = ""; } const chatMsgs = document.getElementById("chat-msgs"); if (chatMsgs) { chatMsgs.innerHTML = ""; } // Reset signup form state so it's clean on next visit document.getElementById("signup-btn").style.display = ""; document.getElementById("signup-success").style.display = "none"; document.getElementById("signup-email").value = ""; document.getElementById("signup-password").value = ""; // Reset coupon state couponApplied = false; const couponBox = document.getElementById("coupon-box"); if (couponBox) couponBox.style.display = ""; const couponSuccessEl = document.getElementById("coupon-success"); if (couponSuccessEl) couponSuccessEl.style.display = "none"; const couponInput = document.getElementById("coupon-input"); if (couponInput) { couponInput.value = ""; couponInput.classList.remove("coupon-input-error"); } const couponError = document.getElementById("coupon-error"); if (couponError) couponError.style.display = "none"; closeUserPanel(); window.location.reload(); } /* ─── View switching ────────────────────────────────────────── */ function showAuth() { document.getElementById("auth-card").style.display = ""; document.getElementById("app-shell").style.display = "flex"; document.getElementById("app-shell").classList.add("demo-mode"); document.body.classList.add("auth-layout"); populateDemoShell(); } function showApp() { const authCard = document.getElementById("auth-card"); const shell = document.getElementById("app-shell"); const fromAuth = document.body.classList.contains("auth-layout"); function reveal() { authCard.style.display = "none"; authCard.classList.remove("auth-leaving"); document.body.classList.remove("auth-layout"); // Make shell visible and play entry animation shell.style.display = "flex"; shell.classList.remove("demo-mode"); shell.classList.add("app-entering"); document.getElementById("user-email-label").textContent = currentEmail; setTimeout(() => shell.classList.remove("app-entering"), 620); } if (fromAuth) { // Transitioning from the auth screen: animate auth card out first authCard.classList.add("auth-leaving"); setTimeout(reveal, 310); } else { // Direct page load while already logged in: skip the auth animation reveal(); } } function populateDemoShell() { hide("wizard-card"); // Header document.getElementById("user-email-label").textContent = "preview"; renderComment("My AI Assistant"); // Agent tab document.getElementById("agent-tabs-bar").innerHTML = ` +`; // Show agent panel with demo data hide("no-agents"); document.getElementById("agent-panel").style.display = "flex"; // API Keys const keysDot = document.getElementById("keys-status-dot"); keysDot.className = "contract-dot contract-dot--green"; keysDot.style.visibility = "visible"; document.getElementById("keys-body").innerHTML = `
Default Key Active
350 / 500 credits remaining · Expires Dec 31, 2026
`; // Server info document.getElementById("server-info-body").innerHTML = `
Agent Type Hermes
Dashboard hermes.derez.ai
SSH Access ssh root@hermes.derez.ai -p 2201
Created 2026-01-01
`; // Contract const demoDot = document.getElementById("contract-status-dot"); demoDot.className = "contract-dot contract-dot--green"; demoDot.style.visibility = "visible"; document.getElementById("contract-body").innerHTML = `
Auto - Monthly No expiration
Cancel
`; // Backups document.getElementById("backups-body").innerHTML = `
2026-05-30 09:12
2026-05-31 14:47
`; } /* ═══════════════════════════════════════════════════════════════ AGENTS — load & render tabs ═══════════════════════════════════════════════════════════════ */ async function loadAgents() { const bar = document.getElementById("agent-tabs-bar"); bar.innerHTML = '
Loading agents…
'; hide("agent-panel"); hide("no-agents"); try { const _agentsUrl2 = `${AGENTS_URL}?email=${encodeURIComponent(currentEmail)}`; dbg("→ List Agents", _agentsUrl2); const res = await apiFetch(_agentsUrl2); if (!res.ok) throw new Error(`HTTP ${res.status}`); let data = []; try { data = await res.json(); } catch (_) {} dbg("← List Agents", data); processAgents(normalizeAgents(data)); } catch (err) { bar.innerHTML = `${escHtml(err.message)}`; } } function processAgents(data) { agents = data; dbg( "processAgents agents[0] (JSON)", agents.length ? JSON.stringify(agents[0]) : "(empty)", ); renderAgentTabs(); if (agents.length > 0) { // Accept UUID (spec) or uuid (n8n lowercase) as fallback selectAgent(agents[0].UUID || agents[0].uuid || ""); } else { show("no-agents"); hide("agent-panel"); } } function renderAgentTabs() { const bar = document.getElementById("agent-tabs-bar"); if (!agents.length) { bar.innerHTML = 'No agents on your account'; return; } bar.innerHTML = agents .map((a) => { const uuid = a.UUID || a.uuid || ""; const server = a.server || ""; return ` `; }) .join("") + `+`; } function selectAgent(uuid) { activeUUID = uuid; renderAgentTabs(); hide("no-agents"); // Show agent panel using flex so its children lay out correctly const panel = document.getElementById("agent-panel"); panel.style.display = "flex"; // Reset password field document.getElementById("pw-new-input").value = ""; togglePwSave(""); // Reset comment cancelComment(); renderComment(""); hide("wizard-card"); loadKeys(uuid); loadBackups(uuid); loadAgentInfo(uuid); loadContract(uuid); } /* ═══════════════════════════════════════════════════════════════ AGENT INFO ═══════════════════════════════════════════════════════════════ */ // Strip the extra surrounding quotes n8n injects into string values: '"Hermes"' → 'Hermes' function stripQuotes(v) { if (v == null) return "—"; return String(v).replace(/^"|"$/g, "").trim() || "—"; } async function loadAgentInfo(uuid) { const body = document.getElementById("server-info-body"); body.innerHTML = '
'; activeAgentInfo = null; hide("wizard-card"); try { const _infoUrl = `${AGENT_INFO_URL}?uuid=${encodeURIComponent(uuid)}`; dbg("→ Agent Info", _infoUrl); const res = await apiFetch(_infoUrl); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); dbg("← Agent Info", json); // Unwrap [[{...}]], [{...}], or plain {...} let info = json; if (Array.isArray(info)) info = info[0]; // outer array if (Array.isArray(info)) info = info[0]; // inner array ([[...]]) if (!info || typeof info !== "object") throw new Error("Unexpected agent info format"); activeAgentInfo = info; renderAgentInfo(info); renderComment(info.comment); loadWizard(info); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; } } /* ─── Header comment ────────────────────────────────────────── */ function renderComment(text) { const el = document.getElementById("header-comment-display"); if (text && text.trim()) { el.textContent = stripQuotes(text); el.classList.remove("comment-empty"); } else { el.textContent = "Name your Agent"; el.classList.add("comment-empty"); } } function editComment() { const display = document.getElementById("header-comment-display"); const edit = document.getElementById("header-comment-edit"); const input = document.getElementById("header-comment-input"); const current = display.classList.contains("comment-empty") ? "" : display.textContent; input.value = current; display.style.display = "none"; edit.style.display = "flex"; input.focus(); input.select(); } function cancelComment() { document.getElementById("header-comment-display").style.display = ""; document.getElementById("header-comment-edit").style.display = "none"; } function handleCommentKey(e) { if (e.key === "Enter") { e.preventDefault(); saveComment(); } if (e.key === "Escape") cancelComment(); } async function saveComment() { const input = document.getElementById("header-comment-input"); const btn = document.getElementById("comment-save-btn"); const text = input.value.trim(); btnLoad(btn, "Saving…"); try { dbg("→ Save Comment", AGENT_INFO_URL); const res = await apiFetch(AGENT_INFO_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID, key: "comment", value: text }), }); if (!res.ok) throw new Error(`Failed to save comment (HTTP ${res.status}).`); renderComment(text); cancelComment(); toast("Comment saved.", "success"); } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); } } function renderAgentInfo(info) { const body = document.getElementById("server-info-body"); const domain = stripQuotes(info.domain); const domainHref = domain !== "—" ? domain.startsWith("http") ? domain : `https://${domain}` : null; const port = stripQuotes(info.ssh_port); const sshCmd = `ssh root@${activeUUID}.derez.ai -p ${port}`; const rows = [ `
Agent Type ${escHtml(stripQuotes(info.agent))}
`, `
Dashboard ${ domainHref ? `${escHtml(domain)}` : escHtml(domain) }
`, `
SSH Access ${escHtml(sshCmd)}
`, `
Created ${escHtml(stripQuotes(info.created))}
`, ]; body.innerHTML = rows.join(""); } function copySSHAccess() { if (!activeAgentInfo) return; const port = stripQuotes(activeAgentInfo.ssh_port); const cmd = `ssh root@${activeUUID}.derez.ai -p ${port}`; navigator.clipboard .writeText(cmd) .then(() => toast("SSH command copied to clipboard.", "info")) .catch(() => toast("Could not access clipboard.", "error")); } function openAgent() { if (!activeAgentInfo) { toast("Agent info not loaded yet.", "warning"); return; } const domain = stripQuotes(activeAgentInfo.domain); if (!domain || domain === "\u2014") { toast("No dashboard URL available for this agent.", "warning"); return; } const href = domain.startsWith("http") ? domain : `https://${domain}`; window.open(href, "_blank", "noopener"); } function openWizard() { const card = document.getElementById("wizard-card"); const btn = document.getElementById("wizard-toggle-btn"); if (card.style.display !== "none") { // Wizard is visible — close it, restore button label hide("wizard-card"); if (btn) btn.textContent = "Integrations"; return; } // Wizard is hidden — open from step 1, update button label wizardPhase = "skills"; wizardSelectedSkills = new Set(); wizardSelectedIntegrations = new Set(); wizardFieldValues = {}; wizardIntegrationStep = 0; wizardSelectedIntegrationList = []; show("wizard-card"); if (btn) btn.textContent = "Backup"; renderWizardStep(); } /* ═══════════════════════════════════════════════════════════════ KEYS ═══════════════════════════════════════════════════════════════ */ async function loadKeys(uuid) { const body = document.getElementById("keys-body"); body.innerHTML = '
'; document.getElementById("keys-status-dot").style.visibility = "hidden"; try { const _keysUrl = `${KEYS_URL}?uuid=${encodeURIComponent(uuid)}`; dbg("→ List API Keys", _keysUrl); const res = await apiFetch(_keysUrl); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); dbg("← List API Keys", json); // Response is [{data: [...]}, ...] or {data: [...]} or flat array const first = Array.isArray(json) ? json[0] : json; const keys = first && Array.isArray(first.data) ? first.data : Array.isArray(json) ? json : []; renderKeys(keys); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; } } function renderKeys(keys) { const body = document.getElementById("keys-body"); const dot = document.getElementById("keys-status-dot"); if (!keys.length) { dot.className = "contract-dot contract-dot--red"; dot.style.visibility = "visible"; body.innerHTML = '

No API keys found.

'; return; } const hasActive = keys.some((k) => !k.disabled); dot.className = `contract-dot ${hasActive ? "contract-dot--green" : "contract-dot--red"}`; dot.style.visibility = "visible"; body.innerHTML = keys .map((k) => { const used = (k.limit || 0) - (k.limit_remaining || 0); const total = k.limit || 0; const pct = total > 0 ? Math.min(100, Math.round((used / total) * 100)) : 0; const progClass = pct < 50 ? "prog-low" : pct < 80 ? "prog-mid" : "prog-high"; const exp = k.expires_at ? new Date(k.expires_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }) : "\u2014"; const statusColor = k.disabled ? "var(--danger)" : "var(--success)"; const statusLabel = k.disabled ? "Disabled" : "Active"; return `
${escHtml(k.name)} ${statusLabel}
${k.limit_remaining ?? "\u2014"} / ${total} credits remaining · Expires ${exp}
`; }) .join(""); } async function buyCredits(keyName, btn) { btnLoad(btn, "Processing\u2026"); try { const res = await fetch(CREDITS_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ key: keyName }), }); if (!res.ok) throw new Error(`Request failed (${res.status})`); toast("Credits purchase initiated!", "success"); } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); } } /* ═══════════════════════════════════════════════════════════════ INSTANCE PASSWORD ═══════════════════════════════════════════════════════════════ */ function togglePwSave(value) { document.getElementById("pw-save-btn").style.display = value ? "flex" : "none"; } async function savePassword() { const newPw = document.getElementById("pw-new-input").value; const btn = document.getElementById("pw-save-btn"); if (!newPw) { toast("Please enter a new password.", "warning"); return; } if (newPw.length < 8) { toast("Password must be at least 8 characters.", "warning"); return; } // Lock the button immediately so a second click can't corrupt the dialog Promise btn.disabled = true; const ok = await confirmDialog( "Update Instance Password", "This will immediately update the root password on your server. Make sure to save it somewhere safe. Continue?", ); if (!ok) { btn.disabled = false; // user cancelled — restore button return; } btnLoad(btn, "Saving\u2026"); try { dbg("\u2192 Update Password", PASSWORD_URL); const res = await apiFetch(PASSWORD_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID, email: currentEmail, password: newPw, }), }); if (!res.ok) throw new Error(`Failed to update password (HTTP ${res.status}).`); toast("Password updated successfully.", "success"); document.getElementById("pw-new-input").value = ""; togglePwSave(""); } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); } } /* ═══════════════════════════════════════════════════════════════ ACCOUNT / USER PANEL ═══════════════════════════════════════════════════════════════ */ function toggleUserPanel() { const panel = document.getElementById("user-panel"); if (!panel) return; if (panel.style.display === "none" || panel.style.display === "") { document.getElementById("user-panel-email-val").textContent = currentEmail || "—"; panel.style.display = "block"; } else { closeUserPanel(); } } function closeUserPanel() { const panel = document.getElementById("user-panel"); if (!panel) return; panel.style.display = "none"; const inp = document.getElementById("acct-pw-input"); if (inp) inp.value = ""; const btn = document.getElementById("acct-pw-save-btn"); if (btn) btn.style.display = "none"; } function toggleAccountPwSave(value) { const btn = document.getElementById("acct-pw-save-btn"); if (btn) btn.style.display = value ? "flex" : "none"; } async function saveAccountPassword() { const newPw = document.getElementById("acct-pw-input").value; const btn = document.getElementById("acct-pw-save-btn"); if (!newPw) { toast("Please enter a new password.", "warning"); return; } if (newPw.length < 8) { toast("Password must be at least 8 characters.", "warning"); return; } btn.disabled = true; const ok = await confirmDialog( "Change Account Password", "This will update your account login password immediately. Continue?", ); if (!ok) { btn.disabled = false; return; } btnLoad(btn, ""); try { dbg("→ Update Account Password", ACCOUNT_PASSWORD_URL); const res = await apiFetch(ACCOUNT_PASSWORD_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: currentEmail, password: newPw }), }); if (!res.ok) throw new Error( `Failed to update account password (HTTP ${res.status}).`, ); toast("Account password updated.", "success"); closeUserPanel(); } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); } } /* ═══════════════════════════════════════════════════════════════ RESTART ═══════════════════════════════════════════════════════════════ */ async function doRestart() { const ok = await confirmDialog( "Restart Agent", "Your agent will be unavailable for a few minutes while it restarts. This will interrupt any active sessions. Continue?", ); if (!ok) return; const btn = document.getElementById("restart-btn"); btnLoad(btn, "Restarting\u2026"); try { dbg("→ Restart Agent", RESTART_URL); const res = await apiFetch(RESTART_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID, email: currentEmail, action: "restart", }), }); if (!res.ok) throw new Error(`Restart request failed (HTTP ${res.status}).`); toast( "Restart initiated \u2014 your agent will be back shortly.", "success", ); } catch (err) { toast(err.message, "error"); } finally { // Rebuild button with its SVG icon btn.disabled = false; btn.innerHTML = ` Restart Agent`; } } /* ═══════════════════════════════════════════════════════════════ BACKUPS — load, render, create, restore ═══════════════════════════════════════════════════════════════ */ async function loadBackups(uuid) { const body = document.getElementById("backups-body"); body.innerHTML = '
'; try { const _backupsUrl = `${BACKUP_URL}?uuid=${encodeURIComponent(uuid)}`; dbg("→ List Backups", _backupsUrl); const res = await apiFetch(_backupsUrl); if (!res.ok) throw new Error(`HTTP ${res.status}`); let data = []; try { data = await res.json(); } catch (_) {} dbg("← List Backups", data); // Response: { uuid, backup_path, archives: [{name, start, end, id}] } const archives = data && Array.isArray(data.archives) ? data.archives : Array.isArray(data) ? data : []; renderBackups(archives); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; } } function renderBackups(backups) { const body = document.getElementById("backups-body"); if (!backups.length) { body.innerHTML = `
No backups yet. Click Make Backup to create one.
`; return; } const rows = [...backups] .sort((a, b) => { const da = new Date( a.start || a.created_at || a.createdAt || a.date || a.timestamp || 0, ); const db = new Date( b.start || b.created_at || b.createdAt || b.date || b.timestamp || 0, ); return db - da; }) .map((b) => { const start = formatBackupDate(b.start); const id = escAttr(String(b.id || b.name || "")); const nm = escAttr(String(b.name || b.id || start)); return `
${escHtml(start)}
`; }) .join(""); body.innerHTML = `
${rows}
`; } async function makeBackup() { const btn = document.getElementById("make-backup-btn"); btnLoad(btn, "…"); try { dbg("→ Make Backup", BACKUP_URL); const res = await apiFetch(BACKUP_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID }), }); if (!res.ok) throw new Error(`Failed to create backup (HTTP ${res.status}).`); toast("Backup created successfully!", "success"); } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); await loadBackups(activeUUID); } } function restoreBackupFromBtn(btn) { restoreBackup({ id: btn.dataset.backupId, name: btn.dataset.backupName }); } async function restoreBackup(backup) { const ok = await confirmDialog( "Restore Backup", `Restore "${backup.name}"? Your server data will be replaced with this backup and the agent will restart briefly.`, ); if (!ok) return; try { dbg("→ Restore Backup", BACKUP_URL); const res = await apiFetch(BACKUP_URL, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID, email: currentEmail, action: "restore", archive_name: backup.name, }), }); if (!res.ok) throw new Error(`Restore failed (HTTP ${res.status}).`); toast( "Restore initiated \u2014 your agent will be back shortly.", "success", ); } catch (err) { toast(err.message, "error"); } } /* ═══════════════════════════════════════════════════════════════ CONTRACT ═══════════════════════════════════════════════════════════════ */ async function loadContract(uuid) { const body = document.getElementById("contract-body"); body.innerHTML = '
'; document.getElementById("contract-status-dot").style.visibility = "hidden"; try { const url = `${CONTRACT_URL}?uuid=${encodeURIComponent(uuid)}`; dbg("\u2192 Contract", url); const res = await apiFetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); dbg("\u2190 Contract", data); renderContract(Array.isArray(data) ? data : [data]); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; } } function renderContract(data) { const body = document.getElementById("contract-body"); const headerDot = document.getElementById("contract-status-dot"); if (!data || !data.length) { body.innerHTML = '

No contract data.

'; return; } const item = data[0]; const type = (item.type || "").trim(); const expires = item.expires || ""; // Determine status const t = type.toLowerCase(); let expired = false; if (expires && expires.trim()) { try { expired = new Date(expires) < new Date(); } catch (_) {} } let statusClass; if (!type || expired || t.includes("none")) { statusClass = "contract-dot--red"; } else if (t.includes("auto")) { statusClass = "contract-dot--green"; } else { statusClass = "contract-dot--yellow"; } // Update the header dot headerDot.className = `contract-dot ${statusClass}`; headerDot.style.visibility = "visible"; // Empty type — show placeholder row with extend button on the right if (!type) { body.innerHTML = `
No contract Stripe payments can take up to 24hrs to be recognized.
Extend Contract
`; return; } const expLabel = expires && expires.trim() ? new Date(expires).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", }) : "No expiration"; const extendHtml = statusClass === "contract-dot--red" ? `Extend Contract` : ""; const cancelHtml = statusClass === "contract-dot--green" ? `Cancel` : ""; body.innerHTML = `
${escHtml(type)} ${escHtml(expLabel)} ${extendHtml}
${cancelHtml}
`; } /* ═══════════════════════════════════════════════════════════════ WIZARD (Integrations setup) ═══════════════════════════════════════════════════════════════ */ function loadWizard(info) { // Check both key casings; strip n8n's extra surrounding quotes before comparing const rawVal = info.wizzard ?? info.WIZZARD; const strVal = String(rawVal ?? "") .replace(/^"|"$/g, "") .trim() .toLowerCase(); const disabled = rawVal === false || strVal === "false"; const tb = document.getElementById("wizard-toggle-btn"); if (disabled) { hide("wizard-card"); if (tb) tb.textContent = "Integrations"; return; } // Reset selection state each time (JSON data cached across calls) wizardPhase = "skills"; wizardSelectedSkills = new Set(); wizardSelectedIntegrations = new Set(); wizardFieldValues = {}; wizardIntegrationStep = 0; wizardSelectedIntegrationList = []; show("wizard-card"); if (tb) tb.textContent = "Backup"; renderWizardStep(); } async function renderWizardStep() { const body = document.getElementById("wizard-body"); if (wizardPhase === "skills") { body.innerHTML = '
'; try { if (!wizardSkillsData) { const res = await fetch("skills/index.json"); if (!res.ok) throw new Error(`HTTP ${res.status}`); wizardSkillsData = await res.json(); } await Promise.all( wizardSkillsData.map(async (file) => { if (!wizardSkillsCache[file]) { const res = await fetch(`skills/${file}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); wizardSkillsCache[file] = await res.json(); } }), ); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; return; } renderWizardSkills(); } else if (wizardPhase === "integrations") { body.innerHTML = '
'; try { if (!wizardIntegrationsData) { const res = await fetch("integrations/index.json"); if (!res.ok) throw new Error(`HTTP ${res.status}`); wizardIntegrationsData = await res.json(); } await Promise.all( wizardIntegrationsData.map(async (file) => { if (!wizardIntegrationsCache[file]) { const res = await fetch(`integrations/${file}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); wizardIntegrationsCache[file] = await res.json(); } }), ); } catch (err) { body.innerHTML = `

${escHtml(err.message)}

`; return; } renderWizardIntegrations(); } else if (wizardPhase === "fields") { renderWizardFields(); } else if (wizardPhase === "review") { renderWizardReview(); } updateWizardNav(); } function renderWizardSkills() { const items = (wizardSkillsData || []) .map((file, i) => { const s = wizardSkillsCache[file] || {}; return ``; }) .join(""); document.getElementById("wizard-body").innerHTML = `
${items}
`; } function renderWizardIntegrations() { const items = (wizardIntegrationsData || []) .map((file, i) => { const s = wizardIntegrationsCache[file] || {}; return ``; }) .join(""); document.getElementById("wizard-body").innerHTML = `
${items}
`; } function renderWizardFields() { const file = wizardSelectedIntegrationList[wizardIntegrationStep]; const integration = file ? wizardIntegrationsCache[file] : null; if (!integration) { wizardPhase = "review"; renderWizardStep(); return; } const saved = wizardFieldValues[wizardIntegrationStep] || {}; const fields = integration.fields .map((f) => { const val = saved[f.key] || ""; const input = f.type === "textarea" ? `` : ``; return `
${input}
`; }) .join(""); document.getElementById("wizard-body").innerHTML = `
${fields}

${escHtml(integration.signup_label)}

`; } function renderWizardReview() { const prompt = buildWizardPrompt(); document.getElementById("wizard-body").innerHTML = `

Your init prompt is ready! Copy it and paste it directly into your Hermes instance — it will automatically install and configure all selected skills and integrations for you.

`; } function buildWizardPrompt() { const parts = []; const skillLines = []; for (const i of wizardSelectedSkills) { const file = wizardSkillsData && wizardSkillsData[i]; const skill = file && wizardSkillsCache[file]; if (skill && skill.prompt) skillLines.push(skill.prompt); } if (skillLines.length) parts.push("## Skills\n\n" + skillLines.join("\n\n")); const intLines = []; for ( let stepIdx = 0; stepIdx < wizardSelectedIntegrationList.length; stepIdx++ ) { const file = wizardSelectedIntegrationList[stepIdx]; const integration = wizardIntegrationsCache[file]; if (!integration) continue; const values = wizardFieldValues[stepIdx] || {}; let prompt = integration.prompt || ""; for (const [k, v] of Object.entries(values)) { prompt = prompt.split(`{${k}}`).join(v || `{${k}}`); } intLines.push(`### ${integration.name}\n${prompt}`); } if (intLines.length) parts.push("## Integrations\n\n" + intLines.join("\n\n")); return parts.join("\n\n---\n\n") || "(No skills or integrations selected.)"; } function updateWizardNav() { const backBtn = document.getElementById("wizard-back-btn"); const nextBtn = document.getElementById("wizard-next-btn"); const cardTitle = document.getElementById("wizard-card-title"); let label = ""; if (wizardPhase === "skills") label = "Select Skills"; else if (wizardPhase === "integrations") label = "Select Integrations"; else if (wizardPhase === "fields") { const file = wizardSelectedIntegrationList[wizardIntegrationStep]; const int = file ? wizardIntegrationsCache[file] : null; label = int ? int.name : ""; } else if (wizardPhase === "review") label = "Review Init Prompt"; if (cardTitle) cardTitle.textContent = label; if (backBtn) { backBtn.style.display = wizardPhase === "skills" ? "none" : ""; backBtn.onclick = wizardBack; } if (nextBtn) { nextBtn.textContent = wizardPhase === "review" ? "Copy & Finish" : "Next \u2192"; nextBtn.onclick = wizardNext; } } function wizardToggleSkill(index, checked) { if (checked) wizardSelectedSkills.add(index); else wizardSelectedSkills.delete(index); } function wizardToggleIntegration(index, checked) { if (checked) wizardSelectedIntegrations.add(index); else wizardSelectedIntegrations.delete(index); } function wizardSetField(stepKey, fieldKey, value) { if (!wizardFieldValues[stepKey]) wizardFieldValues[stepKey] = {}; wizardFieldValues[stepKey][fieldKey] = value; } function wizardFilter(input, listId) { const q = input.value.toLowerCase().trim(); const list = document.getElementById(listId); if (!list) return; list.querySelectorAll(".wizard-list-item").forEach((item) => { const name = (item.dataset.name || "").toLowerCase(); const desc = (item.dataset.desc || "").toLowerCase(); item.style.display = !q || name.includes(q) || desc.includes(q) ? "" : "none"; }); } function wizardNext() { if (wizardPhase === "skills") { wizardPhase = "integrations"; renderWizardStep(); } else if (wizardPhase === "integrations") { wizardSelectedIntegrationList = (wizardIntegrationsData || []).filter( (_, i) => wizardSelectedIntegrations.has(i), ); wizardIntegrationStep = 0; wizardPhase = wizardSelectedIntegrationList.length ? "fields" : "review"; renderWizardStep(); } else if (wizardPhase === "fields") { wizardIntegrationStep++; if (wizardIntegrationStep >= wizardSelectedIntegrationList.length) { wizardPhase = "review"; } renderWizardStep(); } else if (wizardPhase === "review") { wizardCopyAndFinish(); } } function wizardBack() { if (wizardPhase === "integrations") { wizardPhase = "skills"; renderWizardStep(); } else if (wizardPhase === "fields") { if (wizardIntegrationStep > 0) { wizardIntegrationStep--; } else { wizardPhase = "integrations"; } renderWizardStep(); } else if (wizardPhase === "review") { if (wizardSelectedIntegrationList.length > 0) { wizardPhase = "fields"; wizardIntegrationStep = wizardSelectedIntegrationList.length - 1; } else { wizardPhase = "integrations"; } renderWizardStep(); } } async function wizardCopyAndFinish() { const textarea = document.getElementById("wizard-prompt-textarea"); if (!textarea) return; let copied = false; try { await navigator.clipboard.writeText(textarea.value); copied = true; } catch (_) { toast("Could not copy to clipboard — please copy manually.", "error"); return; } // Mark wizard complete via the agent-info endpoint (non-fatal) try { await apiFetch(AGENT_INFO_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ uuid: activeUUID, key: "WIZZARD", value: "false", }), }); } catch (_) { /* non-fatal */ } // Show success screen inside the wizard instead of closing immediately const cardTitle = document.getElementById("wizard-card-title"); if (cardTitle) cardTitle.textContent = "Prompt Copied"; document.getElementById("wizard-body").innerHTML = `

Your init prompt is in the clipboard.

Open your agent’s console and paste the prompt (Ctrl + V or Cmd + V) to apply the configuration.

`; const backBtn = document.getElementById("wizard-back-btn"); const nextBtn = document.getElementById("wizard-next-btn"); if (backBtn) backBtn.style.display = "none"; if (nextBtn) { nextBtn.textContent = "Done"; nextBtn.onclick = () => { hide("wizard-card"); const tb = document.getElementById("wizard-toggle-btn"); if (tb) tb.textContent = "Integrations"; }; } } function wizardDone() { hide("wizard-card"); const tb = document.getElementById("wizard-toggle-btn"); if (tb) tb.textContent = "Integrations"; } /* ═══════════════════════════════════════════════════════════════ CONFIRM DIALOG ═══════════════════════════════════════════════════════════════ */ let _confirmResolve = null; function confirmDialog(title, message, okLabel = "Confirm") { document.getElementById("confirm-title").textContent = title; document.getElementById("confirm-message").textContent = message; document.getElementById("confirm-ok-btn").textContent = okLabel; document.getElementById("confirm-backdrop").classList.add("open"); return new Promise((resolve) => { _confirmResolve = resolve; }); } function confirmClose(result) { document.getElementById("confirm-backdrop").classList.remove("open"); if (_confirmResolve) { _confirmResolve(result); _confirmResolve = null; } } /* ═══════════════════════════════════════════════════════════════ TOAST ═══════════════════════════════════════════════════════════════ */ function toast(msg, type = "info", duration = 4000) { const el = document.createElement("div"); el.className = `toast toast-${type}`; el.textContent = msg; document.getElementById("toast-container").appendChild(el); setTimeout(() => { el.style.opacity = "0"; el.style.transform = "translateX(50px)"; setTimeout(() => el.remove(), 320); }, duration); } /* ═══════════════════════════════════════════════════════════════ UTILITIES ═══════════════════════════════════════════════════════════════ */ /* ─── Cookie helpers ───────────────────────────────────────── */ function setCookie(name, value, days) { const expires = new Date(Date.now() + days * 864e5).toUTCString(); document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Strict`; } function getCookie(name) { return document.cookie.split("; ").reduce((acc, c) => { const [k, ...rest] = c.split("="); return k === name ? decodeURIComponent(rest.join("=")) : acc; }, null); } function deleteCookie(name) { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/; SameSite=Strict`; } /* ─── Authenticated fetch ───────────────────────────────────── */ // Wraps fetch() for all webhook calls — injects session header when logged in. async function apiFetch(url, options = {}) { const headers = { ...(options.headers || {}), ...(currentSession ? { "X-Session-Id": currentSession } : {}), }; const res = await fetch(url, { ...options, headers }); if (res.status === 401) { deleteCookie("al_session"); deleteCookie("al_email"); localStorage.removeItem("al_email"); location.reload(); } return res; } function escHtml(s) { if (s == null) return ""; return String(s) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function escAttr(s) { return escHtml(s).replace(/'/g, "'"); } function isValidEmail(s) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s); } // Format backup start "2026-06-01T17:55:07.000000" → "2026-06-01 17:55" function formatBackupDate(s) { if (!s) return "—"; // Slice to minute precision and swap T for a space const safe = String(s).slice(0, 16).replace("T", " "); return safe.length >= 16 ? safe : "—"; } function formatDate(d) { if (!d) return "\u2014"; try { return new Date(d).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); } catch (_) { return String(d); } } function show(id) { document.getElementById(id).style.display = ""; } function hide(id) { document.getElementById(id).style.display = "none"; } /* Button loading helpers */ function btnLoad(btn, label) { btn.disabled = true; btn._origHTML = btn.innerHTML; btn.innerHTML = `
${label}`; } function btnReset(btn) { btn.disabled = false; btn.innerHTML = btn._origHTML || ""; } /* Auth error helpers */ function showAuthError(id, msg) { const el = document.getElementById(id); el.textContent = msg; el.style.display = "flex"; } function hideAuthError(id) { document.getElementById(id).style.display = "none"; if (id === "signin-error") { const wrap = document.getElementById("reset-pw-wrap"); if (wrap) wrap.style.display = "none"; } } function showResetOption() { const wrap = document.getElementById("reset-pw-wrap"); if (!wrap) return; // Return to idle state in case it was previously used document.getElementById("reset-pw-idle").style.display = "flex"; document.getElementById("reset-pw-sent").style.display = "none"; wrap.style.display = "block"; } async function sendPasswordReset() { const email = document.getElementById("signin-email").value.trim(); if (!email) { toast("Enter your email address above first.", "warning"); return; } const btn = document.getElementById("reset-pw-btn"); btnLoad(btn, "Sending…"); try { await fetch(PW_RESET_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email }), }); } catch (_) { // Always show sent — never reveal whether the address exists } finally { // Hide the red error panel — it's no longer relevant once reset is requested document.getElementById("signin-error").style.display = "none"; document.getElementById("reset-pw-idle").style.display = "none"; document.getElementById("reset-pw-sent").style.display = "flex"; } } /* ═══════════════════════════════════════════════════════════════ TALK TO US ═══════════════════════════════════════════════════════════════ */ function toggleTalk() { talkOpen = !talkOpen; document.getElementById("talk-panel").classList.toggle("open", talkOpen); document .getElementById("talk-toggle-btn") .classList.toggle("active", talkOpen); const chev = document.getElementById("talk-chevron"); chev.style.transform = talkOpen ? "rotate(180deg)" : ""; if (talkOpen) { if (!chatId) { chatId = String(Math.floor(100000 + Math.random() * 900000)); appendChatMsg("bot", "Hi! How can we help you today?"); } document.getElementById("refer-own-email").textContent = currentEmail || ""; setTimeout(() => document.getElementById("chat-input").focus(), 280); } } function appendChatMsg(from, text) { const msgs = document.getElementById("chat-msgs"); const div = document.createElement("div"); div.className = `chat-msg chat-msg-${from}`; div.textContent = text; msgs.appendChild(div); msgs.scrollTop = msgs.scrollHeight; } function handleChatKey(e) { if (e.key === "Enter") { e.preventDefault(); sendChat(); } } async function sendChat() { const input = document.getElementById("chat-input"); const btn = document.getElementById("chat-send-btn"); const msg = input.value.trim(); if (!msg) return; appendChatMsg("user", msg); input.value = ""; btnLoad(btn, "…"); try { dbg("→ Chat", CHAT_URL); const res = await apiFetch(CHAT_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: msg, sessionId: chatId, email: currentEmail, }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const json = await res.json(); dbg("← Chat", json); const first = Array.isArray(json) ? json[0] : json; const reply = typeof json === "string" ? json : (first && (first.output || first.message || first.reply || first.response || first.text)) || JSON.stringify(json); appendChatMsg("bot", reply); } catch (err) { appendChatMsg("bot", `Sorry, something went wrong (${err.message}).`); } finally { btnReset(btn); document.getElementById("chat-input").focus(); } } async function sendReferral() { const nameInput = document.getElementById("name"); const input = document.getElementById("refer-email"); const btn = document.getElementById("refer-send-btn"); const name = nameInput.value.trim(); const email = input.value.trim(); if (!name) { toast("Please enter your friend\u2019s name.", "warning"); return; } if (!email) { toast("Please enter your friend\u2019s email.", "warning"); return; } if (!isValidEmail(email)) { toast("Please enter a valid email address.", "warning"); return; } btnLoad(btn, "Sending\u2026"); try { dbg("\u2192 Referral", REFERRAL_URL); const res = await apiFetch(REFERRAL_URL, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ referrer: currentEmail, friend_name: name, friend_email: email, uuid: activeUUID, }), }); if (!res.ok) throw new Error(`Failed to send invite (HTTP ${res.status}).`); const json = await res.json().catch(() => null); dbg("\u2190 Referral", json); const first = Array.isArray(json) ? json[0] : json; const result = first && first.result; const success = typeof result === "string" && result.toLowerCase().includes("invite send"); showReferralResult( success ? result : "This email address has already been claimed.", success, ); if (success) { input.value = ""; nameInput.value = ""; } } catch (err) { toast(err.message, "error"); } finally { btnReset(btn); } } function showReferralResult(text, success = true) { let el = document.getElementById("refer-result"); if (!el) { el = document.createElement("p"); el.id = "refer-result"; el.className = "refer-result"; document .getElementById("refer-send-btn") .closest(".refer-input-row") .insertAdjacentElement("afterend", el); } el.textContent = text; el.style.color = success ? "var(--success)" : "var(--warning)"; }