diff --git a/app.js b/app.js
index dd92abb..987bd9f 100644
--- a/app.js
+++ b/app.js
@@ -1238,6 +1238,7 @@ function renderInvoices(data) {
Agent
Product
Price
+
${data
.map(
@@ -1247,6 +1248,7 @@ function renderInvoices(data) {
${escHtml(inv.uuid || "—")}
${escHtml(inv.product || "—")}
€ ${escHtml(String(inv.price ?? "—"))}
+ ${inv.invoice_link ? `Download ` : ""}
`,
)
.join("")}
@@ -1270,12 +1272,16 @@ function exportInvoicesCSV() {
toast("No invoices to export.", "warning");
return;
}
- const rows = ["Date,Agent,Product,Price"];
+ const rows = ["Date,Agent,Product,Price,Invoice Link"];
for (const inv of filtered) {
rows.push(
- [inv.date || "", inv.uuid || "", inv.product || "", inv.price || ""].join(
- ",",
- ),
+ [
+ inv.date || "",
+ inv.uuid || "",
+ inv.product || "",
+ inv.price || "",
+ inv.invoice_link || "",
+ ].join(","),
);
}
const blob = new Blob([rows.join("\n")], { type: "text/csv" });
@@ -1288,941 +1294,3 @@ function exportInvoicesCSV() {
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 = '
';
-
- 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;
- }
-
- headerDot.className = "contract-dot contract-dot--green";
- headerDot.style.visibility = "visible";
-
- const item = data[0];
- const product = stripQuotes(item.product || "");
- const period = stripQuotes(item.period || "");
- const type = stripQuotes(item.type || "");
-
- const label =
- product && period
- ? `${product} \u2013 ${period}`
- : product || period || type || "—";
-
- body.innerHTML = `
-
-
- ${escHtml(label)}
-
-
`;
-}
-
-/* ═══════════════════════════════════════════════════════════════
- 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)";
-}
diff --git a/styles.css b/styles.css
index 035b9e1..c612919 100644
--- a/styles.css
+++ b/styles.css
@@ -1585,7 +1585,7 @@ body::after {
.invoice-row {
display: grid;
- grid-template-columns: 100px 1fr 1fr 80px;
+ grid-template-columns: 100px 1fr 1fr 80px 80px;
gap: 10px;
align-items: center;
padding: 7px 10px;
@@ -1641,6 +1641,10 @@ body::after {
white-space: nowrap;
}
+.invoice-dl {
+ text-align: right;
+}
+
/* ─── Credits Modal ────────────────────────────────────────── */
.credits-key-label {
font-size: 12px;