/* ════════════════════════════════════════════════════════════════ 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`; // hidden form POST → 302 Stripe redirect const INVOICES_URL = `${API_BASE}/invoices`; // GET ?email= const INVOICE_EMAIL_URL = `${API_BASE}/user`; // POST {email, invoice_email} 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 = []; let invoicesData = []; // cached full invoice list for filtering/export /* ─── 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 [{data:[…]}] envelope (agents list with nested data array) if ( raw.length > 0 && raw[0] !== null && typeof raw[0] === "object" && Array.isArray(raw[0].data) ) { return normalizeAgents(raw[0].data); } // 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 (skip while modal is open) document.addEventListener("click", (e) => { const wrap = document.getElementById("user-panel-wrap"); const modal = document.getElementById("account-backdrop"); if (modal && modal.classList.contains("open")) return; 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 = ` + Add Agent`; // 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 = `
${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 = [ `${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", }) : ""; const statusColor = k.disabled ? "var(--danger)" : "var(--success)"; const statusLabel = k.disabled ? "Disabled" : "Active"; return `Could not load invoices.
'; } } function applyInvoiceFilter() { const q = ( document.getElementById("invoice-filter").value || "" ).toLowerCase(); const filtered = q ? invoicesData.filter( (inv) => (inv.date || "").toLowerCase().includes(q) || (inv.uuid || "").toLowerCase().includes(q) || (inv.product || "").toLowerCase().includes(q) || (inv.price || "").toLowerCase().includes(q), ) : invoicesData; renderInvoices(filtered); } function renderInvoices(data) { const body = document.getElementById("invoices-body"); if (!data.length) { body.innerHTML = 'No invoices yet.
'; return; } body.innerHTML = `