/* ════════════════════════════════════════════════════════════════
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 PORTAL_URL = `${API_BASE}/account/portal`; // GET ?email= → { url }
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 = `
hermes
EU
bell
EU
+ 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 = `
Default Key
Active
350
/ 500 credits remaining
·
Expires Dec 31, 2026
Buy Credits
`;
// Server info
document.getElementById("server-info-body").innerHTML = `
Agent Type
Hermes
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--red";
demoDot.style.visibility = "visible";
document.getElementById("contract-body").innerHTML = `
`;
// Backups
document.getElementById("backups-body").innerHTML = `
`;
}
/* ═══════════════════════════════════════════════════════════════
AGENTS — load & render tabs
═══════════════════════════════════════════════════════════════ */
async function loadAgents() {
const bar = document.getElementById("agent-tabs-bar");
bar.innerHTML =
'';
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 || "";
const expires = a.expires || "";
const hasExpiry = expires && expires !== "null" && expires !== "";
const dotClass = hasExpiry
? "agent-tab-dot agent-tab-dot--red"
: "agent-tab-dot";
return `
${escHtml(uuid)}
${escHtml(server.toUpperCase())}
`;
})
.join("") +
`+ Add Agent `;
}
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))}
`,
``,
`
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",
})
: "";
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
${exp ? `· ${escHtml(exp)} ` : ""}
Buy Credits
`;
})
.join("");
}
function buyCredits(keyName) {
const backdrop = document.getElementById("credits-backdrop");
backdrop._keyName = keyName;
document.getElementById("credits-key-name").textContent = keyName;
// default to first option
const radios = backdrop.querySelectorAll('input[name="credits-amount"]');
radios.forEach((r, i) => (r.checked = i === 0));
backdrop.classList.add("open");
}
function buyCreditsClose() {
document.getElementById("credits-backdrop").classList.remove("open");
}
function buyCreditsSubmit() {
const backdrop = document.getElementById("credits-backdrop");
const selected = backdrop.querySelector(
'input[name="credits-amount"]:checked',
);
if (!selected) {
toast("Please select an amount.", "warning");
return;
}
const keyName = backdrop._keyName;
const params = new URLSearchParams(window.location.search);
const activeAgent = agents.find((a) => a.UUID === activeUUID);
const couponInput = document.getElementById("coupon-input");
const coupon = couponApplied && couponInput ? couponInput.value.trim() : "";
const fields = {
product: "prod_Ufj2wCf4rhemT8",
key: keyName || "",
value: selected.value,
agent: activeUUID || "",
period: "",
location: activeAgent?.server || "",
email: currentEmail || "",
coupon,
utm_source: params.get("utm_source") || "",
utm_medium: params.get("utm_medium") || "",
utm_campaign: params.get("utm_campaign") || "",
utm_term: params.get("utm_term") || "",
utm_content: params.get("utm_content") || "",
};
const form = document.createElement("form");
form.method = "POST";
form.action = CREDITS_URL;
form.target = "_self";
for (const [name, val] of Object.entries(fields)) {
const inp = document.createElement("input");
inp.type = "hidden";
inp.name = name;
inp.value = val;
form.appendChild(inp);
}
document.body.appendChild(form);
form.submit();
}
/* ═══════════════════════════════════════════════════════════════
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(
"Your Hermes & Root 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 backdrop = document.getElementById("account-backdrop");
if (!backdrop) return;
backdrop.classList.add("open");
const emailEl = document.getElementById("acct-email-display");
if (emailEl) emailEl.textContent = currentEmail || "—";
loadUserData();
loadInvoices();
}
async function loadUserData() {
try {
const res = await apiFetch(
`${INVOICE_EMAIL_URL}?email=${encodeURIComponent(currentEmail || "")}`,
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
let data = await res.json();
dbg("\u2190 User data", data);
// unwrap envelope: plain array or {data:[...]}
if (Array.isArray(data)) {
const first = data[0];
data =
first && typeof first === "object" && "json" in first
? first.json
: first;
} else if (data && !Array.isArray(data) && Array.isArray(data.data)) {
data = data.data[0];
}
if (!data) return;
const invoiceEmail = stripQuotes(
data.invoice_mail || data.invoice_email || data.invoiceEmail || "",
);
const invInput = document.getElementById("invoice-email-input");
if (invInput && invoiceEmail) invInput.value = invoiceEmail;
const code = stripQuotes(
data.affiliate_code ||
data.affiliateCode ||
data.referral_code ||
data.referralCode ||
"",
);
const codeEl = document.getElementById("acct-affiliate-code");
const copyBtn = document.getElementById("acct-affiliate-copy-btn");
if (codeEl) codeEl.textContent = code || "—";
if (copyBtn) copyBtn.style.display = code ? "inline-flex" : "none";
} catch (err) {
dbg("\u2190 User data error", err.message);
}
}
function copyAffiliateCode() {
const code = document.getElementById("acct-affiliate-code")?.textContent;
if (!code || code === "—") return;
const link = `https://derez.ai?utm_source=${encodeURIComponent(code)}`;
navigator.clipboard
.writeText(link)
.then(() => toast("Affiliate link copied!", "success"));
}
function closeUserPanel() {
document.getElementById("account-backdrop").classList.remove("open");
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";
const invBtn = document.getElementById("invoice-email-save-btn");
if (invBtn) invBtn.style.display = "none";
const codeEl = document.getElementById("acct-affiliate-code");
if (codeEl) codeEl.textContent = "—";
const copyBtn = document.getElementById("acct-affiliate-copy-btn");
if (copyBtn) copyBtn.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);
}
}
function toggleInvoiceEmailSave(value) {
const btn = document.getElementById("invoice-email-save-btn");
if (btn) btn.style.display = value ? "flex" : "none";
}
async function saveInvoiceEmail() {
const input = document.getElementById("invoice-email-input");
const btn = document.getElementById("invoice-email-save-btn");
const email = input.value.trim();
if (!email || !isValidEmail(email)) {
toast("Please enter a valid email address.", "warning");
return;
}
btnLoad(btn, "");
try {
const res = await apiFetch(INVOICE_EMAIL_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: currentEmail, invoice_email: email }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
localStorage.setItem("al_invoice_email", email);
toast("Invoice email saved.", "success");
btn.style.display = "none";
} catch (err) {
toast(err.message, "error");
} finally {
btnReset(btn);
}
}
async function openBillingPortal() {
const btn = document.getElementById("portal-btn");
if (!currentEmail) {
toast("No email found. Please sign in again.", "warning");
return;
}
if (btn) btnLoad(btn, "");
try {
const res = await apiFetch(
`${PORTAL_URL}?email=${encodeURIComponent(currentEmail)}`,
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
dbg("\u2190 Portal", data);
const portalUrl =
data.URL || data.url || (data.data && data.data.url) || data;
if (
portalUrl &&
typeof portalUrl === "string" &&
portalUrl.startsWith("http")
) {
window.open(portalUrl, "_blank");
} else {
throw new Error("No portal URL returned.");
}
} catch (err) {
toast(err.message, "error");
} finally {
if (btn) btnReset(btn);
}
}
async function loadInvoices() {
const body = document.getElementById("invoices-body");
if (!body) return;
body.innerHTML = '';
try {
const res = await apiFetch(
`${INVOICES_URL}?email=${encodeURIComponent(currentEmail || "")}`,
);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
let data = await res.json();
dbg("\u2190 Invoices", data);
// unwrap envelope: {data:[...]} or [{json:{...}}] or plain array
if (data && !Array.isArray(data) && Array.isArray(data.data)) {
data = data.data;
} else if (Array.isArray(data)) {
data = data.map((item) =>
item && typeof item === "object" && "json" in item ? item.json : item,
);
} else {
data = [];
}
invoicesData = data;
applyInvoiceFilter();
const filterInput = document.getElementById("invoice-filter");
if (filterInput) filterInput.value = "";
} catch (err) {
dbg("\u2190 Invoices error", err.message);
body.innerHTML =
'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 = `
Date
Agent
Product
Price
PDF
${data
.map(
(inv) => `
${escHtml(inv.date || "—")}
${escHtml(inv.uuid || "—")}
${escHtml(inv.product || "—")}
€ ${escHtml(String(inv.price ?? "—"))}
${inv.invoice_link ? ` ` : `— `}
`,
)
.join("")}
`;
}
function exportInvoicesCSV() {
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;
if (!filtered.length) {
toast("No invoices to export.", "warning");
return;
}
const rows = ["Date,Agent,Product,Price"];
for (const inv of filtered) {
rows.push(
[inv.date || "", inv.uuid || "", inv.product || "", inv.price || ""].join(
",",
),
);
}
const blob = new Blob([rows.join("\n")], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "invoices.csv";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/* ═══════════════════════════════════════════════════════════════
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 = '';
// Check BACKUP flag from agents list
const agent = agents.find((a) => (a.UUID || a.uuid) === uuid);
const hasBackup =
agent &&
!(
agent.BACKUP === null ||
agent.BACKUP === undefined ||
agent.BACKUP === "null" ||
agent.BACKUP === false
);
const btn = document.getElementById("make-backup-btn");
if (btn) btn.style.display = hasBackup ? "" : "none";
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 `
`;
})
.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 product = stripQuotes(item.product || "");
const period = stripQuotes(item.period || "");
const type = stripQuotes(item.type || "");
// Look up expires from the agent data (new field in agents list API)
const agent = agents.find((a) => (a.UUID || a.uuid) === activeUUID);
const rawExpires =
agent && agent.expires
? String(agent.expires).replace(/^"|"$/g, "").trim()
: "";
const hasExpiry = rawExpires && rawExpires !== "null" && rawExpires !== "";
const expires = hasExpiry ? rawExpires : "";
const label =
product && period
? `${product} \u2013 ${period}`
: product || period || type || "—";
headerDot.className = hasExpiry
? "contract-dot contract-dot--red"
: "contract-dot contract-dot--green";
headerDot.style.visibility = "visible";
let html = `
${escHtml(label)} `;
if (hasExpiry) {
html += `
expires ${escHtml(expires)} `;
}
html += `
`;
if (hasExpiry) {
html += `
Resubscribe → `;
}
html += `
`;
body.innerHTML = html;
}
/* ═══════════════════════════════════════════════════════════════
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 `
${escHtml(s.name || file)}
${s.description ? `${escHtml(s.description)} ` : ""}
`;
})
.join("");
document.getElementById("wizard-body").innerHTML = `
`;
}
function renderWizardIntegrations() {
const items = (wizardIntegrationsData || [])
.map((file, i) => {
const s = wizardIntegrationsCache[file] || {};
return `
${escHtml(s.name || file)}
${s.description ? `${escHtml(s.description)} ` : ""}
`;
})
.join("");
document.getElementById("wizard-body").innerHTML = `
`;
}
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 `${escHtml(f.label)} ${input}
`;
})
.join("");
document.getElementById("wizard-body").innerHTML = `
`;
}
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)";
}