/**
 * aide — web UI JavaScript
 *
 * Handles:
 *  - WebSocket connection & reconnect
 *  - Streaming chat (text + tool indicators)
 *  - Confirmation modal
 *  - Pause/resume
 *  - Shared helpers (status bar, nav)
 */

/* ── Web UI fetch marker ─────────────────────────────────────────────────── */
// Adds X-Requested-By: aide-ui to every /api/* call made by the web UI.
// Combined with the session cookie this identifies legitimate web UI requests.
// Swagger's built-in fetch does NOT add this header, so it must use the API key.
(function () {
  const _orig = window.fetch.bind(window);
  window.fetch = function (url, opts) {
    const urlStr = typeof url === "string" ? url : (url && url.url) || "";
    if (urlStr.startsWith("/api/")) {
      opts = opts ? { ...opts } : {};
      opts.headers = { "X-Requested-By": "aide-ui", ...(opts.headers || {}) };
    }
    return _orig(url, opts);
  };
})();

/* ── Global state ────────────────────────────────────────────────────────── */

let ws = null;
let sessionId = null;  // set lazily in initChat() after inline scripts run
let reconnectDelay = 1000;
let isGenerating = false;

/* ── Utility ─────────────────────────────────────────────────────────────── */

function esc(str) {
  return String(str)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#39;");
}

function formatBytes(n) {
  if (n < 1024) return n + " B";
  if (n < 1048576) return (n / 1024).toFixed(1) + " KB";
  return (n / 1048576).toFixed(1) + " MB";
}

const _TZ = "Europe/Oslo";

function _dtParts(iso, opts) {
  const parts = {};
  for (const p of new Intl.DateTimeFormat("en-GB", { ...opts, timeZone: _TZ }).formatToParts(new Date(iso)))
    parts[p.type] = p.value;
  return parts;
}

function formatDate(iso) {
  if (!iso) return "—";
  try {
    const v = _dtParts(iso, { day:"2-digit", month:"2-digit", year:"numeric",
                              hour:"2-digit", minute:"2-digit", second:"2-digit", hour12:false });
    return `${v.day}.${v.month}.${v.year} ${v.hour}:${v.minute}:${v.second}`;
  } catch { return iso; }
}
function formatDateShort(iso) {
  if (!iso) return "—";
  try {
    const v = _dtParts(iso, { day:"2-digit", month:"2-digit", year:"numeric" });
    return `${v.day}.${v.month}.${v.year}`;
  } catch { return iso; }
}

/** Parse "dd.mm.yyyy" or "dd.mm.yyyy HH:MM" → ISO UTC string, or null if invalid. */
function parseEuDate(str, isEnd = false) {
  if (!str?.trim()) return null;
  const m = str.trim().match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:[T ](\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
  if (!m) return null;
  const [, d, mo, y, h = "00", mi = "00", s] = m;
  const ss = s ?? (isEnd ? "59" : "00");
  return `${y}-${mo.padStart(2,"0")}-${d.padStart(2,"0")}T${h.padStart(2,"0")}:${mi}:${ss}Z`;
}

/* ── Status bar (shared) ─────────────────────────────────────────────────── */

async function refreshStatus() {
  try {
    const r = await fetch("/api/status");
    const data = await r.json();
    const dot = document.getElementById("status-dot");
    const label = document.getElementById("status-label");

    if (dot && label) {
      if (data.paused) {
        dot.className = "status-dot paused";
        label.textContent = "Paused";
      } else {
        dot.className = "status-dot";
        label.textContent = "Running";
      }
    }

    // Update sidebar pause button
    const sidebarBtn = document.getElementById("pause-btn");
    if (sidebarBtn) {
      sidebarBtn.classList.toggle("paused", data.paused);
      sidebarBtn.title = data.paused ? "Resume agent" : "Pause agent";
      const icon = sidebarBtn.querySelector(".btn-icon");
      if (icon) icon.textContent = data.paused ? "\u25b6" : "\u23f8";
      const label = sidebarBtn.querySelector(".btn-label");
      if (label) label.textContent = data.paused ? "Resume" : "Pause";
    }
    // Update settings page pause button (has a different id to avoid duplicate)
    const settingsBtn = document.getElementById("settings-pause-btn");
    if (settingsBtn) {
      settingsBtn.className = `btn ${data.paused ? "btn-ghost paused" : "btn-primary"}`;
      const icon = settingsBtn.querySelector(".btn-icon");
      if (icon) icon.textContent = data.paused ? "▶" : "⏸";
      const textNode = Array.from(settingsBtn.childNodes).find(n => n.nodeType === Node.TEXT_NODE && n.textContent.trim());
      if (textNode) textNode.textContent = data.paused ? " Resume agent" : " Pause agent";
    }
  } catch { /* ignore */ }
}

async function togglePause() {
  const pauseBtn = document.getElementById("pause-btn");
  const isPaused = pauseBtn?.classList.contains("paused");
  await fetch(isPaused ? "/api/resume" : "/api/pause", { method: "POST" });
  await refreshStatus();
}

/* ── Navigation (SPA) ───────────────────────────────────────────────────── */

// Snapshot of chat state while on another page
let _chatSnapshot = null;      // { html, sessionId }
let _skipNextRestore = false;  // true when snapshot was just restored — skip WS restore event
let _cachedModels = null;  // { models, default, capabilities } — cached from WS "models" message
let _modelCapabilities = {};  // { "provider:model-id": { vision, tools, online } }

/* ── PWA install ─────────────────────────────────────────────────────────── */
let _deferredInstallPrompt = null;

window.addEventListener("beforeinstallprompt", e => {
  e.preventDefault();
  _deferredInstallPrompt = e;
  const btn = document.getElementById("pwa-install-btn");
  if (btn) btn.style.display = "flex";
});

function handleInstall() {
  const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
  const isStandalone = window.navigator.standalone === true;
  if (isStandalone) return;

  if (_deferredInstallPrompt) {
    _deferredInstallPrompt.prompt();
    _deferredInstallPrompt.userChoice.then(() => { _deferredInstallPrompt = null; });
  } else if (isIOS) {
    alert("To add to your Home Screen:\n1. Tap the Share button (\u25a1\u2191) in Safari's toolbar\n2. Tap \"Add to Home Screen\"");
  }
}

(function _initPWA() {
  const isIOS = /iphone|ipad|ipod/i.test(navigator.userAgent);
  const isStandalone = window.navigator.standalone === true || window.matchMedia("(display-mode: standalone)").matches;
  if (isIOS && !isStandalone) {
    const btn = document.getElementById("pwa-install-btn");
    if (btn) btn.style.display = "flex";
  }
})();

function toggleSidebar() {
  const sidebar = document.getElementById("sidebar");
  const overlay = document.getElementById("sidebar-overlay");
  if (!sidebar) return;
  const isOpen = sidebar.classList.toggle("open");
  overlay && overlay.classList.toggle("visible", isOpen);
}

function closeSidebar() {
  const sidebar = document.getElementById("sidebar");
  const overlay = document.getElementById("sidebar-overlay");
  sidebar && sidebar.classList.remove("open");
  overlay && overlay.classList.remove("visible");
}

function initNav() {
  _setActiveNav(location.pathname);

  // Intercept sidebar nav clicks — swap <main> without a full reload
  document.querySelectorAll(".nav-item[data-page]").forEach(el => {
    el.addEventListener("click", e => {
      const href = el.getAttribute("href");
      if (!href || href.startsWith("http")) return;  // external links: normal
      e.preventDefault();
      closeSidebar();
      navigateTo(href);
    });
  });

  // Browser back/forward
  window.addEventListener("popstate", e => {
    navigateTo(location.pathname, { pushState: false });
  });
}

function _setActiveNav(path) {
  document.querySelectorAll(".nav-item[data-page]").forEach(el => {
    const page = el.dataset.page;
    el.classList.toggle("active", page === path || (path === "/" && page === "/"));
  });
}

async function navigateTo(url, { pushState = true } = {}) {
  // Snapshot chat state before leaving the chat page
  const chatMsgs = document.getElementById("chat-messages");
  if (chatMsgs) {
    _chatSnapshot = { html: chatMsgs.innerHTML, sessionId };
  }

  // Fetch the target page HTML and extract <main> content
  let html;
  try {
    const r = await fetch(url);
    html = await r.text();
  } catch (err) {
    console.error("[aide] navigate fetch failed:", err);
    return;
  }

  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");
  const newMain = doc.querySelector("main, #main");
  if (!newMain) return;

  document.getElementById("main").innerHTML = newMain.innerHTML;
  document.title = doc.title || window.AGENT_NAME || "aide";

  // Extract and run any inline <script> tags from the fetched page
  // (e.g. window.SESSION_ID injected by chat.html — but only on a fresh visit)
  if (!_chatSnapshot || url !== "/") {
    doc.querySelectorAll("script:not([src])").forEach(s => {
      if (s.textContent.trim()) {
        const el = document.createElement("script");
        el.textContent = s.textContent;
        document.head.appendChild(el);
        document.head.removeChild(el);
      }
    });
  }

  if (pushState) history.pushState({}, "", url);
  _setActiveNav(url);
  _initPage(url);
}

function _initPage(url) {
  let path;
  try { path = new URL(url, location.origin).pathname; } catch { path = url.split("?")[0]; }
  if (path === "/" || path === "") { initChat();    return; }
  if (path === "/agents")          { initAgents();  return; }
  if (path.startsWith("/agents/")) { initAgentDetail(); return; }
  if (path === "/usage")           { initUsage();   return; }
  if (path === "/audit")           { initAudit();   return; }
  if (path === "/monitors")        { initMonitors(); return; }
  if (path === "/models")          { initModels();  return; }
  if (path === "/settings")        { initSettings(); return; }
  if (path === "/help")            { initHelp();    return; }
  if (path === "/files")           { fileBrowserNavigate(""); return; }
}

/* ══════════════════════════════════════════════════════════════════════════
   FILES PAGE
══════════════════════════════════════════════════════════════════════════ */

let _fbPath = "";
let _uploadPolicy = null;
const _UPLOAD_ALLOWED_EXACT_NAMES = new Set([
  "known_hosts", "authorized_keys", "config",
  "makefile", "procfile", "dockerfile",
  ".env", ".gitignore", ".htaccess",
]);

function _fbFmtSize(bytes) {
  if (bytes === null || bytes === undefined) return "—";
  if (bytes < 1024) return bytes + " B";
  if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
  if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB";
  return (bytes / (1024 * 1024 * 1024)).toFixed(2) + " GB";
}

function _fbFmtDate(iso) {
  if (!iso) return "—";
  return formatDate(iso);
}

function _fbBuildBreadcrumb(path) {
  const el = document.getElementById("file-breadcrumb");
  if (!el) return;
  el.innerHTML = "";

  const root = document.createElement("span");
  root.style.cssText = "cursor:pointer;color:var(--accent)";
  root.textContent = "My Files";
  root.onclick = function() { fileBrowserNavigate(""); };
  el.appendChild(root);

  if (!path) return;

  const parts = path.split("/").filter(Boolean);
  parts.forEach(function(part, i) {
    const sep = document.createElement("span");
    sep.textContent = " / ";
    el.appendChild(sep);

    const crumb = document.createElement("span");
    const isLast = i === parts.length - 1;
    if (isLast) {
      crumb.style.color = "var(--text)";
      crumb.textContent = part;
    } else {
      crumb.style.cssText = "cursor:pointer;color:var(--accent)";
      crumb.textContent = part;
      const targetPath = parts.slice(0, i + 1).join("/");
      crumb.onclick = (function(tp) { return function() { fileBrowserNavigate(tp); }; })(targetPath);
    }
    el.appendChild(crumb);
  });
}

async function fileBrowserNavigate(path) {
  _fbPath = path || "";
  const tbody = document.getElementById("file-tbody");
  const tableWrap = document.getElementById("file-table-wrap");
  const emptyEl = document.getElementById("file-empty");
  const noFolder = document.getElementById("file-no-folder");
  const zipBtn = document.getElementById("dl-zip-btn");
  const uploadBtn = document.getElementById("upload-btn");
  if (!tbody) return;

  tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">Loading\u2026</td></tr>';
  tableWrap.style.display = "";
  emptyEl.style.display = "none";
  noFolder.style.display = "none";

  const r = await fetch("/api/my/files?path=" + encodeURIComponent(_fbPath));

  if (!r.ok) {
    const d = await r.json().catch(function() { return {}; });
    if (d.detail === "no_folder" || d.detail === "folder_missing") {
      tableWrap.style.display = "none";
      noFolder.style.display = "";
      zipBtn.style.display = "none";
      if (uploadBtn) uploadBtn.style.display = "none";
      document.getElementById("file-breadcrumb").innerHTML = "";
    } else {
      tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;color:var(--red)">' + esc(d.detail || "Error loading files") + "</td></tr>";
    }
    return;
  }

  const data = await r.json();
  _fbBuildBreadcrumb(data.path);
  zipBtn.style.display = "";
  if (uploadBtn) uploadBtn.style.display = "";

  if (!data.entries.length) {
    tableWrap.style.display = "none";
    emptyEl.style.display = "";
    return;
  }

  tableWrap.style.display = "";
  emptyEl.style.display = "none";
  tbody.innerHTML = "";

  if (data.parent !== null && data.parent !== undefined) {
    const tr = document.createElement("tr");
    tr.style.cursor = "pointer";
    tr.innerHTML = '<td style="color:var(--text-dim);font-size:16px;text-align:center">\u2191</td><td style="color:var(--text-dim)" colspan="3">..</td><td></td>';
    tr.onclick = (function(p) { return function() { fileBrowserNavigate(p); }; })(data.parent);
    tr.onmouseenter = function() { tr.style.background = "var(--bg2)"; };
    tr.onmouseleave = function() { tr.style.background = ""; };
    tbody.appendChild(tr);
  }

  for (const entry of data.entries) {
    const tr = document.createElement("tr");

    const iconTd = document.createElement("td");
    iconTd.style.textAlign = "center";
    iconTd.innerHTML = entry.is_dir
      ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="color:var(--yellow)"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>'
      : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16" style="color:var(--text-dim)"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>';

    const nameTd = document.createElement("td");
    if (entry.is_dir) {
      nameTd.style.cssText = "cursor:pointer;color:var(--accent)";
      nameTd.textContent = entry.name;
      nameTd.onclick = (function(p) { return function() { fileBrowserNavigate(p); }; })(entry.path);
    } else if (_fbIsTextFile(entry.name)) {
      nameTd.style.cssText = "cursor:pointer;color:var(--accent)";
      nameTd.textContent = entry.name;
      nameTd.onclick = (function(p, n) { return function() { fileBrowserViewFile(p, n); }; })(entry.path, entry.name);
    } else {
      nameTd.textContent = entry.name;
    }

    const sizeTd = document.createElement("td");
    sizeTd.style.cssText = "text-align:right;color:var(--text-dim);font-size:12px";
    sizeTd.textContent = _fbFmtSize(entry.size);

    const dateTd = document.createElement("td");
    dateTd.style.cssText = "color:var(--text-dim);font-size:12px";
    dateTd.textContent = _fbFmtDate(entry.modified);

    const actionTd = document.createElement("td");
    actionTd.style.cssText = "text-align:right;display:flex;justify-content:flex-end;gap:6px;align-items:center";

    const btn = document.createElement("button");
    btn.className = "btn btn-ghost btn-small";
    if (entry.is_dir) {
      btn.title = "Download folder as ZIP";
      btn.textContent = "\u2193 ZIP";
      btn.onclick = (function(p) { return function(e) { e.stopPropagation(); fileBrowserDownloadZip(p); }; })(entry.path);
    } else {
      btn.title = "Download file";
      btn.textContent = "\u2193 Download";
      btn.onclick = (function(p) { return function() { fileBrowserDownloadFile(p); }; })(entry.path);
    }
    actionTd.appendChild(btn);

    // Delete button — files only, protected names excluded
    if (!entry.is_dir && !entry.name.startsWith("memory_") && !entry.name.startsWith("reasoning_")) {
      const delBtn = document.createElement("button");
      delBtn.className = "btn btn-ghost btn-small";
      delBtn.title = "Delete file";
      delBtn.textContent = "Delete";
      delBtn.style.color = "var(--danger,#dc3c3c)";
      delBtn.onclick = (function(p, n) { return function(e) { e.stopPropagation(); fileBrowserDeleteFile(p, n); }; })(entry.path, entry.name);
      actionTd.appendChild(delBtn);
    }

    tr.appendChild(iconTd);
    tr.appendChild(nameTd);
    tr.appendChild(sizeTd);
    tr.appendChild(dateTd);
    tr.appendChild(actionTd);

    if (entry.is_dir) {
      tr.style.cursor = "pointer";
      tr.onmouseenter = function() { tr.style.background = "var(--bg2)"; };
      tr.onmouseleave = function() { tr.style.background = ""; };
      tr.onclick = (function(p) { return function(e) { if (e.target.closest("button")) return; fileBrowserNavigate(p); }; })(entry.path);
    }

    tbody.appendChild(tr);
  }
}

async function _fbFetch(url) {
  const r = await fetch(url, { credentials: "same-origin" });
  if (!r.ok) {
    let detail = r.statusText;
    try { detail = (await r.json()).detail || detail; } catch (ex) { /* ignore */ }
    throw new Error(detail);
  }
  return r;
}

function _fbTriggerBlob(blob, filename) {
  const blobUrl = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = blobUrl;
  a.download = filename;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 2000);
}

async function fileBrowserDownloadFile(path) {
  try {
    const r = await _fbFetch("/api/my/files/download?path=" + encodeURIComponent(path));
    const blob = await r.blob();
    _fbTriggerBlob(blob, path.split("/").pop() || "file");
  } catch (e) {
    alert("Download failed: " + e.message);
  }
}

async function fileBrowserDownloadZip(path) {
  const target = (path !== undefined) ? path : _fbPath;
  try {
    const r = await _fbFetch("/api/my/files/download-zip?path=" + encodeURIComponent(target || ""));
    const blob = await r.blob();
    const name = (target ? target.split("/").pop() : "files") + ".zip";
    _fbTriggerBlob(blob, name);
  } catch (e) {
    alert("Download failed: " + e.message);
  }
}

async function fileBrowserDeleteFile(path, name) {
  if (!confirm("Delete \u201c" + name + "\u201d?\nThis cannot be undone.")) return;
  try {
    const r = await fetch("/api/my/files?path=" + encodeURIComponent(path), {
      method: "DELETE",
      credentials: "same-origin",
    });
    if (!r.ok) {
      let detail = r.statusText;
      try { detail = (await r.json()).detail || detail; } catch (ex) { /* ignore */ }
      alert("Delete failed: " + detail);
      return;
    }
    fileBrowserNavigate(_fbPath);
  } catch (e) {
    alert("Delete failed: " + e.message);
  }
}

function fileBrowserUploadClick() {
  document.getElementById("file-upload-input").click();
}

async function fileBrowserUploadFiles(input) {
  const files = Array.from(input.files);
  input.value = "";
  if (!files.length) return;

  if (!_uploadPolicy) {
    const pr = await fetch("/api/my/files/upload-policy").catch(function() { return null; });
    if (pr && pr.ok) _uploadPolicy = await pr.json().catch(function() { return null; });
  }

  const policy = _uploadPolicy || { allowed_extensions: [], max_file_size_mb: 50, max_files: 20 };
  const allowedExts = new Set((policy.allowed_extensions || []).map(function(e) { return e.toLowerCase(); }));
  const maxBytes = (policy.max_file_size_mb || 50) * 1024 * 1024;
  const maxFiles = policy.max_files || 20;

  if (files.length > maxFiles) {
    alert("Too many files — max " + maxFiles + " per upload.");
    return;
  }

  const clientRejected = [];
  const fd = new FormData();
  for (const f of files) {
    const hasExt = f.name.includes(".");
    const ext = hasExt ? f.name.split(".").pop().toLowerCase() : "";
    const allowed = hasExt
      ? (allowedExts.size === 0 || allowedExts.has(ext))
      : _UPLOAD_ALLOWED_EXACT_NAMES.has(f.name.toLowerCase());
    if (!allowed) {
      clientRejected.push(f.name + (hasExt ? " (type not allowed)" : " (extensionless file not permitted)"));
      continue;
    }
    if (f.size > maxBytes) {
      clientRejected.push(f.name + " (exceeds " + policy.max_file_size_mb + " MB limit)");
      continue;
    }
    fd.append("files", f);
  }

  let uploadedCount = 0;
  const allRejected = [...clientRejected];
  if ([...fd.entries()].length > 0) {
    try {
      const r = await fetch("/api/my/files/upload?path=" + encodeURIComponent(_fbPath), {
        method: "POST",
        credentials: "same-origin",
        body: fd,
      });
      const d = await r.json().catch(function() { return {}; });
      if (!r.ok) { alert("Upload failed: " + (d.detail || r.statusText)); return; }
      uploadedCount = (d.uploaded || []).length;
      for (const rej of (d.rejected || [])) allRejected.push(rej.name + " (" + rej.reason + ")");
    } catch(e) {
      alert("Upload failed: " + e.message);
      return;
    }
  }

  if (uploadedCount > 0) {
    let msg = "Uploaded " + uploadedCount + " file" + (uploadedCount !== 1 ? "s" : "");
    if (allRejected.length) msg += ". Rejected: " + allRejected.join(", ");
    showFlash(msg);
    fileBrowserNavigate(_fbPath);
  } else if (allRejected.length) {
    alert("No files uploaded. Rejected:\n" + allRejected.join("\n"));
  }
}

const _FB_TEXT_EXTS = new Set([
  "md","txt","json","xml","yaml","yml","csv","html","htm","css","js","ts",
  "jsx","tsx","py","sh","bash","zsh","log","sql","toml","ini","conf","cfg",
  "env","gitignore","dockerfile","rst","tex","diff","patch","nfo","tsv",
]);

function _fbIsTextFile(name) {
  const ext = name.includes(".") ? name.split(".").pop().toLowerCase() : "";
  return _FB_TEXT_EXTS.has(ext);
}

async function fileBrowserViewFile(path, name) {
  try {
    const r = await _fbFetch("/api/my/files/view?path=" + encodeURIComponent(path));
    const data = await r.json();
    const modal = document.getElementById("file-viewer-modal");
    document.getElementById("fv-title").textContent = name;
    const pre = document.getElementById("fv-content");
    pre.textContent = data.content;
    const notice = document.getElementById("fv-truncated");
    if (data.truncated) {
      notice.textContent = "File truncated at 512 KB.";
      notice.style.display = "";
    } else {
      notice.style.display = "none";
    }
    modal.style.display = "flex";
  } catch (e) {
    alert("Could not load file: " + e.message);
  }
}

function closeFileViewer() {
  const modal = document.getElementById("file-viewer-modal");
  if (modal) modal.style.display = "none";
}

/* ══════════════════════════════════════════════════════════════════════════
   CHAT PAGE
══════════════════════════════════════════════════════════════════════════ */

function initChat() {
  if (!document.getElementById("chat-messages")) return;

  // Restore snapshot if returning from another page
  if (_chatSnapshot) {
    const chatMsgs = document.getElementById("chat-messages");
    chatMsgs.innerHTML = _chatSnapshot.html;
    chatMsgs.scrollTop = chatMsgs.scrollHeight;
    sessionId = _chatSnapshot.sessionId;
    _chatSnapshot = null;
    _skipNextRestore = true;  // don't let WS restore overwrite the snapshot
  } else {
    // Restore persisted session so refreshes continue the same conversation.
    // If the URL already has ?session=<id> (reopened from Chats page) use that
    // and persist it. Otherwise fall back to localStorage, then server-generated.
    const urlSession = new URLSearchParams(window.location.search).get("session");
    if (urlSession) {
      sessionId = urlSession;
      localStorage.setItem("current_session_id", urlSession);
      // Clean the URL without triggering a reload
      history.replaceState(null, "", "/");
    } else {
      sessionId = localStorage.getItem("current_session_id") || window.SESSION_ID || null;
      if (sessionId !== window.SESSION_ID) {
        // Using a stored session — the WS restore event will render the history
      }
    }
  }

  // Connect WS only if not already open (survives navigation)
  if (!ws || ws.readyState === WebSocket.CLOSED || ws.readyState === WebSocket.CLOSING) {
    connectWS();
  }

  // Always re-attach listeners — DOM was replaced by navigation swap
  const sendBtn   = document.getElementById("send-btn");
  const chatInput = document.getElementById("chat-input");
  const clearBtn  = document.getElementById("clear-btn");

  sendBtn.addEventListener("click", sendMessage);
  chatInput.addEventListener("keydown", e => {
    if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(); }
  });
  clearBtn?.addEventListener("click", clearChat);
  _initAttachHandler();
  _pendingAttachments = [];
  _renderAttachPreview();

  // Auto-resize textarea
  chatInput.addEventListener("input", () => {
    chatInput.style.height = "auto";
    chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + "px";
  });

  // Re-populate model selector if WS already connected (SPA navigation)
  if (_cachedModels) {
    populateModelSelector(_cachedModels.models, _cachedModels.default);
  }

  // Model picker keyboard navigation
  const mpSearch = document.getElementById("model-picker-search");
  if (mpSearch) {
    mpSearch.addEventListener("keydown", e => {
      if (e.key === "Escape") { closeModelPicker(); return; }
      const listEl = document.getElementById("model-picker-list");
      const items = listEl ? Array.from(listEl.querySelectorAll(".mp-item")) : [];
      if (!items.length) return;
      if (e.key === "ArrowDown" || e.key === "ArrowUp") {
        e.preventDefault();
        items.forEach(it => it.classList.remove("mp-focused"));
        _mpFocusIdx = e.key === "ArrowDown"
          ? Math.min(_mpFocusIdx + 1, items.length - 1)
          : Math.max(_mpFocusIdx - 1, 0);
        items[_mpFocusIdx].classList.add("mp-focused");
        items[_mpFocusIdx].scrollIntoView({ block: "nearest" });
      }
      if (e.key === "Enter" && _mpFocusIdx >= 0) {
        items[_mpFocusIdx]?.click();
      }
    });
  }

  refreshStatus();
  setInterval(refreshStatus, 5000);
}

/* ── WebSocket ──────────────────────────────────────────────────────────── */

function connectWS() {
  if (!sessionId) {
    console.error("[aide] connectWS: sessionId is null — cannot connect");
    return;
  }
  const proto = location.protocol === "https:" ? "wss" : "ws";
  const url = `${proto}://${location.host}/ws/${sessionId}`;
  console.log("[aide] Connecting WebSocket:", url);
  ws = new WebSocket(url);

  ws.onopen = () => {
    reconnectDelay = 1000;
    console.log("[aide] WebSocket connected");
    setConnected(true);
  };

  ws.onclose = e => {
    console.warn("[aide] WebSocket closed (code:", e.code, ")");
    setConnected(false);
    setTimeout(connectWS, reconnectDelay);
    reconnectDelay = Math.min(reconnectDelay * 2, 15000);
  };

  ws.onerror = e => {
    console.error("[aide] WebSocket error:", e);
    ws.close();
  };

  ws.onmessage = e => {
    const msg = JSON.parse(e.data);
    if (msg.type === "models") {
      _modelCapabilities = msg.capabilities || {};
      _cachedModels = { models: msg.models, default: msg.default, capabilities: _modelCapabilities };
      populateModelSelector(msg.models, msg.default);
    } else if (msg.type === "restore") {
      if (_skipNextRestore) { _skipNextRestore = false; }
      else { restoreChat(msg); }
    } else {
      handleEvent(msg);
    }
  };
}

function setConnected(ok) {
  const dot = document.getElementById("status-dot");
  const label = document.getElementById("status-label");
  if (dot) dot.className = ok ? "status-dot" : "status-dot offline";
  if (label) label.textContent = ok ? "Connected" : "Disconnected";
}

/* ── Model selector helpers ──────────────────────────────────────────────── */

/**
 * Parse a "provider:model" string into { provider, model, label }.
 * Bare model strings (no prefix) return provider="".
 */
function _parseModel(m) {
  const known = ["anthropic", "openai", "openrouter"];
  const idx = m.indexOf(":");
  if (idx > 0 && known.includes(m.slice(0, idx))) {
    return { provider: m.slice(0, idx), model: m.slice(idx + 1), label: m.slice(idx + 1) };
  }
  return { provider: "", model: m, label: m };
}

// Canonical provider display names and sort order
const _PROVIDER_ORDER  = ["anthropic", "openai", "openrouter"];
const _PROVIDER_LABELS = { anthropic: "Anthropic", openai: "OpenAI", openrouter: "OpenRouter" };

function _providerLabel(p) {
  return _PROVIDER_LABELS[p] || (p ? p.charAt(0).toUpperCase() + p.slice(1) : "Other");
}

function _sortedProviders(groups) {
  return [
    ..._PROVIDER_ORDER.filter(p => groups[p]),
    ...Object.keys(groups).filter(p => !_PROVIDER_ORDER.includes(p)).sort(),
  ];
}

/**
 * Fill a <select> element with model options grouped by provider.
 * currentValue can be "provider:model" or a bare model id (legacy).
 * Returns the resolved value that was set.
 */
function _fillModelSelect(sel, models, currentValue) {
  if (!sel) return currentValue;
  sel.innerHTML = "";

  // Group by provider
  const groups = {};
  for (const m of models) {
    const { provider } = _parseModel(m);
    const key = provider || "other";
    (groups[key] = groups[key] || []).push(m);
  }

  const multiProvider = Object.keys(groups).length > 1;
  for (const prov of _sortedProviders(groups)) {
    const items = groups[prov];
    let container = sel;
    if (multiProvider) {
      const grp = document.createElement("optgroup");
      grp.label = _providerLabel(prov);
      sel.appendChild(grp);
      container = grp;
    }
    for (const m of items) {
      const opt = document.createElement("option");
      opt.value = m;
      opt.textContent = _parseModel(m).label;
      container.appendChild(opt);
    }
  }

  // Try exact match first, then try prepending each provider prefix (legacy bare IDs)
  sel.value = currentValue || "";
  if (!sel.value && currentValue) {
    const match = models.find(m => {
      const { model } = _parseModel(m);
      return model === currentValue;
    });
    if (match) sel.value = match;
  }
  return sel.value;
}

let _selectedChatModel = "";

function populateModelSelector(models, defaultModel) {
  const saved = localStorage.getItem("preferred-model");
  _selectedChatModel = (saved && models.includes(saved)) ? saved : defaultModel;
  _updateModelBtn();
  _updateAttachBtn();
  _updateCapabilityBadges();
}

function _updateModelBtn() {
  const btn = document.getElementById("model-btn");
  if (!btn) return;
  const { provider, label } = _parseModel(_selectedChatModel);
  const text = label || _selectedChatModel || "Select model";
  btn.textContent = provider ? `${_providerLabel(provider)} · ${text}` : text;
  _updateAttachBtn();
  _updateCapabilityBadges();
}

function _currentModelCaps() {
  return _modelCapabilities[_selectedChatModel] || {};
}

function _updateAttachBtn() {
  const btn = document.getElementById("attach-btn");
  if (!btn) return;
  btn.style.display = _currentModelCaps().vision ? "" : "none";
}

function _updateCapabilityBadges() {
  const el = document.getElementById("model-caps");
  if (!el) return;
  const caps = _currentModelCaps();
  const badges = [
    caps.image_gen ? `<span title="Image generation" style="font-size:12px;opacity:0.7">🎨</span>` : "",
    caps.vision    ? `<span title="Vision" style="font-size:12px;opacity:0.7">👁</span>` : "",
    caps.tools     ? `<span title="Tool use" style="font-size:12px;opacity:0.7">🔧</span>` : "",
    caps.online    ? `<span title="Web access" style="font-size:12px;opacity:0.7">🌐</span>` : "",
  ].filter(Boolean).join("");
  el.innerHTML = badges;
}

function getSelectedModel() {
  return _selectedChatModel || null;
}

/* ── Model picker modal ──────────────────────────────────────────────────── */

let _mpFocusIdx = -1;

function openModelPicker() {
  const modal = document.getElementById("model-picker-modal");
  if (!modal || !_cachedModels) return;
  _mpFocusIdx = -1;
  modal.classList.remove("hidden");
  const s = document.getElementById("model-picker-search");
  if (s) {
    s.value = "";
    if (window.innerWidth > 768) s.focus();
  }
  _renderModelPicker("");
}

function closeModelPicker() {
  document.getElementById("model-picker-modal")?.classList.add("hidden");
}

function _renderModelPicker(q) {
  const listEl = document.getElementById("model-picker-list");
  if (!listEl || !_cachedModels) return;
  const ql = q.toLowerCase();
  _mpFocusIdx = -1;
  listEl.innerHTML = "";

  const groups = {};
  for (const m of _cachedModels.models) {
    const { provider, label } = _parseModel(m);
    if (ql && !m.toLowerCase().includes(ql) && !label.toLowerCase().includes(ql)) continue;
    const key = provider || "other";
    (groups[key] = groups[key] || []).push({ m, label });
  }

  if (!Object.keys(groups).length) {
    listEl.innerHTML = '<div style="padding:16px;text-align:center;color:var(--text-dim);font-size:13px">No models found</div>';
    return;
  }

  const orderedProvs = _sortedProviders(groups);
  for (let i = 0; i < orderedProvs.length; i++) {
    const prov  = orderedProvs[i];
    const items = groups[prov];

    // Horizontal divider between sections
    if (i > 0) {
      const hr = document.createElement("div");
      hr.style.cssText = "border-top:1px solid var(--border);margin:4px 0";
      listEl.appendChild(hr);
    }

    // Section header
    const gh = document.createElement("div");
    gh.style.cssText = "padding:8px 14px 4px;font-size:10px;text-transform:uppercase;letter-spacing:0.8px;color:var(--text-dim);font-weight:600";
    gh.textContent = _providerLabel(prov);
    listEl.appendChild(gh);

    for (const { m, label } of items) {
      const item = document.createElement("div");
      item.className = "mp-item" + (m === _selectedChatModel ? " mp-selected" : "");
      item.textContent = label;
      item.dataset.v = m;
      item.addEventListener("click", () => {
        _selectedChatModel = m;
        localStorage.setItem("preferred-model", m);
        _updateModelBtn();
        closeModelPicker();
      });
      listEl.appendChild(item);
    }
  }
}

/* ── Markdown renderer ───────────────────────────────────────────────────── */

function renderMarkdown(text) {
  const parts = [];
  const re = /```(\w*)\n?([\s\S]*?)```/g;
  let last = 0, m;
  while ((m = re.exec(text)) !== null) {
    if (m.index > last) parts.push({ type: "text", s: text.slice(last, m.index) });
    parts.push({ type: "code", lang: m[1] || "", s: m[2].replace(/\n$/, "") });
    last = m.index + m[0].length;
  }
  if (last < text.length) parts.push({ type: "text", s: text.slice(last) });

  return parts.map(p => {
    if (p.type === "code") {
      const langLabel = p.lang ? `<span class="code-lang">${esc(p.lang)}</span>` : `<span class="code-lang"></span>`;
      return `<div class="code-block">` +
        `<div class="code-block-header">${langLabel}` +
        `<button class="copy-code-btn btn btn-ghost btn-small" onclick="copyCode(this)">Copy</button>` +
        `</div><pre><code>${esc(p.s)}</code></pre></div>`;
    }
    return _renderBlocks(p.s);
  }).join("");
}

function _renderBlocks(text) {
  const lines = text.split('\n');
  const out = [];
  let textBuf = [];
  let i = 0;

  const flushText = () => {
    while (textBuf.length && !textBuf[0].trim()) textBuf.shift();
    while (textBuf.length && !textBuf[textBuf.length - 1].trim()) textBuf.pop();
    if (textBuf.length) out.push(textBuf.map(_renderInline).join('<br>'));
    textBuf = [];
  };

  while (i < lines.length) {
    const line = lines[i];

    // Headings: # through ######
    const hm = line.match(/^(#{1,6}) (.+)/);
    if (hm) {
      flushText();
      out.push(`<h${hm[1].length} class="md-h">${_renderInline(hm[2])}</h${hm[1].length}>`);
      i++; continue;
    }

    // Table: current line has | and next line is a GFM separator (|---|---|)
    if (line.includes('|') && i + 1 < lines.length && /^\|[\s\-:|]+\|/.test(lines[i + 1])) {
      flushText();
      const rows = [];
      while (i < lines.length && lines[i].includes('|')) { rows.push(lines[i]); i++; }
      out.push(_renderTable(rows));
      continue;
    }

    // Unordered list
    if (/^[-*+] /.test(line)) {
      flushText();
      const items = [];
      while (i < lines.length && /^[-*+] /.test(lines[i])) {
        items.push(`<li>${_renderInline(lines[i].replace(/^[-*+] /, ''))}</li>`);
        i++;
      }
      out.push(`<ul class="md-list">${items.join('')}</ul>`);
      continue;
    }

    // Ordered list
    if (/^\d+\. /.test(line)) {
      flushText();
      const items = [];
      while (i < lines.length && /^\d+\. /.test(lines[i])) {
        items.push(`<li>${_renderInline(lines[i].replace(/^\d+\. /, ''))}</li>`);
        i++;
      }
      out.push(`<ol class="md-list">${items.join('')}</ol>`);
      continue;
    }

    textBuf.push(line);
    i++;
  }
  flushText();
  return out.join('');
}

function _renderTable(lines) {
  const isSep = l => /^\|[\s\-:|]+\|/.test(l);
  const parseRow = l => {
    const cells = l.split('|');
    if (cells[0].trim() === '') cells.shift();
    if (cells.length && cells[cells.length - 1].trim() === '') cells.pop();
    return cells.map(c => c.trim());
  };
  const header = parseRow(lines[0]);
  const body = lines.slice(1).filter(l => !isSep(l) && l.trim());
  const thead = `<thead><tr>${header.map(h => `<th>${_renderInline(h)}</th>`).join('')}</tr></thead>`;
  const tbody = body.map(l => `<tr>${parseRow(l).map(c => `<td>${_renderInline(c)}</td>`).join('')}</tr>`).join('');
  return `<div class="md-table-wrap"><table class="md-table"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`;
}

function _renderInline(text) {
  let s = esc(text);
  s = s.replace(/&lt;br\s*\/?&gt;/gi, '<br>');
  s = s.replace(/\*\*([^*\n]+)\*\*/g, '<strong>$1</strong>');
  s = s.replace(/\*([^*\n]+)\*/g, '<em>$1</em>');
  s = s.replace(/`([^`\n]+)`/g, '<code class="inline-code">$1</code>');
  return s;
}

function copyCode(btn) {
  const code = btn.closest(".code-block").querySelector("code").textContent;
  navigator.clipboard.writeText(code).then(() => {
    const orig = btn.textContent;
    btn.textContent = "Copied!";
    setTimeout(() => { btn.textContent = orig; }, 1500);
  }).catch(() => {});
}

function copyResponse(btn) {
  const bubble = btn.closest(".message.assistant").querySelector(".message-bubble");
  const text = bubble.dataset.raw || bubble.textContent;
  navigator.clipboard.writeText(text).then(() => {
    const orig = btn.textContent;
    btn.textContent = "Copied!";
    setTimeout(() => { btn.textContent = orig; }, 1500);
  }).catch(() => {});
}

/* ── Event handling ─────────────────────────────────────────────────────── */

// Track in-progress assistant message and tool indicators by call_id
let currentAssistantDiv = null;
let currentBubble = null;
const toolDivs = {};

function handleEvent(msg) {
  switch (msg.type) {
    case "text":
      appendText(msg.content);
      break;

    case "tool_start":
      showToolIndicator(msg.call_id, msg.tool_name, msg.arguments);
      break;

    case "tool_done":
      resolveToolIndicator(msg.call_id, msg.success, msg.result, msg.confirmed);
      break;

    case "image":
      appendGeneratedImages(msg.data_urls);
      break;

    case "confirmation_required":
      showConfirmModal(msg.call_id, msg.tool_name, msg.arguments, msg.description);
      break;

    case "done":
      finishResponse(msg);
      break;

    case "error":
      showError(msg.message);
      finishGenerating();
      break;
  }
}

function restoreChat(msg) {
  const container = document.getElementById("chat-messages");
  if (!container) return;
  // Persist this session so further refreshes keep the same conversation
  if (msg.session_id) {
    sessionId = msg.session_id;
    localStorage.setItem("current_session_id", msg.session_id);
  }
  // Pre-select the model that was used in this conversation
  if (msg.model) {
    localStorage.setItem("preferred-model", msg.model);
    const btn = document.getElementById("model-btn");
    if (btn) btn.textContent = msg.model;
  }
  // Clear the default greeting
  container.innerHTML = "";
  // Add a subtle "restored" marker
  const marker = document.createElement("div");
  marker.style.cssText = "text-align:center;color:var(--text-dim);font-size:11px;padding:8px 0 4px;";
  marker.textContent = msg.title ? `— ${msg.title} —` : "— Restored conversation —";
  container.appendChild(marker);
  // Render each turn
  for (const turn of (msg.messages || [])) {
    if (turn.role === "user") {
      const div = document.createElement("div");
      div.className = "message user";
      div.innerHTML = `<div class="message-bubble" style="white-space:normal">${renderMarkdown(turn.text)}</div>`;
      container.appendChild(div);
    } else if (turn.role === "assistant") {
      const div = document.createElement("div");
      div.className = "message assistant";
      const bubble = document.createElement("div");
      bubble.className = "message-bubble";
      bubble.dataset.raw = turn.text;
      bubble.innerHTML = renderMarkdown(turn.text);
      bubble.style.whiteSpace = "normal";
      div.appendChild(bubble);
      const copyBtn = document.createElement("button");
      copyBtn.className = "copy-response-btn btn btn-ghost btn-small";
      copyBtn.textContent = "Copy response";
      copyBtn.onclick = function() { copyResponse(this); };
      div.appendChild(copyBtn);
      container.appendChild(div);
    }
  }
  container.scrollTop = container.scrollHeight;
}

function appendText(content) {
  if (!currentAssistantDiv) {
    currentAssistantDiv = createAssistantMessage();
    currentBubble = currentAssistantDiv.querySelector(".message-bubble");
  }
  // Append raw text; simple pre-wrap handles newlines
  currentBubble.textContent += content;
  scrollToBottom();
}

function showToolIndicator(callId, toolName, args) {
  // Commit any in-progress text message first
  if (currentAssistantDiv) {
    currentAssistantDiv = null;
    currentBubble = null;
  }

  const el = document.createElement("div");
  el.className = "tool-indicator";
  el.id = `tool-${callId}`;

  const argStr = Object.entries(args || {})
    .slice(0, 3)
    .map(([k, v]) => `${k}: ${String(v).slice(0, 60)}`)
    .join(", ");

  el.innerHTML = `
    <span class="dot"></span>
    <span class="tool-label"><strong>${esc(toolName)}</strong>${argStr ? " — " + esc(argStr) : ""}</span>
  `;
  document.getElementById("chat-messages").appendChild(el);
  toolDivs[callId] = el;
  scrollToBottom();
}

function resolveToolIndicator(callId, success, result, confirmed) {
  const el = toolDivs[callId];
  if (!el) return;
  el.classList.add(success ? "done" : "error");
  const label = el.querySelector(".tool-label");
  const suffix = confirmed ? " ✓" : (success ? "" : " ✗");
  if (result) {
    label.innerHTML += ` <span class="result">→ ${esc(String(result).slice(0, 120))}${suffix}</span>`;
  }
}

function finishResponse(msg) {
  // Render markdown on the completed bubble and add a copy-response button
  if (currentBubble && currentAssistantDiv) {
    const raw = currentBubble.textContent;
    currentBubble.dataset.raw = raw;
    currentBubble.innerHTML = renderMarkdown(raw);
    currentBubble.style.whiteSpace = "normal";
    const copyBtn = document.createElement("button");
    copyBtn.className = "copy-response-btn btn btn-ghost btn-small";
    copyBtn.textContent = "Copy response";
    copyBtn.onclick = function() { copyResponse(this); };
    currentAssistantDiv.appendChild(copyBtn);
  }
  currentAssistantDiv = null;
  currentBubble = null;
  finishGenerating();
  // Append usage badge
  if (msg.usage) {
    const meta = document.createElement("div");
    meta.className = "message-meta";
    meta.textContent = `↑${msg.usage.input} ↓${msg.usage.output} tokens  •  ${msg.tool_calls_made} tool call${msg.tool_calls_made !== 1 ? "s" : ""}`;
    document.getElementById("chat-messages").appendChild(meta);
  }
}

function showError(message) {
  const el = document.createElement("div");
  el.className = "tool-indicator error";
  el.innerHTML = `<span class="dot"></span><span>Error: ${esc(message)}</span>`;
  document.getElementById("chat-messages").appendChild(el);
  scrollToBottom();
}

/* ── Message DOM helpers ─────────────────────────────────────────────────── */

function createUserMessage(text, imageDataUrls) {
  const wrap = document.createElement("div");
  wrap.className = "message user";
  let html = "";
  if (imageDataUrls && imageDataUrls.length) {
    const imgs = imageDataUrls.map(u =>
      `<img src="${esc(u)}" style="max-height:120px;max-width:200px;border-radius:4px;border:1px solid var(--border);display:block">`
    ).join("");
    html += `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:${text ? "6px" : "0"}">${imgs}</div>`;
  }
  if (text) html += renderMarkdown(text);
  wrap.innerHTML = `<div class="message-bubble" style="white-space:normal">${html}</div>`;
  document.getElementById("chat-messages").appendChild(wrap);
  return wrap;
}

function appendGeneratedImages(dataUrls) {
  if (!dataUrls || !dataUrls.length) return;
  // Ensure an assistant bubble exists
  if (!currentAssistantDiv) {
    currentAssistantDiv = createAssistantMessage();
    currentBubble = currentAssistantDiv.querySelector(".message-bubble");
  }
  const wrap = document.createElement("div");
  wrap.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;margin-top:8px";
  for (const url of dataUrls) {
    const imgWrap = document.createElement("div");
    imgWrap.style.cssText = "position:relative;display:inline-block";

    const img = document.createElement("img");
    img.src = url;
    img.style.cssText = "max-width:100%;border-radius:6px;display:block;cursor:zoom-in";
    img.title = "Click to open full size";
    img.onclick = () => window.open(url, "_blank");
    imgWrap.appendChild(img);

    // Download button overlay
    const dlBtn = document.createElement("a");
    dlBtn.href = url;
    dlBtn.download = `generated-${Date.now()}.png`;
    dlBtn.style.cssText = "position:absolute;bottom:6px;right:6px;background:rgba(0,0,0,0.6);color:#fff;border-radius:4px;padding:3px 8px;font-size:11px;text-decoration:none";
    dlBtn.textContent = "↓ Save";
    imgWrap.appendChild(dlBtn);

    wrap.appendChild(imgWrap);
  }
  currentBubble.appendChild(wrap);
  scrollToBottom();
}

function createAssistantMessage() {
  const wrap = document.createElement("div");
  wrap.className = "message assistant";
  wrap.innerHTML = `<div class="message-bubble"></div>`;
  document.getElementById("chat-messages").appendChild(wrap);
  scrollToBottom();
  return wrap;
}

function scrollToBottom() {
  const el = document.getElementById("chat-messages");
  el.scrollTop = el.scrollHeight;
}

/* ── File attachments ────────────────────────────────────────────────────── */

let _pendingAttachments = [];  // [{ name, media_type, data, dataUrl }]

const _ALLOWED_TYPES = new Set([
  "image/jpeg", "image/png", "image/gif", "image/webp", "image/avif",
  "application/pdf",
]);

async function _addFile(file) {
  if (!file || !_ALLOWED_TYPES.has(file.type)) return;
  const dataUrl = await new Promise((res, rej) => {
    const r = new FileReader();
    r.onload = e => res(e.target.result);
    r.onerror = rej;
    r.readAsDataURL(file);
  });
  const base64 = dataUrl.split(",")[1];
  _pendingAttachments.push({ name: file.name || "attachment", media_type: file.type, data: base64, dataUrl });
  _renderAttachPreview();
}

function _initAttachHandler() {
  const input = document.getElementById("img-file-input");
  if (!input) return;
  input.addEventListener("change", async () => {
    for (const file of input.files) await _addFile(file);
    input.value = "";
  });

  // Paste images from clipboard into the chat input
  const chatInput = document.getElementById("chat-input");
  chatInput?.addEventListener("paste", async e => {
    const items = e.clipboardData?.items || [];
    for (const item of items) {
      if (item.type.startsWith("image/")) {
        e.preventDefault();
        const file = item.getAsFile();
        if (file) await _addFile(file);
      }
    }
  });
}

function _renderAttachPreview() {
  const strip = document.getElementById("attach-preview");
  if (!strip) return;
  if (!_pendingAttachments.length) {
    strip.style.display = "none";
    strip.innerHTML = "";
    return;
  }
  strip.style.display = "flex";
  strip.innerHTML = "";
  _pendingAttachments.forEach((att, idx) => {
    const wrap = document.createElement("div");
    wrap.style.cssText = "position:relative;display:inline-flex;align-items:center";
    const rm = document.createElement("button");
    rm.textContent = "\u2715";
    rm.style.cssText = "position:absolute;top:-4px;right:-4px;background:var(--bg2);border:1px solid var(--border);border-radius:50%;width:16px;height:16px;font-size:9px;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;color:var(--text-dim);z-index:1";
    rm.onclick = function() { _pendingAttachments.splice(idx, 1); _renderAttachPreview(); };

    if (att.media_type.startsWith("image/")) {
      const img = document.createElement("img");
      img.src = att.dataUrl;
      img.style.cssText = "height:56px;width:56px;object-fit:cover;border-radius:4px;border:1px solid var(--border)";
      img.title = att.name;
      wrap.appendChild(img);
    } else {
      // Non-image (PDF etc.) — show a file chip
      const chip = document.createElement("div");
      chip.title = att.name;
      chip.style.cssText = "display:flex;align-items:center;gap:6px;height:56px;padding:0 12px;border-radius:4px;border:1px solid var(--border);background:var(--bg2);font-size:12px;color:var(--text-dim);max-width:160px";
      const icon = document.createElement("span");
      icon.style.cssText = "flex-shrink:0;font-size:18px";
      icon.textContent = "\ud83d\udcc4";  // 📄
      const label = document.createElement("span");
      label.style.cssText = "overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
      label.textContent = att.name;
      chip.appendChild(icon);
      chip.appendChild(label);
      wrap.appendChild(chip);
    }

    wrap.appendChild(rm);
    strip.appendChild(wrap);
  });
}

/* ── Send ────────────────────────────────────────────────────────────────── */

function sendMessage() {
  if (isGenerating) return;
  const input = document.getElementById("chat-input");
  const text = input.value.trim();
  if (!text && !_pendingAttachments.length) return;
  if (!ws || ws.readyState !== WebSocket.OPEN) {
    console.error("[aide] sendMessage: WebSocket not open. State:", ws ? ws.readyState : "null (ws is null)");
    return;
  }

  // Persist session so page refreshes continue the same conversation
  if (sessionId) localStorage.setItem("current_session_id", sessionId);

  const attachments = _pendingAttachments.length ? _pendingAttachments.map(a => ({ media_type: a.media_type, data: a.data })) : null;
  createUserMessage(text, _pendingAttachments.map(a => a.dataUrl));
  _pendingAttachments = [];
  _renderAttachPreview();

  input.value = "";
  input.style.height = "auto";

  const model = getSelectedModel();
  const payload = { type: "message", content: text, model };
  if (attachments) payload.attachments = attachments;
  ws.send(JSON.stringify(payload));
  startGenerating();
  scrollToBottom();
}

function clearChat() {
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "clear" }));
  }
  // Generate a fresh session for the next conversation
  localStorage.removeItem("current_session_id");
  sessionId = window.SESSION_ID || crypto.randomUUID();
  document.getElementById("chat-messages").innerHTML = '<div class="message assistant"><div class="message-bubble">Hello. What can I help you with?</div></div>';
  Object.keys(toolDivs).forEach(k => delete toolDivs[k]);
  currentAssistantDiv = null;
  currentBubble = null;
  _pendingAttachments = [];
  _renderAttachPreview();
}

function startGenerating() {
  isGenerating = true;
  document.getElementById("send-btn").disabled = true;
  // Show typing indicator
  const el = document.createElement("div");
  el.id = "typing-indicator";
  el.className = "message assistant";
  el.innerHTML = `<div class="message-bubble"><div class="typing"><span></span><span></span><span></span></div></div>`;
  document.getElementById("chat-messages").appendChild(el);
  scrollToBottom();
}

function finishGenerating() {
  isGenerating = false;
  document.getElementById("send-btn").disabled = false;
  document.getElementById("typing-indicator")?.remove();
}

/* ── Confirmation modal ─────────────────────────────────────────────────── */

let pendingCallId = null;

function showConfirmModal(callId, toolName, args, description) {
  pendingCallId = callId;
  document.getElementById("confirm-tool-name").textContent = toolName;
  document.getElementById("confirm-description").textContent = description || JSON.stringify(args, null, 2);
  document.getElementById("confirm-modal").classList.remove("hidden");
}

function respondConfirm(approved) {
  if (!pendingCallId) return;
  if (ws && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({ type: "confirm", approved, call_id: pendingCallId }));
  }
  pendingCallId = null;
  document.getElementById("confirm-modal").classList.add("hidden");
}

/* ══════════════════════════════════════════════════════════════════════════
   USAGE PAGE
══════════════════════════════════════════════════════════════════════════ */

let _usageRange = "7d";

function setUsageRange(range) {
  _usageRange = range;
  ["today","7d","30d","all"].forEach(r => {
    const btn = document.getElementById("usage-range-" + r);
    if (!btn) return;
    const active = r === range;
    btn.style.background = active ? "var(--accent)" : "";
    btn.style.color = active ? "#fff" : "";
    btn.style.borderColor = active ? "var(--accent)" : "";
  });
  loadUsage();
}

function _fmtTokens(n) {
  if (n === null || n === undefined) return "—";
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(2) + "M";
  if (n >= 1_000) return (n / 1_000).toFixed(1) + "k";
  return String(n);
}

function _fmtCost(c) {
  if (c === null || c === undefined) return "—";
  if (c < 0.001) return "< $0.001";
  return "$" + c.toFixed(4);
}

async function loadUsage() {
  const tbody = document.getElementById("usage-tbody");
  if (!tbody) return;
  tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-dim);padding:32px">Loading…</td></tr>';

  const resp = await fetch("/api/usage?since=" + encodeURIComponent(_usageRange));
  if (!resp.ok) {
    tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--danger)">Failed to load usage data</td></tr>';
    return;
  }
  const data = await resp.json();
  const s = data.summary;

  // Update summary cards
  const set = (id, v) => { const el = document.getElementById(id); if (el) el.textContent = v; };
  set("u-total-runs",    s.runs.toLocaleString());
  set("u-input-tokens",  _fmtTokens(s.input_tokens));
  set("u-output-tokens", _fmtTokens(s.output_tokens));
  set("u-total-tokens",  _fmtTokens(s.input_tokens + s.output_tokens));
  set("u-cost",          _fmtCost(s.cost_usd));

  // Find max tokens for bar scaling
  const agents = data.by_agent;
  const maxTokens = agents.reduce((m, a) => Math.max(m, a.input_tokens + a.output_tokens), 1);
  let anyNullCost = false;

  if (!agents.length) {
    tbody.innerHTML = '<tr><td colspan="8" style="text-align:center;color:var(--text-dim);padding:32px">No runs found for this period</td></tr>';
    return;
  }

  tbody.innerHTML = agents.map(a => {
    const total = a.input_tokens + a.output_tokens;
    const pct = maxTokens > 0 ? Math.round((total / maxTokens) * 100) : 0;
    const costStr = _fmtCost(a.cost_usd);
    if (a.cost_usd === null) anyNullCost = true;
    const modelShort = a.model ? a.model.replace(/^(anthropic|openrouter|openai):/, "") : "—";
    const modelTitle = a.model || "";
    return `<tr>
      <td><a href="/agents/${a.agent_id}" style="color:var(--accent);text-decoration:none">${esc(a.agent_name)}</a></td>
      <td style="font-size:12px;color:var(--text-dim)" title="${esc(modelTitle)}">${esc(modelShort)}</td>
      <td style="text-align:right">${a.runs.toLocaleString()}</td>
      <td style="text-align:right;font-size:13px">${_fmtTokens(a.input_tokens)}</td>
      <td style="text-align:right;font-size:13px">${_fmtTokens(a.output_tokens)}</td>
      <td style="text-align:right;font-size:13px">${_fmtTokens(total)}</td>
      <td style="text-align:right;font-size:13px;font-weight:${a.cost_usd !== null ? "600" : "400"}">${costStr}</td>
      <td>
        <div style="background:var(--bg2);border-radius:3px;height:6px;overflow:hidden">
          <div style="background:var(--accent);height:100%;width:${pct}%;transition:width 0.3s"></div>
        </div>
      </td>
    </tr>`;
  }).join("");

  // Chat section
  const chatTbody = document.getElementById("usage-chat-tbody");
  if (chatTbody) {
    const c = data.chat;
    if (!c || c.sessions === 0) {
      chatTbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-dim);padding:24px">No chat sessions in this period</td></tr>';
    } else {
      const chatTotal = c.input_tokens + c.output_tokens;
      if (c.cost_usd === null) anyNullCost = true;
      chatTbody.innerHTML = `<tr>
        <td>Interactive chat</td>
        <td style="text-align:right">${c.sessions.toLocaleString()}</td>
        <td style="text-align:right;font-size:13px">${_fmtTokens(c.input_tokens)}</td>
        <td style="text-align:right;font-size:13px">${_fmtTokens(c.output_tokens)}</td>
        <td style="text-align:right;font-size:13px">${_fmtTokens(chatTotal)}</td>
        <td style="text-align:right;font-size:13px;font-weight:${c.cost_usd !== null ? "600" : "400"}">${_fmtCost(c.cost_usd)}</td>
      </tr>`;
    }
  }

  const note = document.getElementById("usage-no-cost-note");
  if (note) note.style.display = anyNullCost ? "" : "none";
}

function initUsage() {
  if (!document.getElementById("usage-container")) return;
  loadUsage();
}

async function clearUsageCost() {
  if (!confirm("Clear all usage data?\n\nThis deletes all agent run history and resets chat session cost estimates. The agents themselves are not affected. This cannot be undone.")) return;
  const r = await fetch("/api/usage/cost", { method: "DELETE", credentials: "same-origin" });
  if (!r.ok) {
    alert("Failed to clear cost data.");
    return;
  }
  loadUsage();
}


/* ══════════════════════════════════════════════════════════════════════════
   AUDIT PAGE
══════════════════════════════════════════════════════════════════════════ */

let _auditTaskId = "";  // set when navigating from run detail

function initAudit() {
  if (!document.getElementById("audit-table")) return;
  // Pre-filter by task_id if passed in URL (e.g. from agent run detail link)
  const urlParams = new URLSearchParams(window.location.search);
  _auditTaskId = urlParams.get("task_id") || "";
  if (_auditTaskId) {
    // Clear all other filters so they don't interfere with the task_id filter
    ["filter-start","filter-end","filter-tool","filter-session"].forEach(id => {
      const el = document.getElementById(id);
      if (el) el.value = "";
    });
    const banner = document.getElementById("audit-task-filter-banner");
    if (banner) {
      banner.textContent = `Showing entries for run ${_auditTaskId.slice(0, 8)}…`;
      banner.style.display = "";
    }
  }
  loadAudit();

  document.getElementById("filter-btn")?.addEventListener("click", loadAudit);
  document.getElementById("filter-reset")?.addEventListener("click", () => {
    ["filter-start","filter-end","filter-tool","filter-session"].forEach(id => {
      const el = document.getElementById(id);
      if (el) el.value = "";
    });
    _auditTaskId = "";
    const banner = document.getElementById("audit-task-filter-banner");
    if (banner) banner.style.display = "none";
    loadAudit();
  });
}

let auditPage = 1;

async function loadAudit(page) {
  if (page) auditPage = page;
  const params = new URLSearchParams();
  const start = document.getElementById("filter-start")?.value;
  const end = document.getElementById("filter-end")?.value;
  const tool = document.getElementById("filter-tool")?.value;
  const session = document.getElementById("filter-session")?.value;
  const startISO = parseEuDate(start);
  const endISO   = parseEuDate(end, true);
  if (startISO) params.set("start", startISO);
  if (endISO)   params.set("end",   endISO);
  if (tool) params.set("tool_name", tool);
  if (session) params.set("session_id", session);
  if (_auditTaskId) params.set("task_id", _auditTaskId);
  params.set("page", auditPage);
  params.set("per_page", 50);

  const r = await fetch("/api/audit?" + params);
  const data = await r.json();
  renderAuditTable(data);
}

function renderAuditTable(data) {
  const tbody = document.querySelector("#audit-table tbody");
  tbody.innerHTML = "";
  for (const e of data.entries) {
    const tr = document.createElement("tr");
    tr.style.cursor = "pointer";
    tr.title = "Click for details";
    tr.innerHTML = `
      <td>${esc(formatDate(e.timestamp))}</td>
      <td><code>${esc(e.tool_name)}</code></td>
      <td class="args-cell">${esc(JSON.stringify(e.arguments).slice(0, 80))}</td>
      <td>${esc((e.result_summary || "").slice(0, 80))}</td>
      <td><code>${esc((e.session_id || "").slice(0,8))}</code></td>
    `;
    tr.addEventListener("click", () => openAuditDetail(e));
    tbody.appendChild(tr);
  }

  // Pagination
  const pag = document.getElementById("audit-pagination");
  if (!pag) return;
  pag.innerHTML = "";
  for (let p = 1; p <= data.pages; p++) {
    const btn = document.createElement("button");
    btn.className = "page-btn" + (p === data.page ? " active" : "");
    btn.textContent = p;
    btn.onclick = () => loadAudit(p);
    pag.appendChild(btn);
  }
}

function openAuditDetail(e) {
  const modal = document.getElementById("audit-detail-modal");
  if (!modal) return;
  document.getElementById("audit-detail-tool").textContent = e.tool_name;
  document.getElementById("audit-detail-ts").textContent = formatDate(e.timestamp);
  document.getElementById("audit-detail-session").textContent = e.session_id || "—";
  document.getElementById("audit-detail-task").textContent = e.task_id || "—";
  let _args = e.arguments;
  if (typeof _args === "string") { try { _args = JSON.parse(_args); } catch {} }
  document.getElementById("audit-detail-args").textContent =
    _args != null ? JSON.stringify(_args, null, 2) : "—";
  document.getElementById("audit-detail-result").textContent = e.result_summary || "—";
  modal.style.display = "flex";
}

function closeAuditDetail() {
  const modal = document.getElementById("audit-detail-modal");
  if (modal) modal.style.display = "none";
}

/* ══════════════════════════════════════════════════════════════════════════
   SETTINGS PAGE
══════════════════════════════════════════════════════════════════════════ */

/* ── Mobile settings tab select ──────────────────────────────────────────── */
function _initMobileTabSelect(switchFn) {
  if (window.innerWidth > 768) return;
  const bar = document.querySelector(".tab-bar");
  if (!bar || bar.dataset.selectBuilt) return;
  bar.dataset.selectBuilt = "1";

  const buttons = [...bar.querySelectorAll(".tab-btn")];
  if (!buttons.length) return;

  const wrap = document.createElement("div");
  wrap.className = "tab-select-wrap";

  const sel = document.createElement("select");
  sel.className = "tab-select";
  buttons.forEach(btn => {
    const opt = document.createElement("option");
    opt.value = btn.id.replace(/^[us]tab-|^stab-/, "");
    opt.textContent = btn.textContent.trim();
    if (btn.classList.contains("active")) opt.selected = true;
    sel.appendChild(opt);
  });
  sel.addEventListener("change", () => switchFn(sel.value));

  const arrow = document.createElement("span");
  arrow.className = "tab-select-arrow";
  arrow.textContent = "▾";

  wrap.appendChild(sel);
  wrap.appendChild(arrow);
  bar.parentNode.insertBefore(wrap, bar);
  bar.style.display = "none";
}

function _syncTabSelect(name) {
  const sel = document.querySelector(".tab-select");
  if (sel) sel.value = name;
}

function switchSettingsTab(name) {
  ["general", "whitelists", "credentials", "caldav", "pushover", "inbox", "emailaccounts", "telegram", "system", "brain", "mcp", "security", "branding", "webhooks", "mfa"].forEach(t => {
    const pane = document.getElementById(`spane-${t}`);
    const btn  = document.getElementById(`stab-${t}`);
    if (pane) pane.style.display = t === name ? "" : "none";
    if (btn)  btn.classList.toggle("active", t === name);
  });
  _syncTabSelect(name);
  if (name === "inbox") { loadInboxStatus(); }
  if (name === "emailaccounts") { loadEmailAccounts(); }
  if (name === "brain") { loadBrainKey("brain-mcp-key", "brain-mcp-cmd"); loadBrainAutoApprove(); }
  if (name === "mfa") { loadTheme(); loadMyProfile(); loadMfaStatus(); loadDataFolder(); initSshKey(); }
  if (name === "webhooks") { loadWebhooks(); loadWebhookTargets(); }
  if (name === "caldav") { loadAdminCaldav(); }
  if (name === "pushover") { loadAdminPushover(); }
}

/* ── API Key management ──────────────────────────────────────────────────── */

async function loadApiKeyStatus() {
  const r = await fetch("/api/settings/api-key");
  if (!r.ok) return;
  const d = await r.json();
  const statusEl = document.getElementById("apikey-status");
  const revokeBtn = document.getElementById("apikey-revoke-btn");
  const regenBtn  = document.getElementById("apikey-regen-btn");
  if (!statusEl) return;

  if (d.configured) {
    const when = d.created_at ? " (generated " + formatDate(d.created_at) + ")" : "";
    statusEl.innerHTML = `<span style="color:var(--green)">&#10003; Key configured</span><span style="color:var(--text-dim);font-size:11px">${when}</span>`;
    if (revokeBtn) revokeBtn.style.display = "";
    if (regenBtn)  regenBtn.style.display  = "";
  } else {
    statusEl.innerHTML = `<span style="color:var(--text-dim)">No key configured - API is open (home-network mode)</span>`;
    if (revokeBtn) revokeBtn.style.display = "none";
    if (regenBtn)  regenBtn.style.display  = "none";
  }
}

async function generateApiKey(isRegen) {
  if (isRegen && !confirm("This will invalidate the current key. All existing external clients will lose access. Continue?")) return;
  const r = await fetch("/api/settings/api-key", { method: "POST" });
  if (!r.ok) { showFlash("Error generating key"); return; }
  const d = await r.json();

  // Show the one-time reveal - key is NOT stored anywhere; copy it for external tools
  const box = document.getElementById("apikey-reveal-box");
  const keyEl = document.getElementById("apikey-reveal-value");
  if (box && keyEl) {
    keyEl.textContent = d.key;
    box.style.display = "";
  }

  showFlash(isRegen ? "Key regenerated - copy it for external tools!" : "Key generated - copy it for external tools!");
  loadApiKeyStatus();
}

async function revokeApiKey() {
  if (!confirm("Revoke the API key? All external clients will lose access immediately.")) return;
  const r = await fetch("/api/settings/api-key", { method: "DELETE" });
  if (!r.ok) { showFlash("Error revoking key"); return; }
  const box = document.getElementById("apikey-reveal-box");
  if (box) box.style.display = "none";
  showFlash("Key revoked");
  loadApiKeyStatus();
}

function copyApiKey() {
  const val = document.getElementById("apikey-reveal-value")?.textContent;
  if (!val) return;
  navigator.clipboard.writeText(val).then(() => showFlash("Copied!"));
}

async function loadProviderKeys() {
  const r = await fetch("/api/settings/provider-keys");
  if (!r.ok) return;
  const d = await r.json();
  const notSet = '<span style="color:var(--text-dim)">Not set — seeded from .env on startup if present</span>';
  const isSet  = '<span style="color:var(--green)">&#10003; Key configured</span>';
  const antEl = document.getElementById("provider-key-anthropic-status");
  const orEl  = document.getElementById("provider-key-openrouter-status");
  const oaiEl = document.getElementById("provider-key-openai-status");
  if (antEl) antEl.innerHTML = d.anthropic_set  ? isSet : notSet;
  if (orEl)  orEl.innerHTML  = d.openrouter_set ? isSet : notSet;
  if (oaiEl) oaiEl.innerHTML = d.openai_set     ? isSet : notSet;
}

async function saveProviderKey(provider) {
  const val = document.getElementById(`provider-key-${provider}-input`)?.value.trim();
  if (!val) { showFlash("Enter a key first."); return; }
  const r = await fetch("/api/settings/provider-keys", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ provider, key: val }),
  });
  if (r.ok) {
    document.getElementById(`provider-key-${provider}-input`).value = "";
    showFlash(`${provider} key saved.`);
    loadProviderKeys();
    loadDefaultProvider();
  } else {
    const d = await r.json().catch(() => ({}));
    showFlash(d.detail || "Error saving key.");
  }
}

async function clearProviderKey(provider) {
  if (!confirm(`Clear the ${provider} API key? This may break agent runs that rely on it.`)) return;
  const r = await fetch(`/api/settings/provider-keys/${provider}`, { method: "DELETE" });
  if (r.ok) { showFlash(`${provider} key cleared.`); loadProviderKeys(); loadDefaultProvider(); }
  else showFlash("Error clearing key.");
}

async function loadUsersBaseFolder() {
  const el = document.getElementById("users-base-folder");
  if (!el) return;
  const r = await fetch("/api/settings/users-base-folder");
  if (!r.ok) return;
  const d = await r.json();
  el.value = d.path || "";
}

async function saveUsersBaseFolder() {
  const val = document.getElementById("users-base-folder")?.value.trim();
  if (val === undefined) return;
  const r = await fetch("/api/settings/users-base-folder", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ path: val }),
  });
  if (r.ok) {
    showFlash(val ? "Users base folder saved." : "Users base folder cleared.");
  } else {
    const d = await r.json().catch(() => ({}));
    showFlash(d.detail || "Error saving.", true);
  }
}

async function clearUsersBaseFolder() {
  document.getElementById("users-base-folder").value = "";
  await saveUsersBaseFolder();
}

function switchUserTab(name) {
  document.querySelectorAll("[id^='ustab-']").forEach(b => b.classList.remove("active"));
  document.querySelectorAll("[id^='uspane-']").forEach(p => { p.style.display = "none"; });
  const tab = document.getElementById("ustab-" + name);
  const pane = document.getElementById("uspane-" + name);
  if (tab) tab.classList.add("active");
  if (pane) pane.style.display = "";
  _syncTabSelect(name);
  if (name === "mcp") loadMyMcpServers();
  if (name === "inbox") { loadMyInboxConfig(); loadMyInboxTriggers(); }
  if (name === "emailaccounts") { loadEmailAccounts(); }
  if (name === "caldav") { loadMyCaldavConfig(); }
  if (name === "telegram") { loadMyTelegramConfig(); loadMyTelegramWhitelist(); loadMyTelegramTriggers(); }
  if (name === "brain") { loadBrainKey("ubrain-mcp-key", "ubrain-mcp-cmd"); loadBrainAutoApprove(); }
  if (name === "mfa") { loadTheme(); loadMyProfile(); loadMfaStatus(); loadDataFolder(); initSshKey(); }
  if (name === "webhooks") { loadMyWebhooks(); loadMyWebhookTargets(); }
  if (name === "browser") { loadMyBrowserTrusted(); }
  if (name === "pushover") { loadMyPushover(); }
}

async function loadMyProviderKeys() {
  const r = await fetch("/api/my/provider-keys");
  if (!r.ok) return;
  const d = await r.json();

  const antEl = document.getElementById("my-apikey-anthropic-status");
  const orEl  = document.getElementById("my-apikey-openrouter-status");
  const oaiEl = document.getElementById("my-apikey-openai-status");
  const noteEl = document.getElementById("my-api-keys-tier-note");

  if (antEl) antEl.innerHTML = d.anthropic_set
    ? '<span style="color:var(--green)">&#10003; Your own key is set</span>'
    : (d.anthropic_blocked ? '<span style="color:var(--yellow)">Anthropic disabled — set your own key to unlock</span>'
    : '<span style="color:var(--text-dim)">Not set</span>');

  if (orEl) orEl.innerHTML = d.openrouter_set
    ? '<span style="color:var(--green)">&#10003; Your own key is set</span>'
    : (d.openrouter_free_only ? '<span style="color:var(--yellow)">Using system default (free models only — set your own key for full access)</span>'
    : '<span style="color:var(--text-dim)">Not set</span>');

  if (oaiEl) oaiEl.innerHTML = d.openai_set
    ? '<span style="color:var(--green)">&#10003; Your own key is set</span>'
    : '<span style="color:var(--text-dim)">Not set — required to use OpenAI models</span>';

  _renderAccessNote(noteEl, d);
}

async function saveMyProviderKey(provider) {
  const val = document.getElementById(`my-apikey-${provider}-input`)?.value.trim();
  if (!val) { showFlash("Enter a key first."); return; }
  const r = await fetch("/api/my/provider-keys", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ provider, key: val }),
  });
  if (r.ok) {
    document.getElementById(`my-apikey-${provider}-input`).value = "";
    showFlash(`${provider} key saved.`);
    loadMyProviderKeys();
  } else {
    const d = await r.json().catch(() => ({}));
    showFlash(d.detail || "Error saving key.");
  }
}

async function clearMyProviderKey(provider) {
  const r = await fetch(`/api/my/provider-keys/${provider}`, { method: "DELETE" });
  if (r.ok) { showFlash(`${provider} key cleared.`); loadMyProviderKeys(); }
  else showFlash("Error clearing key.");
}

async function loadMyPersonality() {
  const r = await fetch("/api/my/personality");
  if (!r.ok) return;
  const d = await r.json();
  const soulEl = document.getElementById("my-personality-soul");
  const userEl = document.getElementById("my-personality-user");
  if (soulEl) soulEl.value = d.soul || "";
  if (userEl) userEl.value = d.user_context || "";
}

async function saveMyPersonality() {
  const soul = document.getElementById("my-personality-soul")?.value ?? null;
  const user_context = document.getElementById("my-personality-user")?.value ?? null;
  const r = await fetch("/api/my/personality", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ soul, user_context }),
  });
  if (r.ok) showFlash("Personality saved.");
  else { const d = await r.json().catch(() => ({})); showFlash(d.detail || "Error saving."); }
}

async function clearMyPersonality() {
  const r = await fetch("/api/my/personality", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ soul: "", user_context: "" }),
  });
  if (r.ok) {
    document.getElementById("my-personality-soul").value = "";
    document.getElementById("my-personality-user").value = "";
    showFlash("Personality reset to system defaults.");
  }
}

async function dismissNag() {
  document.getElementById("nag-banner")?.remove();
  await fetch("/api/my/personality/dismiss-nag", { method: "POST" });
}

// ── My MCP Servers ────────────────────────────────────────────────────────────

let _myMcpServerId = null;

async function loadMyMcpServers() {
  const r = await fetch("/api/my/mcp-servers");
  if (!r.ok) return;
  const servers = await r.json();
  const tbody = document.getElementById("my-mcp-tbody");
  if (!tbody) return;
  if (!servers.length) {
    tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">No personal MCP servers configured.</td></tr>';
    return;
  }
  tbody.innerHTML = servers.map(s => `
    <tr>
      <td>${s.name}</td>
      <td style="font-size:12px;color:var(--text-dim)">${s.url}</td>
      <td>${s.transport}</td>
      <td>${s.enabled ? '<span style="color:var(--green)">enabled</span>' : '<span style="color:var(--text-dim)">disabled</span>'}</td>
      <td style="text-align:right">
        <button class="btn btn-small btn-ghost" onclick="editMyMcpServer('${s.id}')">Edit</button>
        <button class="btn btn-small btn-ghost" onclick="toggleMyMcpServer('${s.id}')">${s.enabled ? 'Disable' : 'Enable'}</button>
        <button class="btn btn-small btn-danger" onclick="deleteMyMcpServer('${s.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

function openMyMcpModal(serverId) {
  _myMcpServerId = serverId || null;
  const modal = document.getElementById("my-mcp-modal");
  if (!modal) return;
  if (!serverId) {
    document.getElementById("my-mcp-name").value = "";
    document.getElementById("my-mcp-url").value = "";
    document.getElementById("my-mcp-transport").value = "sse";
    document.getElementById("my-mcp-apikey").value = "";
    document.getElementById("my-mcp-modal-title").textContent = "Add MCP Server";
  }
  modal.classList.remove("hidden");
}

async function editMyMcpServer(id) {
  const r = await fetch(`/api/my/mcp-servers/${id}`);
  if (!r.ok) return;
  const s = await r.json();
  _myMcpServerId = id;
  document.getElementById("my-mcp-name").value = s.name;
  document.getElementById("my-mcp-url").value = s.url;
  document.getElementById("my-mcp-transport").value = s.transport;
  document.getElementById("my-mcp-apikey").value = "";
  document.getElementById("my-mcp-modal-title").textContent = "Edit MCP Server";
  document.getElementById("my-mcp-modal").classList.remove("hidden");
}

async function saveMyMcpServer() {
  const body = {
    name: document.getElementById("my-mcp-name").value.trim(),
    url: document.getElementById("my-mcp-url").value.trim(),
    transport: document.getElementById("my-mcp-transport").value,
    api_key: document.getElementById("my-mcp-apikey").value.trim() || null,
    enabled: true,
  };
  if (!body.name || !body.url) { showFlash("Name and URL are required."); return; }
  const method = _myMcpServerId ? "PUT" : "POST";
  const url = _myMcpServerId ? `/api/my/mcp-servers/${_myMcpServerId}` : "/api/my/mcp-servers";
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
  if (r.ok) {
    closeMyMcpModal();
    showFlash("MCP server saved.");
    loadMyMcpServers();
  } else {
    const d = await r.json().catch(() => ({}));
    showFlash(d.detail || "Error saving server.");
  }
}

async function toggleMyMcpServer(id) {
  const r = await fetch(`/api/my/mcp-servers/${id}/toggle`, { method: "POST" });
  if (r.ok) { showFlash("Server updated."); loadMyMcpServers(); }
  else showFlash("Error toggling server.");
}

async function deleteMyMcpServer(id) {
  if (!confirm("Delete this MCP server?")) return;
  const r = await fetch(`/api/my/mcp-servers/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Server deleted."); loadMyMcpServers(); }
  else showFlash("Error deleting server.");
}

function closeMyMcpModal() {
  document.getElementById("my-mcp-modal")?.classList.add("hidden");
  _myMcpServerId = null;
}

// ── My Inbox ──────────────────────────────────────────────────────────────────

let _myInboxTriggerId = null;

async function loadMyInboxConfig() {
  const r = await fetch("/api/my/inbox/config");
  if (!r.ok) return;
  const d = await r.json();
  const fields = ["imap_host","imap_port","imap_username","smtp_host","smtp_port","smtp_username"];
  for (const f of fields) {
    const el = document.getElementById(`my-inbox-${f.replace("_","-")}`);
    if (el) el.value = d[f] || "";
  }
  // Load status
  const sr = await fetch("/api/my/inbox/status");
  const s = sr.ok ? await sr.json() : {};
  const statusEl = document.getElementById("my-inbox-status");
  if (statusEl) {
    if (!s.configured) statusEl.innerHTML = '<span style="color:var(--text-dim)">Not configured</span>';
    else if (s.connected) statusEl.innerHTML = '<span style="color:var(--green)">Connected</span>';
    else statusEl.innerHTML = `<span style="color:var(--yellow)">Disconnected${s.error ? ': ' + s.error : ''}</span>`;
  }
}

async function loadMyInboxTriggers() {
  const r = await fetch("/api/my/inbox/triggers");
  if (!r.ok) return;
  const triggers = await r.json();
  const tbody = document.getElementById("my-inbox-triggers-tbody");
  if (!tbody) return;
  if (!triggers.length) {
    tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">No triggers configured.</td></tr>';
    return;
  }
  tbody.innerHTML = triggers.map(t => `
    <tr>
      <td><code>${t.trigger_word}</code></td>
      <td>${t.agent_name || t.agent_id}</td>
      <td>${t.enabled ? '<span style="color:var(--green)">enabled</span>' : '<span style="color:var(--text-dim)">disabled</span>'}</td>
      <td style="text-align:right">
        <button class="btn btn-small btn-ghost" onclick="editMyInboxTrigger('${t.id}','${t.trigger_word}','${t.agent_id}','${t.description||''}')">Edit</button>
        <button class="btn btn-small btn-ghost" onclick="toggleMyInboxTrigger('${t.id}')">${t.enabled?'Disable':'Enable'}</button>
        <button class="btn btn-small btn-danger" onclick="deleteMyInboxTrigger('${t.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

async function saveMyInboxConfig() {
  const body = {};
  const fields = ["imap_host","imap_port","imap_username","imap_password","smtp_host","smtp_port","smtp_username","smtp_password"];
  for (const f of fields) {
    const el = document.getElementById(`my-inbox-${f.replace(/_/g,"-")}`);
    if (el && el.value.trim()) body[f] = el.value.trim();
  }
  const r = await fetch("/api/my/inbox/config", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify(body) });
  if (r.ok) {
    await fetch("/api/my/inbox/reconnect", { method:"POST" });
    showFlash("Inbox config saved. Connecting…");
    setTimeout(loadMyInboxConfig, 2000);
  } else showFlash("Error saving inbox config.");
}

function openMyInboxTriggerModal(id, word, agentId, desc) {
  _myInboxTriggerId = id || null;
  document.getElementById("my-inbox-trigger-id").value = id || "";
  document.getElementById("my-inbox-trigger-word").value = word || "";
  document.getElementById("my-inbox-trigger-desc").value = desc || "";
  document.getElementById("my-inbox-trigger-modal-title").textContent = id ? "Edit Trigger" : "Add Inbox Trigger";
  _populateAgentSelect("my-inbox-trigger-agent", agentId);
  document.getElementById("my-inbox-trigger-modal").classList.remove("hidden");
}

function editMyInboxTrigger(id, word, agentId, desc) { openMyInboxTriggerModal(id, word, agentId, desc); }
function closeMyInboxTriggerModal() { document.getElementById("my-inbox-trigger-modal")?.classList.add("hidden"); _myInboxTriggerId = null; }

async function saveMyInboxTrigger() {
  const body = {
    trigger_word: document.getElementById("my-inbox-trigger-word").value.trim(),
    agent_id: document.getElementById("my-inbox-trigger-agent").value,
    description: document.getElementById("my-inbox-trigger-desc").value.trim(),
    enabled: true,
  };
  if (!body.trigger_word || !body.agent_id) { showFlash("Trigger word and agent are required."); return; }
  const method = _myInboxTriggerId ? "PUT" : "POST";
  const url = _myInboxTriggerId ? `/api/my/inbox/triggers/${_myInboxTriggerId}` : "/api/my/inbox/triggers";
  const r = await fetch(url, { method, headers:{"Content-Type":"application/json"}, body:JSON.stringify(body) });
  if (r.ok) { closeMyInboxTriggerModal(); showFlash("Trigger saved."); loadMyInboxTriggers(); }
  else { const d = await r.json().catch(()=>({})); showFlash(d.detail || "Error saving trigger."); }
}

async function toggleMyInboxTrigger(id) {
  const r = await fetch(`/api/my/inbox/triggers/${id}/toggle`, { method:"POST" });
  if (r.ok) { showFlash("Trigger updated."); loadMyInboxTriggers(); }
}

async function deleteMyInboxTrigger(id) {
  if (!confirm("Delete this trigger?")) return;
  const r = await fetch(`/api/my/inbox/triggers/${id}`, { method:"DELETE" });
  if (r.ok) { showFlash("Trigger deleted."); loadMyInboxTriggers(); }
}

// ── My Telegram ───────────────────────────────────────────────────────────────

let _myTelegramTriggerId = null;

async function loadMyTelegramConfig() {
  const r = await fetch("/api/my/telegram/config");
  if (!r.ok) return;
  const d = await r.json();
  const sr = await fetch("/api/my/telegram/status");
  const s = sr.ok ? await sr.json() : {};
  const statusEl = document.getElementById("my-telegram-status");
  if (statusEl) {
    if (!d.bot_token_set) statusEl.innerHTML = '<span style="color:var(--text-dim)">No bot configured</span>';
    else if (s.running) statusEl.innerHTML = '<span style="color:var(--green)">Bot running</span>';
    else statusEl.innerHTML = `<span style="color:var(--yellow)">Bot stopped${s.error ? ': ' + s.error : ''}</span>`;
  }
}

async function loadMyTelegramWhitelist() {
  const r = await fetch("/api/my/telegram-whitelist");
  if (!r.ok) return;
  const list = await r.json();
  const tbody = document.getElementById("my-telegram-whitelist-tbody");
  if (!tbody) return;
  if (!list.length) { tbody.innerHTML = '<tr><td colspan="3" style="color:var(--text-dim)">No whitelisted chats.</td></tr>'; return; }
  tbody.innerHTML = list.map(e => `
    <tr>
      <td>${e.chat_id}</td>
      <td>${e.label || ''}</td>
      <td style="text-align:right"><button class="btn btn-small btn-danger" onclick="removeMyTelegramWhitelist('${e.chat_id}')">Remove</button></td>
    </tr>`).join("");
}

async function loadMyTelegramTriggers() {
  const r = await fetch("/api/my/telegram-triggers");
  if (!r.ok) return;
  const triggers = await r.json();
  const tbody = document.getElementById("my-telegram-triggers-tbody");
  if (!tbody) return;
  if (!triggers.length) { tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">No triggers configured.</td></tr>'; return; }
  tbody.innerHTML = triggers.map(t => `
    <tr>
      <td><code>${t.trigger_word}</code></td>
      <td>${t.agent_name || t.agent_id}</td>
      <td>${t.enabled ? '<span style="color:var(--green)">enabled</span>' : '<span style="color:var(--text-dim)">disabled</span>'}</td>
      <td style="text-align:right">
        <button class="btn btn-small btn-ghost" onclick="editMyTelegramTrigger('${t.id}','${t.trigger_word}','${t.agent_id}','${t.description||''}')">Edit</button>
        <button class="btn btn-small btn-ghost" onclick="toggleMyTelegramTrigger('${t.id}')">${t.enabled?'Disable':'Enable'}</button>
        <button class="btn btn-small btn-danger" onclick="deleteMyTelegramTrigger('${t.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

async function saveMyTelegramConfig() {
  const token = document.getElementById("my-telegram-bot-token")?.value.trim();
  if (!token) { showFlash("Enter a bot token first."); return; }
  const r = await fetch("/api/my/telegram/config", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ bot_token: token }) });
  if (r.ok) {
    document.getElementById("my-telegram-bot-token").value = "";
    await fetch("/api/my/telegram/reconnect", { method:"POST" });
    showFlash("Telegram bot saved. Starting…");
    setTimeout(loadMyTelegramConfig, 2000);
  } else showFlash("Error saving Telegram config.");
}

async function deleteMyTelegramConfig() {
  if (!confirm("Remove your Telegram bot configuration?")) return;
  const r = await fetch("/api/my/telegram/config", { method:"DELETE" });
  if (r.ok) { showFlash("Telegram bot removed."); loadMyTelegramConfig(); }
}

async function addMyTelegramWhitelist() {
  const chatId = document.getElementById("my-tg-chat-id")?.value.trim();
  const label = document.getElementById("my-tg-chat-label")?.value.trim() || "";
  if (!chatId) { showFlash("Enter a chat ID."); return; }
  const r = await fetch("/api/my/telegram-whitelist", { method:"POST", headers:{"Content-Type":"application/json"}, body:JSON.stringify({ chat_id: chatId, label }) });
  if (r.ok) {
    document.getElementById("my-tg-chat-id").value = "";
    document.getElementById("my-tg-chat-label").value = "";
    showFlash("Chat ID whitelisted.");
    loadMyTelegramWhitelist();
  } else showFlash("Error adding chat ID.");
}

async function removeMyTelegramWhitelist(chatId) {
  if (!confirm(`Remove chat ID ${chatId} from your whitelist?`)) return;
  const r = await fetch(`/api/my/telegram-whitelist/${chatId}`, { method:"DELETE" });
  if (r.ok) { showFlash("Chat ID removed."); loadMyTelegramWhitelist(); }
}

function openMyTelegramTriggerModal(id, word, agentId, desc) {
  _myTelegramTriggerId = id || null;
  document.getElementById("my-telegram-trigger-id").value = id || "";
  document.getElementById("my-telegram-trigger-word").value = word || "";
  document.getElementById("my-telegram-trigger-desc").value = desc || "";
  document.getElementById("my-telegram-trigger-modal-title").textContent = id ? "Edit Trigger" : "Add Telegram Trigger";
  _populateAgentSelect("my-telegram-trigger-agent", agentId);
  document.getElementById("my-telegram-trigger-modal").classList.remove("hidden");
}

function editMyTelegramTrigger(id, word, agentId, desc) { openMyTelegramTriggerModal(id, word, agentId, desc); }
function closeMyTelegramTriggerModal() { document.getElementById("my-telegram-trigger-modal")?.classList.add("hidden"); _myTelegramTriggerId = null; }

async function saveMyTelegramTrigger() {
  const body = {
    trigger_word: document.getElementById("my-telegram-trigger-word").value.trim(),
    agent_id: document.getElementById("my-telegram-trigger-agent").value,
    description: document.getElementById("my-telegram-trigger-desc").value.trim(),
    enabled: true,
  };
  if (!body.trigger_word || !body.agent_id) { showFlash("Trigger word and agent are required."); return; }
  const method = _myTelegramTriggerId ? "PUT" : "POST";
  const url = _myTelegramTriggerId ? `/api/my/telegram-triggers/${_myTelegramTriggerId}` : "/api/my/telegram-triggers";
  const r = await fetch(url, { method, headers:{"Content-Type":"application/json"}, body:JSON.stringify(body) });
  if (r.ok) { closeMyTelegramTriggerModal(); showFlash("Trigger saved."); loadMyTelegramTriggers(); }
  else { const d = await r.json().catch(()=>({})); showFlash(d.detail || "Error saving trigger."); }
}

async function toggleMyTelegramTrigger(id) {
  const r = await fetch(`/api/my/telegram-triggers/${id}/toggle`, { method:"POST" });
  if (r.ok) { showFlash("Trigger updated."); loadMyTelegramTriggers(); }
}

async function deleteMyTelegramTrigger(id) {
  if (!confirm("Delete this trigger?")) return;
  const r = await fetch(`/api/my/telegram-triggers/${id}`, { method:"DELETE" });
  if (r.ok) { showFlash("Trigger deleted."); loadMyTelegramTriggers(); }
}

// Helper: populate an agent <select> element
async function _populateAgentSelect(selectId, selectedId) {
  const sel = document.getElementById(selectId);
  if (!sel) return;
  try {
    const r = await fetch("/api/agents");
    const agents = r.ok ? await r.json() : [];
    sel.innerHTML = agents.map(a =>
      `<option value="${a.id}"${a.id === selectedId ? " selected" : ""}>${a.name}</option>`
    ).join("");
  } catch (e) {}
}

// ── Email Handling Accounts ────────────────────────────────────────────────────

let _emailAccounts = [];
let _editingAccountId = null;

async function loadEmailAccounts() {
  const tbody = document.getElementById("email-accounts-tbody");
  if (!tbody) return;
  const r = await fetch("/api/my/email-accounts");
  if (!r.ok) { tbody.innerHTML = '<tr><td colspan="5" style="color:var(--danger)">Error loading accounts</td></tr>'; return; }
  _emailAccounts = await r.json();
  if (!_emailAccounts.length) {
    tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-dim)">No handling accounts configured.</td></tr>';
    return;
  }
  tbody.innerHTML = _emailAccounts.map(a => `
    <tr>
      <td>${esc(a.label)}</td>
      <td style="color:var(--text-dim);font-size:12px">${esc(a.imap_username)}</td>
      <td>
        <span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:${a.enabled ? 'var(--green)' : 'var(--text-dim)'}">${a.enabled ? 'enabled' : 'disabled'}</span>
        ${a.paused ? '<span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:var(--yellow,#e5a) ;margin-left:4px">paused</span>' : ''}
      </td>
      <td style="font-size:12px;color:var(--text-dim)">${esc(a.agent_model || '—')}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="editEmailAccount('${a.id}')">Edit</button>
        <button class="btn btn-ghost btn-small" onclick="toggleEmailAccount('${a.id}')">${a.enabled ? 'Disable' : 'Enable'}</button>
        <button class="btn btn-danger btn-small" onclick="deleteEmailAccount('${a.id}')">Delete</button>
      </td>
    </tr>
  `).join("");
}

function openEmailAccountModal(id) {
  _editingAccountId = id || null;
  const acct = id ? _emailAccounts.find(a => a.id === id) : null;
  const modal = document.getElementById("email-account-modal");
  if (!modal) return;
  document.getElementById("eam-title").textContent = id ? "Edit Handling Account" : "Add Handling Account";
  document.getElementById("eam-label").value = acct?.label || "";
  document.getElementById("eam-imap-host").value = acct?.imap_host || "";
  document.getElementById("eam-imap-port").value = acct?.imap_port || "993";
  document.getElementById("eam-imap-username").value = acct?.imap_username || "";
  document.getElementById("eam-imap-password").value = "";
  document.getElementById("eam-initial-load-limit").value = acct?.initial_load_limit || "200";
  document.getElementById("eam-folders-display").textContent = (acct?.monitored_folders || ["INBOX"]).join(", ");
  document.getElementById("eam-folders-hidden").value = JSON.stringify(acct?.monitored_folders || ["INBOX"]);
  document.getElementById("eam-agent-prompt").value = acct?.agent_prompt || "";
  const mtc = document.getElementById("eam-max-tool-calls");
  if (mtc) mtc.value = acct?.agent_max_tool_calls != null ? acct.agent_max_tool_calls : "";
  const pm = document.getElementById("eam-prompt-mode");
  if (pm) pm.value = acct?.agent_prompt_mode || "combined";
  _populateEamModelSelect(acct?.agent_model);
  _loadEamExtraTools(acct?.extra_tools || [], acct?.telegram_chat_id || "", acct?.telegram_keyword || "");

  // Keyword field — show only on edit (create sets it after Telegram checkbox)
  const kwRow = document.getElementById("eam-keyword-row");
  const kwInput = document.getElementById("eam-telegram-keyword");
  const kwPreview = document.getElementById("eam-keyword-preview");
  if (kwRow && kwInput) {
    kwInput.value = acct?.telegram_keyword || "";
    if (kwPreview) kwPreview.textContent = acct?.telegram_keyword || "keyword";
    kwRow.style.display = (acct?.extra_tools || []).includes("telegram") ? "" : "none";
  }

  // Pause/resume button — edit only
  const pauseRow = document.getElementById("eam-pause-row");
  const pauseBtn = document.getElementById("eam-pause-btn");
  const pauseStatus = document.getElementById("eam-pause-status");
  if (pauseRow) {
    if (id) {
      pauseRow.style.display = "";
      const paused = acct?.paused;
      if (pauseBtn) pauseBtn.textContent = paused ? "Resume listener" : "Pause listener";
      if (pauseStatus) pauseStatus.textContent = paused ? "Listener is paused" : "Listener is running";
    } else {
      pauseRow.style.display = "none";
    }
  }

  modal.style.display = "flex";
}

async function _loadEamExtraTools(currentSelected, currentChatId, currentKeyword) {
  const area = document.getElementById("eam-extra-tools-area");
  if (!area) return;
  area.innerHTML = '<span style="font-size:12px;color:var(--text-dim)">Loading…</span>';
  const [toolsRes, chatsRes] = await Promise.all([
    fetch("/api/my/email-accounts/available-extra-tools"),
    fetch("/api/my/telegram/whitelisted-chats"),
  ]);
  if (!toolsRes.ok) { area.innerHTML = '<span style="font-size:12px;color:var(--text-dim)">No notification tools configured.</span>'; return; }
  const tools = await toolsRes.json();
  const chats = chatsRes.ok ? await chatsRes.json() : [];
  if (!tools.length) {
    area.innerHTML = '<span style="font-size:12px;color:var(--text-dim)">No notification tools configured. Add Telegram or Pushover in Settings → Credentials.</span>';
    return;
  }
  area.innerHTML = tools.map(t => {
    let extra = "";
    if (t.id === "telegram") {
      const chatOptions = chats.map(c =>
        `<option value="${esc(c.chat_id)}" ${c.chat_id === currentChatId ? "selected" : ""}>${esc(c.label)}</option>`
      ).join("");
      const noChats = !chats.length ? `<option value="">No whitelisted chats — add one in Settings → Telegram</option>` : "";
      extra = `
        <div id="eam-telegram-chat-row" style="margin-top:6px;display:${currentSelected.includes("telegram") ? "block" : "none"}">
          <label style="font-size:12px;color:var(--text-dim);display:block;margin-bottom:4px">Send notifications to chat:</label>
          <select id="eam-telegram-chat-id" class="form-input" style="font-size:12px;padding:4px 8px">
            ${noChats || chatOptions}
          </select>
        </div>`;
    }
    return `
    <div>
      <label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer;font-size:13px">
        <input type="checkbox" id="eam-extra-${esc(t.id)}" value="${esc(t.id)}"
          ${currentSelected.includes(t.id) ? "checked" : ""}
          style="margin-top:2px;accent-color:var(--accent)"
          onchange="_onEamExtraToolChange('${esc(t.id)}')">
        <span>
          <span style="font-weight:500;color:var(--text)">${esc(t.label)}</span>
          <span style="color:var(--text-dim);font-size:12px;display:block">${esc(t.description)}</span>
        </span>
      </label>
      ${extra}
    </div>`;
  }).join("");
}

function _onEamExtraToolChange(toolId) {
  if (toolId === "telegram") {
    const checked = document.getElementById("eam-extra-telegram")?.checked;
    const chatRow = document.getElementById("eam-telegram-chat-row");
    if (chatRow) chatRow.style.display = checked ? "block" : "none";
    const kwRow = document.getElementById("eam-keyword-row");
    if (kwRow) kwRow.style.display = checked ? "" : "none";
  }
}

function editEmailAccount(id) { openEmailAccountModal(id); }

function closeEmailAccountModal() {
  const modal = document.getElementById("email-account-modal");
  if (modal) modal.style.display = "none";
}

async function _populateEamModelSelect(selectedModel) {
  const sel = document.getElementById("eam-agent-model");
  if (!sel) return;
  sel.innerHTML = '<option value="">Loading…</option>';
  const r = await fetch("/api/models");
  const d = r.ok ? await r.json() : {};
  const models = d.models || [];
  const chosen = selectedModel || d.default || "";
  _fillModelSelect(sel, models, chosen);
}

async function loadEamFolders() {
  const btn = document.getElementById("eam-load-folders-btn");
  if (btn) btn.textContent = "Loading…";
  let r;
  const host = document.getElementById("eam-imap-host").value.trim();
  const port = document.getElementById("eam-imap-port").value.trim() || "993";
  const username = document.getElementById("eam-imap-username").value.trim();
  const password = document.getElementById("eam-imap-password").value;
  if (_editingAccountId && !password) {
    // Use saved DB credentials — no new password entered
    r = await fetch(`/api/my/email-accounts/${_editingAccountId}/list-folders`, { method: "POST" });
  } else {
    if (!host || !username || !password) {
      if (btn) btn.textContent = "Load folders";
      showFlash("Enter IMAP host, username and password first.");
      return;
    }
    r = await fetch("/api/my/email-accounts/list-folders-preview", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ imap_host: host, imap_port: port, imap_username: username, imap_password: password }),
    });
  }
  if (btn) btn.textContent = "Load folders";
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Failed to load folders: " + (e.detail || r.status)); return; }
  const d = await r.json();
  const folders = d.folders || [];
  const currentSelected = JSON.parse(document.getElementById("eam-folders-hidden").value || '["INBOX"]');
  const container = document.getElementById("eam-folders-checklist");
  if (!container) return;
  container.innerHTML = folders.map(f => `
    <label style="display:flex;align-items:center;gap:6px;font-size:13px;cursor:pointer;padding:2px 0">
      <input type="checkbox" value="${esc(f)}" ${currentSelected.includes(f) ? "checked" : ""}
        onchange="_updateFolderSelection()"> ${esc(f)}
    </label>
  `).join("");
  container.style.display = "";
}

function _updateFolderSelection() {
  const checks = document.querySelectorAll("#eam-folders-checklist input[type=checkbox]:checked");
  const selected = Array.from(checks).map(c => c.value);
  document.getElementById("eam-folders-hidden").value = JSON.stringify(selected.length ? selected : ["INBOX"]);
  document.getElementById("eam-folders-display").textContent = selected.join(", ") || "INBOX";
}

async function saveEmailAccount() {
  const label = document.getElementById("eam-label").value.trim();
  const imap_host = document.getElementById("eam-imap-host").value.trim();
  const imap_port = document.getElementById("eam-imap-port").value.trim() || "993";
  const imap_username = document.getElementById("eam-imap-username").value.trim();
  const imap_password = document.getElementById("eam-imap-password").value;
  const agent_model = document.getElementById("eam-agent-model").value;
  const agent_prompt = document.getElementById("eam-agent-prompt").value.trim();
  const _mtcRaw = document.getElementById("eam-max-tool-calls")?.value;
  const agent_max_tool_calls = _mtcRaw ? parseInt(_mtcRaw) : null;
  const agent_prompt_mode = document.getElementById("eam-prompt-mode")?.value || "combined";
  const initial_load_limit = parseInt(document.getElementById("eam-initial-load-limit").value || "200");
  const monitored_folders = JSON.parse(document.getElementById("eam-folders-hidden").value || '["INBOX"]');
  const extra_tools = Array.from(document.querySelectorAll("#eam-extra-tools-area input[type=checkbox]:checked")).map(c => c.value);
  const telegram_chat_id = extra_tools.includes("telegram")
    ? (document.getElementById("eam-telegram-chat-id")?.value || "")
    : "";
  const telegram_keyword = extra_tools.includes("telegram")
    ? (document.getElementById("eam-telegram-keyword")?.value || "").trim()
    : "";

  if (!label || !imap_host || !imap_username) { showFlash("Label, IMAP host and username are required."); return; }
  if (!_editingAccountId && !imap_password) { showFlash("Password is required for new accounts."); return; }
  if (!agent_model) { showFlash("Please select a model for the handling agent."); return; }
  if (extra_tools.includes("telegram") && !telegram_chat_id) { showFlash("Please select a Telegram chat to send notifications to."); return; }
  if (extra_tools.includes("telegram") && !telegram_keyword) { showFlash("Please enter a reply keyword for Telegram (e.g. 'work')."); return; }

  const payload = { label, imap_host, imap_port, imap_username, imap_password,
    agent_model, agent_prompt, agent_max_tool_calls, agent_prompt_mode,
    initial_load_limit, monitored_folders, extra_tools,
    telegram_chat_id, telegram_keyword };

  const url = _editingAccountId ? `/api/my/email-accounts/${_editingAccountId}` : "/api/my/email-accounts";
  const method = _editingAccountId ? "PUT" : "POST";
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  closeEmailAccountModal();
  showFlash("Account saved.");
  loadEmailAccounts();
}

async function toggleEmailAccount(id) {
  const r = await fetch(`/api/my/email-accounts/${id}/toggle`, { method: "POST" });
  if (r.ok) { showFlash("Account updated."); loadEmailAccounts(); }
}

async function deleteEmailAccount(id) {
  if (!confirm("Delete this handling account?")) return;
  const r = await fetch(`/api/my/email-accounts/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Account deleted."); loadEmailAccounts(); }
}

async function togglePauseEmailAccount() {
  if (!_editingAccountId) return;
  const acct = _emailAccounts.find(a => a.id === _editingAccountId);
  const paused = acct?.paused;
  const action = paused ? "resume" : "pause";
  const r = await fetch(`/api/my/email-accounts/${_editingAccountId}/${action}`, { method: "POST" });
  if (!r.ok) { showFlash("Failed to " + action + " account."); return; }
  showFlash(`Listener ${action}d.`);
  await loadEmailAccounts();
  // Refresh button state
  const updatedAcct = _emailAccounts.find(a => a.id === _editingAccountId);
  const btn = document.getElementById("eam-pause-btn");
  const status = document.getElementById("eam-pause-status");
  if (btn) btn.textContent = updatedAcct?.paused ? "Resume listener" : "Pause listener";
  if (status) status.textContent = updatedAcct?.paused ? "Listener is paused" : "Listener is running";
}

async function loadDataFolder() {
  const hint = document.getElementById("data-folder-hint");
  if (!hint) return;
  const r = await fetch("/api/my/data-folder");
  if (!r.ok) { hint.textContent = "Could not load folder info."; return; }
  const d = await r.json();
  hint.textContent = d.data_folder
    ? `Your folder: ${d.data_folder}`
    : "Not provisioned — ask your administrator to set system:users_base_folder in Credentials.";
}

// ── Per-user SSH key ──────────────────────────────────────────────────────────

async function initSshKey() {
  const area = document.getElementById("ssh-key-area");
  if (!area) return;
  try {
    const r = await fetch("/api/my/ssh/pubkey");
    const d = await r.json();
    if (d.no_folder) {
      area.innerHTML = '<p style="font-size:13px;color:var(--text-dim)">No data folder configured — contact your administrator.</p>';
      return;
    }
    _renderSshKeyArea(d);
  } catch(e) {
    area.innerHTML = '<p style="font-size:13px;color:var(--red)">Failed to load SSH key status.</p>';
  }
}

function _renderSshKeyArea(d) {
  const area = document.getElementById("ssh-key-area");
  if (!area) return;
  if (d.exists) {
    area.innerHTML =
      `<div class="form-group">
        <label>Public key <span style="font-size:11px;color:var(--text-dim)">(add this to remote servers' authorized_keys)</span></label>
        <textarea id="ssh-pubkey-box" class="form-input" readonly rows="3"
          style="font-family:var(--mono);font-size:11px;resize:none;line-height:1.5">${esc(d.pubkey)}</textarea>
      </div>
      <div style="display:flex;gap:8px;flex-wrap:wrap">
        <button class="btn btn-ghost btn-small" onclick="copySshPubkey()">Copy public key</button>
        <button class="btn btn-ghost btn-small" style="color:var(--text-dim)" onclick="generateSshKey(true)">Regenerate…</button>
      </div>`;
  } else {
    area.innerHTML =
      `<p style="font-size:13px;color:var(--text-dim);margin-bottom:12px">No SSH key found in your data folder.</p>
      <button class="btn btn-primary btn-small" onclick="generateSshKey(false)">Generate SSH key</button>`;
  }
}

async function generateSshKey(force) {
  if (force && !confirm(
    "Regenerate SSH key?\n\nThe existing private key will be deleted. " +
    "Any remote servers using it will stop working until you update their authorized_keys."
  )) return;
  const area = document.getElementById("ssh-key-area");
  if (area) area.innerHTML = '<p style="font-size:13px;color:var(--text-dim)">Generating…</p>';
  try {
    const r = await fetch("/api/my/ssh/generate", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({force}),
    });
    const d = await r.json();
    if (!r.ok) throw new Error(d.detail || "Failed");
    _renderSshKeyArea({exists: true, pubkey: d.pubkey});
    showFlash("SSH key generated successfully");
  } catch(e) {
    if (area) _renderSshKeyArea({exists: false});
    showFlash("Error: " + e.message);
  }
}

function copySshPubkey() {
  const box = document.getElementById("ssh-pubkey-box");
  if (!box) return;
  navigator.clipboard.writeText(box.value)
    .then(() => showFlash("Public key copied!"))
    .catch(() => {});
}

// ── Per-user CalDAV ───────────────────────────────────────────────────────────

async function loadMyCaldavConfig() {
  const r = await fetch("/api/my/caldav/config");
  if (!r.ok) return;
  const d = await r.json();
  const el = id => document.getElementById(id);
  if (el("my-caldav-url")) el("my-caldav-url").value = d.url || "";
  if (el("my-caldav-username")) el("my-caldav-username").value = d.username || "";
  if (el("my-caldav-password")) el("my-caldav-password").value = d.password || "";
  if (el("my-caldav-calendar-name")) el("my-caldav-calendar-name").value = d.calendar_name || "";
  // CardDAV
  const sameChk = el("my-carddav-same");
  if (sameChk) {
    sameChk.checked = !!d.carddav_same_as_caldav;
    _applyCarddavSameState(!!d.carddav_same_as_caldav);
  }
  if (el("my-carddav-url")) el("my-carddav-url").value = d.carddav_url || "";
  if (el("my-carddav-username")) el("my-carddav-username").value = d.carddav_username || "";
  if (el("my-carddav-password")) el("my-carddav-password").value = d.carddav_password || "";
  if (el("my-contacts-allow-write")) el("my-contacts-allow-write").checked = !!d.contacts_allow_write;
}

function _applyCarddavSameState(same) {
  const fields = document.getElementById("carddav-custom-fields");
  if (fields) fields.style.display = same ? "none" : "";
}

function onCarddavSameChange() {
  const same = document.getElementById("my-carddav-same")?.checked || false;
  _applyCarddavSameState(same);
}

async function saveMyCaldavConfig() {
  const el = id => document.getElementById(id);
  const same = el("my-carddav-same")?.checked || false;
  const payload = {
    url: el("my-caldav-url")?.value.trim() || "",
    username: el("my-caldav-username")?.value.trim() || "",
    password: el("my-caldav-password")?.value || "",
    calendar_name: el("my-caldav-calendar-name")?.value.trim() || "",
    carddav_same_as_caldav: same,
    carddav_url: same ? "" : (el("my-carddav-url")?.value.trim() || ""),
    carddav_username: same ? "" : (el("my-carddav-username")?.value.trim() || ""),
    carddav_password: same ? "" : (el("my-carddav-password")?.value || ""),
    contacts_allow_write: el("my-contacts-allow-write")?.checked || false,
  };
  const r = await fetch("/api/my/caldav/config", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
  if (r.ok) { showFlash("CalDAV / CardDAV settings saved."); loadMyCaldavConfig(); }
  else { showFlash("Error saving settings."); }
}

async function testMyCaldavConfig() {
  const btn = document.getElementById("caldav-test-btn");
  const res = document.getElementById("caldav-test-result");
  if (btn) btn.textContent = "Testing…";
  const r = await fetch("/api/my/caldav/test", { method: "POST" });
  if (btn) btn.textContent = "Test CalDAV";
  const d = r.ok ? await r.json() : { success: false, message: "Request failed" };
  if (res) { res.textContent = d.message; res.style.color = d.success ? "var(--green)" : "var(--danger)"; }
}

async function testMyCarddavConfig() {
  const btn = document.getElementById("carddav-test-btn");
  const res = document.getElementById("carddav-test-result");
  if (btn) btn.textContent = "Testing…";
  const r = await fetch("/api/my/caldav/test-carddav", { method: "POST" });
  if (btn) btn.textContent = "Test CardDAV";
  const d = r.ok ? await r.json() : { success: false, message: "Request failed" };
  if (res) { res.textContent = d.message; res.style.color = d.success ? "var(--green)" : "var(--danger)"; }
}

async function deleteMyCaldavConfig() {
  if (!confirm("Clear all CalDAV / CardDAV settings? System defaults will be used instead.")) return;
  const r = await fetch("/api/my/caldav/config", { method: "DELETE" });
  if (r.ok) { showFlash("Settings cleared."); loadMyCaldavConfig(); }
}

/* ══════════════════════════════════════════════════════════════════════════
   ADMIN: CalDAV / CardDAV settings tab
══════════════════════════════════════════════════════════════════════════ */

async function loadAdminCaldav() {
  // Admin uses the same per-user CalDAV config endpoint as any other user
  const r = await fetch("/api/my/caldav/config");
  if (!r.ok) return;
  const d = await r.json();
  const el = id => document.getElementById(id);
  // Response field names from /api/my/caldav/config:
  //   url, username, password_set, calendar_name, carddav_same_as_caldav (bool),
  //   carddav_url, carddav_username, carddav_password_set, contacts_allow_write (bool)
  if (el("adm-caldav-host"))          el("adm-caldav-host").value          = d.url || "";
  if (el("adm-caldav-username"))       el("adm-caldav-username").value       = d.username || "";
  if (el("adm-caldav-password"))       el("adm-caldav-password").value       = d.password || "";
  if (el("adm-caldav-calendar-name"))  el("adm-caldav-calendar-name").value  = d.calendar_name || "";
  const same = d.carddav_same_as_caldav === true;
  if (el("adm-carddav-same")) el("adm-carddav-same").checked = same;
  _applyAdmCarddavSameState(same);
  if (!same) {
    if (el("adm-carddav-url"))      el("adm-carddav-url").value      = d.carddav_url || "";
    if (el("adm-carddav-username")) el("adm-carddav-username").value  = d.carddav_username || "";
    if (el("adm-carddav-password")) el("adm-carddav-password").value  = d.carddav_password || "";
  }
  if (el("adm-contacts-allow-write")) el("adm-contacts-allow-write").checked = d.contacts_allow_write === true;
}

function _applyAdmCarddavSameState(same) {
  const fields = document.getElementById("adm-carddav-custom-fields");
  if (fields) fields.style.display = same ? "none" : "";
}

function onAdmCarddavSameChange() {
  const same = document.getElementById("adm-carddav-same")?.checked || false;
  _applyAdmCarddavSameState(same);
}

async function saveAdminCaldav() {
  const el = id => document.getElementById(id);
  const same = el("adm-carddav-same")?.checked || false;
  const payload = {
    url:                   el("adm-caldav-host")?.value.trim()          || "",
    username:              el("adm-caldav-username")?.value.trim()       || "",
    password:              el("adm-caldav-password")?.value              || "",
    calendar_name:         el("adm-caldav-calendar-name")?.value.trim()  || "",
    carddav_same_as_caldav: same,
    carddav_url:            same ? "" : (el("adm-carddav-url")?.value.trim()      || ""),
    carddav_username:       same ? "" : (el("adm-carddav-username")?.value.trim() || ""),
    carddav_password:       same ? "" : (el("adm-carddav-password")?.value        || ""),
    contacts_allow_write:   el("adm-contacts-allow-write")?.checked || false,
  };
  // Don't send placeholder passwords
  if (payload.password === "••••••••") delete payload.password;
  if (payload.carddav_password === "••••••••") delete payload.carddav_password;
  const r = await fetch("/api/my/caldav/config", {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
  });
  if (r.ok) { showFlash("CalDAV / CardDAV settings saved."); loadAdminCaldav(); }
  else { const d = await r.json().catch(() => ({})); showFlash(d.detail || "Error saving settings."); }
}

async function testAdminCaldav() {
  const btn = document.getElementById("adm-caldav-test-btn");
  const res = document.getElementById("adm-caldav-test-result");
  if (btn) btn.disabled = true;
  if (res) res.textContent = "Testing…";
  try {
    const r = await fetch("/api/my/caldav/test", { method: "POST" });
    const d = await r.json();
    if (res) res.innerHTML = d.success
      ? `<span style="color:var(--green)">✓ Connected — ${esc(d.message || "")}</span>`
      : `<span style="color:var(--red)">✗ ${esc(d.message || "Failed")}</span>`;
  } catch (e) {
    if (res) res.innerHTML = `<span style="color:var(--red)">✗ ${esc(String(e))}</span>`;
  } finally {
    if (btn) btn.disabled = false;
  }
}

async function testAdminCarddav() {
  const btn = document.getElementById("adm-carddav-test-btn");
  const res = document.getElementById("adm-carddav-test-result");
  if (btn) btn.disabled = true;
  if (res) res.textContent = "Testing…";
  try {
    const r = await fetch("/api/my/caldav/test-carddav", { method: "POST" });
    const d = await r.json();
    if (res) res.innerHTML = d.success
      ? `<span style="color:var(--green)">✓ Connected — ${esc(d.message || "")}</span>`
      : `<span style="color:var(--red)">✗ ${esc(d.message || "Failed")}</span>`;
  } catch (e) {
    if (res) res.innerHTML = `<span style="color:var(--red)">✗ ${esc(String(e))}</span>`;
  } finally {
    if (btn) btn.disabled = false;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   ADMIN: Pushover tab
══════════════════════════════════════════════════════════════════════════ */

async function loadAdminPushover() {
  const r = await fetch("/api/settings/pushover");
  if (!r.ok) return;
  const d = await r.json();
  const tokenEl = document.getElementById("adm-pushover-app-token");
  const keyEl   = document.getElementById("adm-pushover-user-key");
  const tokenSt = document.getElementById("adm-pushover-app-token-status");
  const keySt   = document.getElementById("adm-pushover-user-key-status");
  if (tokenEl) tokenEl.value = "";  // never pre-fill secrets
  if (keyEl)   keyEl.value   = "";
  if (tokenSt) tokenSt.innerHTML = d.app_token_set
    ? `<span style="color:var(--green)">&#10003; App token is configured</span>`
    : `<span style="color:var(--text-dim)">Not set</span>`;
  if (keySt) keySt.innerHTML = d.user_key_set
    ? `<span style="color:var(--green)">&#10003; Admin user key is configured</span>`
    : `<span style="color:var(--text-dim)">Not set — admin will use system-level key if set</span>`;
}

async function saveAdminPushover() {
  const payload = {};
  const tokenVal = document.getElementById("adm-pushover-app-token")?.value.trim();
  const keyVal   = document.getElementById("adm-pushover-user-key")?.value.trim();
  if (tokenVal) payload.app_token = tokenVal;
  if (keyVal)   payload.user_key  = keyVal;
  if (!Object.keys(payload).length) { showFlash("Enter at least one value to save."); return; }
  const r = await fetch("/api/settings/pushover", {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload),
  });
  if (r.ok) { showFlash("Pushover settings saved."); loadAdminPushover(); }
  else { const d = await r.json().catch(() => ({})); showFlash(d.detail || "Error saving."); }
}

/* ══════════════════════════════════════════════════════════════════════════
   USER: Pushover tab
══════════════════════════════════════════════════════════════════════════ */

async function loadMyPushover() {
  const r = await fetch("/api/my/pushover");
  if (!r.ok) return;
  const d = await r.json();
  const st = document.getElementById("my-pushover-status");
  const inp = document.getElementById("my-pushover-user-key");
  if (inp) inp.value = "";
  if (st) st.innerHTML = d.user_key_set
    ? `<span style="color:var(--green)">&#10003; Your Pushover key is configured</span>`
    : `<span style="color:var(--text-dim)">Not set — notifications will use the admin's key (if configured)</span>`;
}

async function saveMyPushover() {
  const val = document.getElementById("my-pushover-user-key")?.value.trim();
  if (!val) { showFlash("Enter your Pushover user key first."); return; }
  const r = await fetch("/api/my/pushover", {
    method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_key: val }),
  });
  if (r.ok) { showFlash("Pushover key saved."); document.getElementById("my-pushover-user-key").value = ""; loadMyPushover(); }
  else { const d = await r.json().catch(() => ({})); showFlash(d.detail || "Error saving."); }
}

async function deleteMyPushover() {
  if (!confirm("Remove your Pushover user key?")) return;
  const r = await fetch("/api/my/pushover", { method: "DELETE" });
  if (r.ok) { showFlash("Pushover key removed."); loadMyPushover(); }
  else { showFlash("Error removing key."); }
}

function togglePasswordVisibility(inputId, btn) {
  const input = document.getElementById(inputId);
  if (!input) return;
  if (input.type === "password") { input.type = "text"; btn.textContent = "Hide"; }
  else { input.type = "password"; btn.textContent = "Show"; }
}

function initUserSettings() {
  _initMobileTabSelect(switchUserTab);
  const tab = new URLSearchParams(window.location.search).get("tab") || "apikeys";
  switchUserTab(tab);
  loadMyProviderKeys();
  loadMyPersonality();
  loadMyMcpServers();

  document.getElementById("my-browser-trusted-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    await addBrowserTrusted("my-bt-domain", "my-bt-note");
  });
}

function initSettings() {
  // Branch: admin settings vs user settings
  if (document.getElementById("uspane-apikeys")) {
    initUserSettings();
    return;
  }
  if (!document.getElementById("limits-form")) return;

  _initMobileTabSelect(switchSettingsTab);
  loadApiKeyStatus();
  loadProviderKeys();
  loadUsersBaseFolder();
  loadDefaultProvider();
  loadLimits();
  loadDefaultModels();
  loadProxyTrust();
  loadAuditRetention();
  loadBrainStatus();
  loadMcpServers();
  loadSecuritySettings();
  loadLockouts();
  loadBrandingSettings();
  loadEmailWhitelist();
  loadWebWhitelist();
  loadFilesystemWhitelist();
  loadBrowserTrusted();
  reloadCredList();
  loadInboxStatus();
  loadInboxTriggers();
  initInboxForm();
  loadTelegramStatus();
  loadTelegramWhitelist();
  loadTelegramTriggers();
  loadTgDefaultAgent();
  initTelegramConfigForm();
  loadSystemPrompts();

  document.getElementById("telegram-whitelist-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    const chatId = document.getElementById("tg-wl-chatid").value.trim();
    const label  = document.getElementById("tg-wl-label").value.trim();
    if (!chatId) return;
    const r = await fetch("/api/telegram-whitelist", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ chat_id: chatId, label }),
    });
    if (r.ok) {
      document.getElementById("tg-wl-chatid").value = "";
      document.getElementById("tg-wl-label").value  = "";
      showFlash("Added ✓");
      loadTelegramWhitelist();
    } else {
      const d = await r.json().catch(() => ({}));
      alert(d.detail || "Error adding chat ID");
    }
  });

  document.getElementById("proxy-trust-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    const ips = document.getElementById("proxy-trust-ips").value.trim();
    if (!ips) return;
    const r = await fetch("/api/settings/proxy-trust", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ trusted_ips: ips }),
    });
    if (r.ok) showFlash("Saved ✓ — restart the server for this to take effect");
    else showFlash("Error saving proxy trust setting");
  });

  document.getElementById("limits-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    const tc = parseInt(document.getElementById("lim-tool-calls").value, 10);
    const rph = parseInt(document.getElementById("lim-runs-per-hour").value, 10);
    const mcr = parseInt(document.getElementById("lim-concurrent-runs").value, 10);
    const body = {};
    if (!isNaN(tc)) body.max_tool_calls = tc;
    if (!isNaN(rph)) body.max_autonomous_runs_per_hour = rph;
    if (!isNaN(mcr)) body.max_concurrent_runs = mcr;
    const r = await fetch("/api/settings/limits", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (r.ok) showFlash("Limits saved ✓");
    else showFlash("Error saving limits");
  });

  document.getElementById("email-whitelist-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    await addEmail();
  });

  document.getElementById("web-whitelist-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    await addWebDomain();
  });

  document.getElementById("fs-whitelist-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    await addFilesystemPath();
  });

  document.getElementById("browser-trusted-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    await addBrowserTrusted("bt-domain", "bt-note");
  });

  document.getElementById("brain-capture-form")?.addEventListener("submit", async e => {
    e.preventDefault();
    const content = document.getElementById("brain-capture-text").value.trim();
    if (!content) return;
    const btn = e.target.querySelector("button[type=submit]");
    btn.disabled = true;
    btn.textContent = "Capturing…";
    const r = await fetch("/api/brain/capture", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ content }),
    });
    btn.disabled = false;
    btn.textContent = "Capture";
    if (r.ok) {
      const d = await r.json();
      document.getElementById("brain-capture-text").value = "";
      document.getElementById("brain-capture-result").textContent = d.confirmation || "Captured ✓";
      setTimeout(() => { const el = document.getElementById("brain-capture-result"); if (el) el.textContent = ""; }, 5000);
      loadBrainStatus();
    } else {
      showFlash("Error capturing thought");
    }
  });

  refreshStatus();
}

async function loadProxyTrust() {
  try {
    const r = await fetch("/api/settings/proxy-trust");
    const data = await r.json();
    const el = document.getElementById("proxy-trust-ips");
    if (el) el.value = data.trusted_ips;
  } catch { /* ignore */ }
}

async function loadBrainStatus() {
  const badge    = document.getElementById("brain-status-badge");
  const statsEl  = document.getElementById("brain-stats");
  if (!badge) return;

  try {
    const r    = await fetch("/api/brain/status");
    const data = await r.json();

    if (!data.connected) {
      badge.textContent = "Disconnected";
      badge.style.background = "var(--red)";
      badge.style.color = "#fff";
      if (statsEl) statsEl.innerHTML = `<div style="color:var(--text-dim);font-size:13px">${data.error || "Brain DB not available"}</div>`;
      _renderBrainRows([], false);
      return;
    }

    badge.textContent = "Connected";
    badge.style.background = "var(--green)";
    badge.style.color = "#000";

    // Stats cards
    const s = data.stats;
    const byType = (s.by_type || []).map(t => `${t.type}: ${t.count}`).join(" · ") || "—";
    const lastSeen = s.most_recent ? formatDate(s.most_recent) : "—";
    if (statsEl) statsEl.innerHTML = `
      <div class="meta-card"><div class="meta-label">Total thoughts</div><div class="meta-value">${s.total.toLocaleString()}</div></div>
      <div class="meta-card"><div class="meta-label">By type</div><div class="meta-value" style="font-size:12px">${esc(byType)}</div></div>
      <div class="meta-card"><div class="meta-label">Most recent</div><div class="meta-value" style="font-size:12px">${esc(lastSeen)}</div></div>
    `;

    _renderBrainRows(data.recent, false);
  } catch (e) {
    if (badge) { badge.textContent = "Error"; badge.style.background = "var(--red)"; badge.style.color = "#fff"; }
  }
}

function _renderBrainRows(thoughts, isSearch) {
  const tbody   = document.querySelector("#brain-recent-table tbody");
  const heading = document.getElementById("brain-table-heading");
  if (!tbody) return;
  if (heading) heading.textContent = isSearch ? "Search Results" : "Recent Thoughts";
  tbody.innerHTML = "";
  if (!thoughts.length) {
    const msg = isSearch ? "No matching thoughts" : "No thoughts yet";
    tbody.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">${msg}</td></tr>`;
    return;
  }
  for (const t of thoughts) {
    const type    = t.metadata?.type || "—";
    const date    = formatDate(t.created_at);
    const preview = t.content.length > 120 ? t.content.slice(0, 120) + "…" : t.content;
    const tr = document.createElement("tr");
    tr.style.cursor = "pointer";
    tr.title = "Click to view full thought";
    let simBadge = "";
    if (t.similarity != null) {
      const pct = Math.round(t.similarity * 100);
      simBadge = `<span style="margin-left:6px;font-size:10px;color:var(--text-dim)">${pct}%</span>`;
    }
    tr.innerHTML = `
      <td style="font-size:12px;max-width:400px;white-space:pre-wrap;word-break:break-word">${esc(preview)}${simBadge}</td>
      <td><span class="badge badge-blue" style="font-size:11px">${esc(type)}</span></td>
      <td style="font-size:12px;color:var(--text-dim)">${esc(date)}</td>
    `;
    tr.addEventListener("click", () => openBrainThoughtModal(t));
    tbody.appendChild(tr);
  }
}

let _brainSearchTimer = null;

function onBrainSearchInput(val) {
  const clearBtn = document.getElementById("brain-search-clear");
  if (clearBtn) clearBtn.style.display = val.trim() ? "" : "none";
  clearTimeout(_brainSearchTimer);
  if (!val.trim()) { loadBrainStatus(); return; }
  _brainSearchTimer = setTimeout(() => runBrainSearch(val.trim()), 400);
}

async function runBrainSearch(q) {
  const tbody = document.querySelector("#brain-recent-table tbody");
  if (tbody) tbody.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--text-dim)">Searching…</td></tr>`;
  if (document.getElementById("brain-table-heading")) document.getElementById("brain-table-heading").textContent = "Search Results";
  try {
    const r = await fetch(`/api/brain/search?q=${encodeURIComponent(q)}&limit=20`);
    if (!r.ok) { throw new Error(await r.text()); }
    const data = await r.json();
    _renderBrainRows(data.results, true);
  } catch (e) {
    if (tbody) tbody.innerHTML = `<tr><td colspan="3" style="text-align:center;color:var(--red)">Search failed</td></tr>`;
  }
}

function clearBrainSearch() {
  const inp = document.getElementById("brain-search-input");
  const clearBtn = document.getElementById("brain-search-clear");
  if (inp) inp.value = "";
  if (clearBtn) clearBtn.style.display = "none";
  loadBrainStatus();
}

function openBrainThoughtModal(t) {
  document.getElementById("btm-content").textContent = t.content;
  document.getElementById("btm-type").textContent = t.metadata?.type || "—";
  document.getElementById("btm-date").textContent = formatDate(t.created_at);
  const simEl = document.getElementById("btm-sim");
  if (t.similarity != null) {
    simEl.textContent = `${Math.round(t.similarity * 100)}% match`;
    simEl.style.display = "";
  } else {
    simEl.textContent = "";
    simEl.style.display = "none";
  }
  const tagsEl = document.getElementById("btm-tags");
  const tags = t.metadata?.tags || [];
  tagsEl.style.display = tags.length ? "" : "none";
  tagsEl.innerHTML = tags.map(tag => `<span class="badge" style="font-size:11px;background:var(--bg2);color:var(--text-dim)">${esc(tag)}</span>`).join("");
  const modal = document.getElementById("brain-thought-modal");
  modal.style.display = "flex";
  modal.onclick = e => { if (e.target === modal) closeBrainThoughtModal(); };
}

function closeBrainThoughtModal() {
  document.getElementById("brain-thought-modal").style.display = "none";
}

async function loadBrainKey(inputId = "brain-mcp-key", cmdId = "brain-mcp-cmd") {
  const input = document.getElementById(inputId);
  const cmdEl = document.getElementById(cmdId);
  if (!input) return;
  const r = await fetch("/api/my/brain/key");
  if (!r.ok) return;
  const d = await r.json();
  input.value = d.key;
  if (cmdEl) cmdEl.textContent =
    `claude mcp add --transport http brain https://jarvis.oai.pm/brain-mcp/sse --header "x-brain-key: ${d.key}"`;
}

function toggleBrainKeyVisibility(btn, inputId = "brain-mcp-key") {
  const input = document.getElementById(inputId);
  if (!input) return;
  if (input.type === "password") { input.type = "text"; btn.textContent = "Hide"; }
  else { input.type = "password"; btn.textContent = "Show"; }
}

async function copyBrainKey(inputId = "brain-mcp-key") {
  const input = document.getElementById(inputId);
  if (!input) return;
  await navigator.clipboard.writeText(input.value);
  showFlash("Key copied to clipboard");
}

async function regenerateBrainKey(inputId = "brain-mcp-key", cmdId = "brain-mcp-cmd") {
  if (!confirm("Regenerate your brain MCP key? Your existing MCP connections will stop working until you update them.")) return;
  const r = await fetch("/api/my/brain/key/regenerate", { method: "POST" });
  if (!r.ok) { showFlash("Failed to regenerate key"); return; }
  const d = await r.json();
  const input = document.getElementById(inputId);
  const cmdEl = document.getElementById(cmdId);
  if (input) input.value = d.key;
  if (cmdEl) cmdEl.textContent =
    `claude mcp add --transport http brain https://jarvis.oai.pm/brain-mcp/sse --header "x-brain-key: ${d.key}"`;
  showFlash("New key generated — update your MCP clients");
}

async function loadBrainAutoApprove() {
  try {
    const r = await fetch("/api/my/brain-settings");
    if (!r.ok) return;
    const d = await r.json();
    const tog1 = document.getElementById("brain-auto-approve-toggle");
    const tog2 = document.getElementById("ubrain-auto-approve-toggle");
    if (tog1) tog1.checked = !!d.brain_auto_approve;
    if (tog2) tog2.checked = !!d.brain_auto_approve;
  } catch { /* ignore */ }
}

async function saveBrainAutoApprove(enabled) {
  try {
    const r = await fetch("/api/my/brain-settings", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ brain_auto_approve: enabled }),
    });
    if (!r.ok) { showFlash("Failed to save setting"); return; }
    // Show flash next to whichever pane is visible
    const flashEl = document.getElementById("brain-auto-approve-flash") || document.getElementById("ubrain-auto-approve-flash");
    if (flashEl) {
      flashEl.textContent = enabled ? "Auto-approve enabled" : "Auto-approve disabled";
      flashEl.style.display = "block";
      setTimeout(() => { flashEl.style.display = "none"; }, 3000);
    }
  } catch { showFlash("Failed to save setting"); }
}

async function loadLimits() {
  try {
    const r = await fetch("/api/settings/limits");
    const data = await r.json();
    const tcEl  = document.getElementById("lim-tool-calls");
    const rphEl = document.getElementById("lim-runs-per-hour");
    const mcrEl = document.getElementById("lim-concurrent-runs");
    const defaultsEl = document.getElementById("limits-defaults");
    if (tcEl)  tcEl.value  = data.max_tool_calls;
    if (rphEl) rphEl.value = data.max_autonomous_runs_per_hour;
    if (mcrEl) mcrEl.value = data.max_concurrent_runs;
    if (defaultsEl) {
      defaultsEl.textContent =
        `Built-in defaults: tool_calls=${data.defaults.max_tool_calls}, ` +
        `runs/hour=${data.defaults.max_autonomous_runs_per_hour}, ` +
        `concurrent=${data.defaults.max_concurrent_runs}`;
    }
  } catch { /* ignore */ }
}

async function loadDefaultProvider() {
  const sel = document.getElementById("default-provider-select");
  if (!sel) return;
  try {
    const r = await fetch("/api/settings/provider");
    if (!r.ok) return;
    const data = await r.json();
    const labels = { anthropic: "Anthropic", openrouter: "OpenRouter", openai: "OpenAI" };
    const available = data.available_providers || [];
    sel.innerHTML = "";
    if (available.length === 0) {
      const opt = document.createElement("option");
      opt.value = "";
      opt.textContent = "No providers configured";
      sel.appendChild(opt);
      sel.disabled = true;
      sel.closest("section")?.querySelector(".btn")?.setAttribute("disabled", "");
      return;
    }
    for (const p of available) {
      const opt = document.createElement("option");
      opt.value = p;
      opt.textContent = labels[p] || p;
      sel.appendChild(opt);
    }
    sel.disabled = false;
    sel.closest("section")?.querySelector(".btn")?.removeAttribute("disabled");
    sel.value = data.default_provider || available[0];
  } catch { /* ignore */ }
}

async function saveDefaultProvider() {
  const sel = document.getElementById("default-provider-select");
  if (!sel) return;
  const r = await fetch("/api/settings/provider", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ default_provider: sel.value }),
  });
  if (r.ok) showFlash("Default provider saved ✓");
  else showFlash("Error saving default provider");
}

async function loadDefaultModels() {
  const defSel  = document.getElementById("dm-default");
  const freeSel = document.getElementById("dm-free");
  if (!defSel || !freeSel) return;

  // Fetch all models and free-only model info in parallel
  const [modelsResp, infoResp, savedResp] = await Promise.all([
    fetch("/api/models"),
    fetch("/api/models/info"),
    fetch("/api/settings/default-models"),
  ]);

  const allModels  = modelsResp.ok  ? (await modelsResp.json()).models || [] : [];
  const modelInfo  = infoResp.ok    ? await infoResp.json() : [];
  const saved      = savedResp.ok   ? await savedResp.json() : {};

  // Free OpenRouter models: openrouter:* with prompt_per_1m == 0 and completion_per_1m == 0
  const freeModels = modelInfo
    .filter(m => m.id?.startsWith("openrouter:") &&
                 m.pricing?.prompt_per_1m === 0 &&
                 m.pricing?.completion_per_1m === 0)
    .map(m => m.id);

  // Helper: prepend a blank "use default" option after _fillModelSelect clears the select
  const fillWithBlank = (sel, models, currentVal) => {
    _fillModelSelect(sel, models, currentVal);
    const blank = document.createElement("option");
    blank.value = "";
    blank.textContent = "(use .env / first available)";
    sel.insertBefore(blank, sel.firstChild);
    if (!currentVal) sel.value = "";
  };

  fillWithBlank(defSel,  allModels,  saved.default_model  || "");
  fillWithBlank(freeSel, freeModels, saved.free_tier_model || "");

  const form = document.getElementById("default-models-form");
  if (form && !form._bound) {
    form._bound = true;
    form.addEventListener("submit", async e => {
      e.preventDefault();
      const r = await fetch("/api/settings/default-models", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          default_model:   defSel.value  || "",
          free_tier_model: freeSel.value || "",
        }),
      });
      if (r.ok) showFlash("Default models saved ✓");
      else showFlash("Error saving default models");
    });
  }
}

async function loadAuditRetention() {
  try {
    const r = await fetch("/api/settings/audit-retention");
    const data = await r.json();
    const sel = document.getElementById("audit-retention-days");
    if (sel) sel.value = data.days;
  } catch { /* ignore */ }

  const form = document.getElementById("audit-retention-form");
  if (form) form.addEventListener("submit", async e => {
    e.preventDefault();
    const days = parseInt(document.getElementById("audit-retention-days").value, 10);
    const r = await fetch("/api/settings/audit-retention", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ days }),
    });
    if (r.ok) showFlash(days > 0 ? `Retention set to ${days} days ✓` : "Retention disabled (keep forever) ✓");
  });
}

async function clearAuditLog(olderThanDays) {
  const msg = olderThanDays > 0
    ? `Delete all audit entries older than ${olderThanDays} days?`
    : "Delete the ENTIRE audit log? This cannot be undone.";
  if (!confirm(msg)) return;
  const r = await fetch(`/api/audit?older_than_days=${olderThanDays}`, { method: "DELETE" });
  const data = await r.json();
  showFlash(`Deleted ${data.deleted} entries ✓`);
  if (document.getElementById("audit-table")) loadAudit();
}

function toggleCredVisibility() {
  const input = document.getElementById("cred-modal-value");
  if (input) input.type = input.type === "password" ? "text" : "password";
}

async function deleteCred(key) {
  if (!confirm(`Delete credential "${key}"?`)) return;
  const r = await fetch(`/api/credentials/${encodeURIComponent(key)}`, { method: "DELETE" });
  if (r.ok) {
    showFlash("Deleted ✓");
    await reloadCredList();
  } else {
    const d = await r.json();
    alert(d.detail || "Error");
  }
}

const _SYSTEM_CRED_KEYS = new Set([
  "system:paused", "system:max_tool_calls", "system:max_autonomous_runs_per_hour"
]);

const _CRED_LABELS = {
  // AI providers
  "anthropic_api_key":                      "Anthropic API Key",
  "openrouter_api_key":                     "OpenRouter API Key",
  // CalDAV
  "caldav_url":                             "CalDAV URL",
  "caldav_username":                        "CalDAV Username",
  "caldav_password":                        "CalDAV Password",
  "caldav_calendar_name":                   "Calendar Name",
  // Mailcow (legacy keys)
  "mailcow_host":                           "Mailcow Host",
  "mailcow_username":                       "Mailcow Username",
  "mailcow_password":                       "Mailcow Password",
  "mailcow_imap_host":                      "IMAP Host",
  "mailcow_smtp_host":                      "SMTP Host",
  "mailcow_smtp_port":                      "SMTP Port",
  // Pushover
  "pushover_user_key":                      "Pushover User Key",
  "pushover_app_token":                     "Pushover App Token",
  // Inbox
  "inbox:imap_host":                        "IMAP Host",
  "inbox:imap_username":                    "IMAP Username",
  "inbox:imap_password":                    "IMAP Password",
  "inbox:smtp_host":                        "SMTP Host",
  "inbox:smtp_port":                        "SMTP Port",
  "inbox:smtp_username":                    "SMTP Username",
  "inbox:smtp_password":                    "SMTP Password",
  // Telegram
  "telegram:bot_token":                     "Bot Token",
  "telegram:default_agent_id":              "Default Agent",
  // 2nd Brain
  "brain:mcp_key":                          "MCP Key",
  // System — runtime limits
  "system:max_tool_calls":                  "Max Tool Calls",
  "system:max_autonomous_runs_per_hour":    "Max Autonomous Runs / Hour",
  "system:paused":                          "Agent Paused",
  "system:trusted_proxy_ips":              "Trusted Proxy IPs",
  "system:audit_retention_days":            "Audit Log Retention (days)",
  // System — branding
  "system:brand_name":                      "Brand Name",
  "system:brand_logo_filename":             "Brand Logo",
  // System — security
  "system:security_sanitize_enhanced":      "Enhanced Sanitization",
  "system:security_canary_enabled":         "Canary Token Enabled",
  "system:canary_token":                    "Canary Token Value",
  "system:canary_rotated_at":               "Canary Token Last Rotated",
  "system:security_llm_screen_enabled":     "LLM Content Screening",
  "system:security_llm_screen_model":       "LLM Screening Model",
  "system:security_llm_screen_block":       "LLM Screening Block Mode",
  "system:security_output_validation_enabled": "Output Validation",
  "system:security_truncation_enabled":     "Content Truncation",
  "system:security_max_web_chars":          "Max Web Content (chars)",
  "system:security_max_email_chars":        "Max Email Content (chars)",
  "system:security_max_file_chars":         "Max File Content (chars)",
  "system:security_max_subject_chars":      "Max Subject (chars)",
};

const _CRED_PREFIX_LABELS = {
  "inbox": "Inbox", "telegram": "Telegram", "system": "System",
  "brain": "2nd Brain", "caldav": "CalDAV",
};

const _ACRONYMS = new Set(["api", "url", "imap", "smtp", "mcp", "ip", "id"]);

function _credLabel(key) {
  if (_CRED_LABELS[key]) return _CRED_LABELS[key];
  // Fallback: split prefix:suffix, humanize
  const colonIdx = key.indexOf(":");
  let prefix = "", name = key;
  if (colonIdx !== -1) {
    prefix = _CRED_PREFIX_LABELS[key.slice(0, colonIdx)] || _titleCase(key.slice(0, colonIdx));
    name = key.slice(colonIdx + 1);
  }
  const humanName = name.split("_").map(w => _ACRONYMS.has(w) ? w.toUpperCase() : w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
  return prefix ? `${prefix} — ${humanName}` : humanName;
}

function _titleCase(str) {
  return str.split("_").map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
}

// Keys managed in dedicated tabs — excluded from the generic Credentials list.
const _DEDICATED_CRED_KEYS = new Set([
  // CalDAV / CardDAV tab
  "mailcow_host", "mailcow_username", "mailcow_password", "caldav_calendar_name",
  "contacts:allow_write", "carddav_same_as_caldav", "carddav_url", "carddav_username", "carddav_password",
  "mailcow_imap_host", "mailcow_smtp_host", "mailcow_smtp_port",
  // Pushover tab
  "pushover_app_token", "pushover_user_key",
  // Trusted proxy — General tab
  "system:trusted_proxy_ips",
  // These are managed by Inbox / Telegram / Brain / Security / Branding tabs
]);

/* ── Browser trusted domains ─────────────────────────────────────────────── */

function _renderBrowserTrustedTable(rows, tbodyId) {
  const tbody = document.getElementById(tbodyId);
  if (!tbody) return;
  tbody.innerHTML = "";
  if (!rows.length) {
    tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No trusted domains</td></tr>";
    return;
  }
  for (const row of rows) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><code>${esc(row.domain)}</code></td>
      <td style="color:var(--text-dim)">${esc(row.note || "")}</td>
      <td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
                  onclick="removeBrowserTrusted('${esc(row.domain)}')">Remove</button></td>
    `;
    tbody.appendChild(tr);
  }
}

async function loadBrowserTrusted() {
  const r = await fetch("/api/my/browser-trusted");
  if (!r.ok) return;
  _renderBrowserTrustedTable(await r.json(), "browser-trusted-list");
}

async function loadMyBrowserTrusted() {
  const r = await fetch("/api/my/browser-trusted");
  if (!r.ok) return;
  _renderBrowserTrustedTable(await r.json(), "my-browser-trusted-list");
}

async function addBrowserTrusted(domainInputId, noteInputId) {
  const domain = document.getElementById(domainInputId)?.value.trim();
  const note   = document.getElementById(noteInputId)?.value.trim();
  if (!domain) return;
  const r = await fetch("/api/my/browser-trusted", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ domain, note }),
  });
  if (r.ok) {
    document.getElementById(domainInputId).value = "";
    document.getElementById(noteInputId).value   = "";
    showFlash("Added ✓");
    loadBrowserTrusted();
    loadMyBrowserTrusted();
  } else {
    const d = await r.json().catch(() => ({}));
    alert(d.detail || "Error adding domain");
  }
}

async function removeBrowserTrusted(domain) {
  if (!confirm(`Remove "${domain}" from trusted domains?`)) return;
  const r = await fetch(`/api/my/browser-trusted/${encodeURIComponent(domain)}`, { method: "DELETE" });
  if (r.ok) {
    showFlash("Removed ✓");
    loadBrowserTrusted();
    loadMyBrowserTrusted();
  } else {
    showFlash("Error removing domain");
  }
}

/* ── end browser trusted domains ─────────────────────────────────────────── */

async function reloadCredList() {
  const r = await fetch("/api/credentials");
  if (!r.ok) return;
  const rows = await r.json();

  const list = document.getElementById("cred-list");
  if (!list) return;
  list.innerHTML = "";

  // Filter out keys that have dedicated management tabs
  const display = rows.filter(row => {
    const k = typeof row === "string" ? row : row.key;
    return !_DEDICATED_CRED_KEYS.has(k);
  });

  if (!display.length) {
    list.innerHTML = `<tr><td colspan="4" style="text-align:center;color:var(--text-dim)">No custom credentials stored. Use "Add Credential" to add one.</td></tr>`;
    return;
  }

  for (const row of display) {
    const k    = typeof row === "string" ? row : row.key;
    const desc = (typeof row === "object" ? row.description : "") || "";

    const tr = document.createElement("tr");

    const tdKey = document.createElement("td");
    tdKey.innerHTML = `<code style="font-size:12px">${esc(k)}</code>`;

    const tdDesc = document.createElement("td");
    tdDesc.style.cssText = "color:var(--text-dim);font-size:12px";
    tdDesc.textContent = desc;

    const tdStatus = document.createElement("td");
    tdStatus.style.cssText = "font-size:12px";
    tdStatus.innerHTML = `<span style="color:var(--green)">&#10003; Set</span>`;

    const tdAct = document.createElement("td");
    const editBtn = document.createElement("button");
    editBtn.className = "btn btn-ghost";
    editBtn.style.cssText = "padding:4px 10px;font-size:12px;margin-right:6px";
    editBtn.textContent = "Edit";
    editBtn.addEventListener("click", () => openCredModal(k, desc));
    const delBtn = document.createElement("button");
    delBtn.className = "btn btn-danger";
    delBtn.style.cssText = "padding:4px 10px;font-size:12px";
    delBtn.textContent = "Delete";
    delBtn.addEventListener("click", () => deleteCred(k));
    tdAct.appendChild(editBtn);
    tdAct.appendChild(delBtn);

    tr.appendChild(tdKey);
    tr.appendChild(tdDesc);
    tr.appendChild(tdStatus);
    tr.appendChild(tdAct);
    list.appendChild(tr);
  }
}

function _isSensitiveCredKey(key) {
  return /password|token|secret/i.test(key);
}

async function openCredModal(key = null, desc = "") {
  const modal     = document.getElementById("cred-modal");
  const title     = document.getElementById("cred-modal-title");
  const keyInput  = document.getElementById("cred-modal-key-custom");
  const valInput  = document.getElementById("cred-modal-value");
  const descInput = document.getElementById("cred-modal-desc");

  valInput.value = "";
  valInput.type = "password";
  descInput.value = desc;

  if (key) {
    title.textContent = "Edit Credential";
    keyInput.value    = key;
    keyInput.readOnly = true;
    // Pre-fill existing value
    try {
      const r = await fetch(`/api/credentials/${encodeURIComponent(key)}`);
      if (r.ok) {
        const d = await r.json();
        valInput.value = d.value ?? "";
      }
    } catch { /* leave blank */ }
  } else {
    title.textContent = "Add Credential";
    keyInput.readOnly = false;
    keyInput.value    = "";
    descInput.value   = "";
  }
  modal.classList.remove("hidden");
}

function closeCredModal() {
  document.getElementById("cred-modal").classList.add("hidden");
}

async function saveCredModal() {
  const keyInput  = document.getElementById("cred-modal-key-custom");
  const valInput  = document.getElementById("cred-modal-value");
  const descInput = document.getElementById("cred-modal-desc");

  const key = keyInput.value.trim();
  if (!key) { alert("Please enter a credential key name."); return; }

  const value = valInput.value;
  if (!value) { alert("Please enter a value."); return; }

  const r = await fetch("/api/credentials", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ key, value, description: descInput.value.trim() }),
  });
  if (r.ok) {
    closeCredModal();
    showFlash("Saved ✓");
    await reloadCredList();
  } else {
    const d = await r.json().catch(() => ({}));
    alert(d.detail || "Error saving credential");
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   MCP SERVERS SETTINGS
══════════════════════════════════════════════════════════════════════════ */

async function loadMcpServers() {
  const tbody = document.querySelector("#mcp-server-table tbody");
  if (!tbody) return;
  try {
    const r = await fetch("/api/mcp-servers");
    const servers = await r.json();
    if (!servers.length) {
      tbody.innerHTML = `<tr><td colspan="6" style="text-align:center;color:var(--text-dim)">No MCP servers configured yet.</td></tr>`;
      return;
    }
    tbody.innerHTML = "";
    for (const s of servers) {
      const tr = document.createElement("tr");
      tr.innerHTML = `
        <td>${esc(s.name)}</td>
        <td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(s.url)}</td>
        <td>${esc(s.transport)}</td>
        <td id="mcp-tool-count-${s.id}">—</td>
        <td><span style="color:${s.enabled ? "var(--green)" : "var(--text-dim)"}">${s.enabled ? "Enabled" : "Disabled"}</span></td>
        <td style="display:flex;gap:6px;flex-wrap:wrap">
          <button class="btn btn-ghost" style="padding:3px 8px;font-size:12px" onclick="refreshMcpServer('${s.id}')">Refresh</button>
          <button class="btn btn-ghost" style="padding:3px 8px;font-size:12px" onclick="editMcpServer('${s.id}')">Edit</button>
          <button class="btn btn-ghost" style="padding:3px 8px;font-size:12px" onclick="toggleMcpServer('${s.id}')">${s.enabled ? "Disable" : "Enable"}</button>
          <button class="btn btn-danger" style="padding:3px 8px;font-size:12px" onclick="deleteMcpServer('${s.id}', '${esc(s.name)}')">Delete</button>
        </td>`;
      tbody.appendChild(tr);
    }
  } catch (e) {
    tbody.innerHTML = `<tr><td colspan="6" style="color:var(--red)">${esc(String(e))}</td></tr>`;
  }
}

function showMcpModal(server = null) {
  document.getElementById("mcp-modal-title").textContent = server ? "Edit MCP Server" : "Add MCP Server";
  document.getElementById("mcp-modal-id").value = server ? server.id : "";
  document.getElementById("mcp-name").value = server ? server.name : "";
  document.getElementById("mcp-url").value = server ? server.url : "";
  document.getElementById("mcp-transport").value = server ? server.transport : "sse";
  document.getElementById("mcp-api-key").value = "";  // never pre-fill secrets
  document.getElementById("mcp-headers").value = "";
  document.getElementById("mcp-enabled").checked = server ? server.enabled : true;
  document.getElementById("mcp-modal").classList.remove("hidden");
}

function closeMcpModal() {
  document.getElementById("mcp-modal").classList.add("hidden");
}

async function editMcpServer(id) {
  const r = await fetch(`/api/mcp-servers/${id}`);
  const server = await r.json();
  showMcpModal(server);
}

async function saveMcpServer() {
  const id = document.getElementById("mcp-modal-id").value;
  const headersRaw = document.getElementById("mcp-headers").value.trim();
  let headers = null;
  if (headersRaw) {
    try { headers = JSON.parse(headersRaw); } catch { alert("Extra headers must be valid JSON"); return; }
  }
  const body = {
    name: document.getElementById("mcp-name").value.trim(),
    url: document.getElementById("mcp-url").value.trim(),
    transport: document.getElementById("mcp-transport").value,
    api_key: document.getElementById("mcp-api-key").value,
    headers,
    enabled: document.getElementById("mcp-enabled").checked,
  };
  if (!body.name || !body.url) { alert("Name and URL are required"); return; }
  const url = id ? `/api/mcp-servers/${id}` : "/api/mcp-servers";
  const method = id ? "PUT" : "POST";
  try {
    const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
    if (r.ok) {
      closeMcpModal();
      showFlash("Saved — discovering tools in background…");
      setTimeout(loadMcpServers, 1500);
    } else {
      const d = await r.json().catch(() => ({}));
      alert(d.detail || `Error saving MCP server (HTTP ${r.status})`);
    }
  } catch (e) {
    alert(`Network error: ${e.message}`);
  }
}

async function deleteMcpServer(id, name) {
  if (!confirm(`Delete MCP server "${name}"?`)) return;
  const r = await fetch(`/api/mcp-servers/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted ✓"); loadMcpServers(); }
}

async function toggleMcpServer(id) {
  await fetch(`/api/mcp-servers/${id}/toggle`, { method: "POST" });
  loadMcpServers();
}

async function refreshMcpServer(id) {
  const countEl = document.getElementById(`mcp-tool-count-${id}`);
  if (countEl) countEl.textContent = "…";
  const r = await fetch(`/api/mcp-servers/${id}/refresh`, { method: "POST" });
  if (r.ok) {
    const d = await r.json();
    if (countEl) countEl.textContent = d.tool_count;
    showFlash(`Refreshed — ${d.tool_count} tool(s) found ✓`);
  } else {
    if (countEl) countEl.textContent = "error";
    showFlash("Refresh failed");
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   SECURITY SETTINGS
══════════════════════════════════════════════════════════════════════════ */

// ── Branding ──────────────────────────────────────────────────────────────────

async function loadBrandingSettings() {
  try {
    const r = await fetch("/api/settings/branding");
    const d = await r.json();
    const nameInput = document.getElementById("brand-name-input");
    if (nameInput) nameInput.value = d.brand_name || "";
    _updateLogoPreview(d.has_custom_logo, d.logo_filename);
  } catch { /* ignore */ }
}

function _updateLogoPreview(hasCustom, filename) {
  const img = document.getElementById("brand-logo-preview");
  if (!img) return;
  img.src = hasCustom && filename ? `/static/${filename}?t=${Date.now()}` : "/static/logo.png";
  const resetBtn = document.getElementById("brand-logo-reset-btn");
  if (resetBtn) resetBtn.style.display = hasCustom ? "" : "none";
}

async function saveBrandingName(e) {
  e.preventDefault();
  const name = document.getElementById("brand-name-input")?.value.trim() || "";
  const r = await fetch("/api/settings/branding", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ brand_name: name }),
  });
  if (r.ok) {
    showFlash("Branding saved — reload to see sidebar update");
  } else {
    showFlash("Save failed");
  }
}

async function uploadBrandLogo() {
  const fileInput = document.getElementById("brand-logo-file");
  if (!fileInput?.files?.length) return;
  const fd = new FormData();
  fd.append("file", fileInput.files[0]);
  const r = await fetch("/api/settings/branding/logo/upload", { method: "POST", body: fd });
  if (r.ok) {
    const d = await r.json();
    _updateLogoPreview(true, d.filename);
    showFlash("Logo uploaded — reload to see sidebar update");
  } else {
    const d = await r.json().catch(() => ({}));
    showFlash("Upload failed: " + (d.detail || "unknown error"));
  }
  fileInput.value = "";
}

async function resetBrandLogo() {
  const r = await fetch("/api/settings/branding/logo", { method: "DELETE" });
  if (r.ok) {
    _updateLogoPreview(false);
    showFlash("Logo reset — reload to see sidebar update");
  }
}

async function loadSecuritySettings() {
  const form = document.getElementById("security-form");
  if (!form) return;
  try {
    const r = await fetch("/api/settings/security");
    const data = await r.json();
    const setChk = (id, val) => { const el = document.getElementById(id); if (el) el.checked = !!val; };
    const setNum = (id, val) => { const el = document.getElementById(id); if (el) el.value = val; };
    setChk("sec-sanitize-enhanced",   data.sanitize_enhanced);
    setChk("sec-truncation-enabled",  data.truncation_enabled);
    setChk("sec-output-validation",   data.output_validation_enabled);
    setChk("sec-canary-enabled",      data.canary_enabled);
    setNum("sec-max-web-chars",       data.max_web_chars);
    setNum("sec-max-email-chars",     data.max_email_chars);
    setNum("sec-max-file-chars",      data.max_file_chars);
    setNum("sec-max-subject-chars",   data.max_subject_chars);
    setChk("sec-llm-screen-enabled",  data.llm_screen_enabled);
    const mdEl = document.getElementById("sec-llm-screen-model");
    if (mdEl && data.llm_screen_model) mdEl.value = data.llm_screen_model;
    const modeEl = document.getElementById("sec-llm-screen-mode");
    if (modeEl) modeEl.value = data.llm_screen_block ? "block" : "flag";
    const extEl = document.getElementById("sec-upload-extensions");
    if (extEl && data.allowed_extensions) extEl.value = data.allowed_extensions.join(", ");
    setNum("sec-upload-max-mb",    data.max_file_size_mb);
    setNum("sec-upload-max-files", data.max_files);
    _uploadPolicy = { allowed_extensions: data.allowed_extensions, max_file_size_mb: data.max_file_size_mb, max_files: data.max_files };
  } catch { /* ignore */ }

  form.addEventListener("submit", async e => {
    e.preventDefault();
    const getChk = id => { const el = document.getElementById(id); return el ? el.checked : null; };
    const getNum = id => { const el = document.getElementById(id); return el ? parseInt(el.value, 10) : null; };
    const extRaw = (document.getElementById("sec-upload-extensions") || {}).value || "";
    const extList = extRaw.split(/[\s,]+/).map(s => s.replace(/^\./, "").toLowerCase()).filter(Boolean);
    const body = {
      sanitize_enhanced:          getChk("sec-sanitize-enhanced"),
      truncation_enabled:         getChk("sec-truncation-enabled"),
      output_validation_enabled:  getChk("sec-output-validation"),
      canary_enabled:             getChk("sec-canary-enabled"),
      max_web_chars:              getNum("sec-max-web-chars"),
      max_email_chars:            getNum("sec-max-email-chars"),
      max_file_chars:             getNum("sec-max-file-chars"),
      max_subject_chars:          getNum("sec-max-subject-chars"),
      llm_screen_enabled:         getChk("sec-llm-screen-enabled"),
      llm_screen_model:           (() => { const el = document.getElementById("sec-llm-screen-model"); return el ? el.value : null; })(),
      llm_screen_block:           (() => { const el = document.getElementById("sec-llm-screen-mode"); return el ? el.value === "block" : null; })(),
      upload_allowed_extensions:  extList.length ? extList : null,
      upload_max_file_size_mb:    getNum("sec-upload-max-mb"),
      upload_max_files:           getNum("sec-upload-max-files"),
    };
    for (const k of Object.keys(body)) {
      if (body[k] === null || (typeof body[k] === "number" && isNaN(body[k]))) delete body[k];
    }
    const r = await fetch("/api/settings/security", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body),
    });
    if (r.ok) {
      showFlash("Security settings saved ✓");
      _uploadPolicy = null; // bust cache so files page picks up new policy
    } else {
      showFlash("Error saving security settings");
    }
  });
}

async function loadLockouts() {
  const container = document.getElementById("lockout-list");
  if (!container) return;
  try {
    const r = await fetch("/api/settings/login-lockouts");
    const list = await r.json();
    if (!list.length) {
      container.innerHTML = "<span style='color:var(--text-dim);font-size:13px'>No locked IPs.</span>";
      return;
    }
    const now = Date.now() / 1000;
    container.innerHTML = list.map(item => {
      const badge = item.type === "permanent"
        ? "<span style='background:var(--danger,#dc3c3c);color:#fff;border-radius:3px;padding:1px 6px;font-size:11px'>PERMANENT</span>"
        : "<span style='background:var(--yellow,#e0a632);color:#000;border-radius:3px;padding:1px 6px;font-size:11px'>30 min</span>";
      const since = item.locked_at ? formatDate(new Date(item.locked_at * 1000).toISOString()) : "—";
      const until = item.locked_until ? formatDate(new Date(item.locked_until * 1000).toISOString()) : "—";
      return `<div style="display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--border);font-size:13px">
        <code style="flex:1">${item.ip}</code>
        ${badge}
        <span style="color:var(--text-dim)">since ${since}</span>
        ${item.type === "temporary" ? `<span style="color:var(--text-dim)">until ${until}</span>` : ""}
        <button class="btn btn-ghost btn-small" style="color:var(--accent)" onclick="unlockIp(${JSON.stringify(item.ip)})">Unlock</button>
      </div>`;
    }).join("");
  } catch(e) {
    container.innerHTML = "<span style='color:var(--danger)'>Failed to load lockouts.</span>";
  }
}

async function unlockIp(ip) {
  const r = await fetch("/api/settings/login-lockouts/" + encodeURIComponent(ip), { method: "DELETE" });
  if (r.ok) { showFlash("Unlocked " + ip); loadLockouts(); }
  else showFlash("Failed to unlock " + ip);
}

async function unlockAllIps() {
  if (!confirm("Unlock all locked IPs?")) return;
  const r = await fetch("/api/settings/login-lockouts", { method: "DELETE" });
  if (r.ok) { const d = await r.json(); showFlash("Unlocked " + d.unlocked + " IP(s)"); loadLockouts(); }
  else showFlash("Failed to unlock all");
}

/* ══════════════════════════════════════════════════════════════════════════
   INBOX SETTINGS
══════════════════════════════════════════════════════════════════════════ */

async function loadInboxStatus() {
  const badge = document.getElementById("inbox-status-badge");
  const btn   = document.getElementById("inbox-action-btn");
  if (!badge) return;
  try {
    const r = await fetch("/api/inbox/status");
    const raw = await r.json();
    const d = raw.global || raw;  // route wraps in {global: {...}, all: [...]}
    if (!d.configured) {
      badge.textContent = "Not configured";
      badge.style.cssText = "font-size:12px;padding:2px 10px;border-radius:20px;background:var(--bg2);color:var(--text-dim)";
      if (btn) { btn.textContent = "Reconnect"; btn.onclick = reconnectInbox; }
    } else if (d.connected) {
      badge.textContent = "Connected";
      badge.style.cssText = "font-size:12px;padding:2px 10px;border-radius:20px;background:rgba(80,200,120,0.15);color:var(--green)";
      if (btn) { btn.textContent = "Disconnect"; btn.onclick = disconnectInbox; }
    } else {
      badge.textContent = d.error ? `Error: ${d.error}` : "Disconnected";
      badge.style.cssText = "font-size:12px;padding:2px 10px;border-radius:20px;background:rgba(220,80,80,0.15);color:var(--danger,#e05)";
      if (btn) { btn.textContent = "Reconnect"; btn.onclick = reconnectInbox; }
    }
  } catch { /* ignore */ }
}

async function reconnectInbox() {
  await fetch("/api/inbox/reconnect", { method: "POST" });
  showFlash("Reconnecting…");
  setTimeout(loadInboxStatus, 2000);
}

async function disconnectInbox() {
  await fetch("/api/inbox/disconnect", { method: "POST" });
  showFlash("Disconnected");
  setTimeout(loadInboxStatus, 500);
}

function initInboxForm() {
  const form = document.getElementById("inbox-config-form");
  if (!form) return;

  // Pre-fill non-sensitive fields
  const nonSecret = [
    ["inbox:imap_host",    "inbox-imap-host"],
    ["inbox:imap_username","inbox-imap-user"],
    ["inbox:smtp_host",    "inbox-smtp-host"],
    ["inbox:smtp_port",    "inbox-smtp-port"],
    ["inbox:smtp_username","inbox-smtp-user"],
  ];
  nonSecret.forEach(async ([key, elId]) => {
    const el = document.getElementById(elId);
    if (!el) return;
    try {
      const r = await fetch(`/api/credentials/${encodeURIComponent(key)}`);
      if (r.ok) { const d = await r.json(); el.value = d.value; }
    } catch { /* leave blank */ }
  });

  form.addEventListener("submit", async e => {
    e.preventDefault();
    const fields = [
      ["inbox:imap_host",     document.getElementById("inbox-imap-host").value.trim()],
      ["inbox:imap_username", document.getElementById("inbox-imap-user").value.trim()],
      ["inbox:imap_password", document.getElementById("inbox-imap-pass").value],
      ["inbox:smtp_host",     document.getElementById("inbox-smtp-host").value.trim()],
      ["inbox:smtp_port",     document.getElementById("inbox-smtp-port").value.trim()],
      ["inbox:smtp_username", document.getElementById("inbox-smtp-user").value.trim()],
      ["inbox:smtp_password", document.getElementById("inbox-smtp-pass").value],
    ];
    for (const [key, value] of fields) {
      if (!value) continue;   // skip blanks — don't overwrite existing secrets
      await fetch("/api/credentials", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ key, value }),
      });
    }
    await fetch("/api/inbox/reconnect", { method: "POST" });
    showFlash("Saved & reconnecting ✓");
    setTimeout(loadInboxStatus, 2000);
  });
}

async function loadInboxTriggers() {
  const tbody = document.getElementById("inbox-trigger-list");
  if (!tbody) return;
  const r = await fetch("/api/inbox-triggers");
  const triggers = await r.json();
  tbody.innerHTML = "";
  if (!triggers.length) {
    tbody.innerHTML = "<tr><td colspan='5' style='text-align:center;color:var(--text-dim)'>No trigger rules yet</td></tr>";
    return;
  }
  for (const t of triggers) {
    const tr = document.createElement("tr");
    const statusLabel = t.enabled
      ? "<span style='color:var(--green);font-size:12px'>Enabled</span>"
      : "<span style='color:var(--text-dim);font-size:12px'>Disabled</span>";
    tr.innerHTML = `
      <td><code>${esc(t.trigger_word)}</code></td>
      <td>${esc(t.agent_name || t.agent_id)}</td>
      <td style="color:var(--text-dim)">${esc(t.description || "—")}</td>
      <td>${statusLabel}</td>
      <td></td>
    `;
    const tdAct = tr.querySelector("td:last-child");
    const editBtn = document.createElement("button");
    editBtn.className = "btn btn-ghost";
    editBtn.style.cssText = "padding:4px 10px;font-size:12px;margin-right:6px";
    editBtn.textContent = "Edit";
    editBtn.addEventListener("click", () => openTriggerModal(t));

    const toggleBtn = document.createElement("button");
    toggleBtn.className = "btn btn-ghost";
    toggleBtn.style.cssText = "padding:4px 10px;font-size:12px;margin-right:6px";
    toggleBtn.textContent = t.enabled ? "Disable" : "Enable";
    toggleBtn.addEventListener("click", async () => {
      await fetch(`/api/inbox-triggers/${t.id}/toggle`, { method: "POST" });
      loadInboxTriggers();
    });

    const delBtn = document.createElement("button");
    delBtn.className = "btn btn-danger";
    delBtn.style.cssText = "padding:4px 10px;font-size:12px";
    delBtn.textContent = "Delete";
    delBtn.addEventListener("click", async () => {
      if (!confirm(`Delete trigger rule "${t.trigger_word}"?`)) return;
      await fetch(`/api/inbox-triggers/${t.id}`, { method: "DELETE" });
      loadInboxTriggers();
    });

    tdAct.appendChild(editBtn);
    tdAct.appendChild(toggleBtn);
    tdAct.appendChild(delBtn);
    tbody.appendChild(tr);
  }
}

async function openTriggerModal(trigger = null) {
  const modal   = document.getElementById("trigger-modal");
  const title   = document.getElementById("trigger-modal-title");
  const idInput = document.getElementById("trigger-modal-id");
  const wordEl  = document.getElementById("trigger-modal-word");
  const descEl  = document.getElementById("trigger-modal-desc");
  const enEl    = document.getElementById("trigger-modal-enabled");
  const agentSel = document.getElementById("trigger-modal-agent");

  title.textContent = trigger ? "Edit Trigger Rule" : "Add Trigger Rule";
  idInput.value  = trigger ? trigger.id : "";
  wordEl.value   = trigger ? trigger.trigger_word : "";
  descEl.value   = trigger ? (trigger.description || "") : "";
  enEl.checked   = trigger ? !!trigger.enabled : true;

  // Populate agent dropdown
  agentSel.innerHTML = "<option value=''>Loading…</option>";
  try {
    const r = await fetch("/api/agents");
    const agents = await r.json();
    agentSel.innerHTML = agents.length
      ? agents.map(a => `<option value="${esc(a.id)}">${esc(a.name)}</option>`).join("")
      : "<option value=''>No agents defined</option>";
    if (trigger) agentSel.value = trigger.agent_id;
  } catch {
    agentSel.innerHTML = "<option value=''>Error loading agents</option>";
  }

  modal.classList.remove("hidden");
}

function closeTriggerModal() {
  document.getElementById("trigger-modal").classList.add("hidden");
}

async function saveTrigger() {
  const id      = document.getElementById("trigger-modal-id").value;
  const word    = document.getElementById("trigger-modal-word").value.trim();
  const agentId = document.getElementById("trigger-modal-agent").value;
  const desc    = document.getElementById("trigger-modal-desc").value.trim();
  const enabled = document.getElementById("trigger-modal-enabled").checked;

  if (!word)    { alert("Please enter a trigger word."); return; }
  if (!agentId) { alert("Please select an agent."); return; }

  const body = { trigger_word: word, agent_id: agentId, description: desc, enabled };
  const url    = id ? `/api/inbox-triggers/${id}` : "/api/inbox-triggers";
  const method = id ? "PUT" : "POST";

  const r = await fetch(url, {
    method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (r.ok) {
    closeTriggerModal();
    showFlash("Saved ✓");
    loadInboxTriggers();
  } else {
    const d = await r.json().catch(() => ({}));
    alert(d.detail || "Error saving trigger rule");
  }
}

/* ── end inbox ─────────────────────────────────────────────────────────── */

/* ── Telegram ──────────────────────────────────────────────────────────── */

async function loadTelegramStatus() {
  const badge = document.getElementById("telegram-status-badge");
  const btn   = document.getElementById("telegram-action-btn");
  if (!badge) return;
  const r = await fetch("/api/telegram/status");
  if (!r.ok) return;
  const s = await r.json();
  if (!s.configured) {
    badge.textContent = "Not configured";
    badge.style.background = "var(--bg2)"; badge.style.color = "var(--text-dim)";
    if (btn) { btn.textContent = "Reconnect"; btn.onclick = reconnectTelegram; }
  } else if (s.running) {
    badge.textContent = "Connected";
    badge.style.background = "rgba(72,199,142,0.15)"; badge.style.color = "var(--green)";
    if (btn) { btn.textContent = "Disconnect"; btn.onclick = disconnectTelegram; }
  } else {
    badge.textContent = "Offline";
    badge.style.background = "rgba(255,100,100,0.12)"; badge.style.color = "#ff6b6b";
    if (btn) { btn.textContent = "Reconnect"; btn.onclick = reconnectTelegram; }
  }
}

async function reconnectTelegram() {
  await fetch("/api/telegram/reconnect", { method: "POST" });
  setTimeout(loadTelegramStatus, 2000);
}

async function disconnectTelegram() {
  await fetch("/api/telegram/disconnect", { method: "POST" });
  setTimeout(loadTelegramStatus, 500);
}

function initTelegramConfigForm() {
  const form = document.getElementById("telegram-config-form");
  if (!form) return;
  form.addEventListener("submit", async e => {
    e.preventDefault();
    const token = document.getElementById("telegram-bot-token").value.trim();
    if (token) {
      await fetch("/api/credentials", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ key: "telegram:bot_token", value: token, description: "Telegram Bot API token" }),
      });
    }
    await fetch("/api/telegram/reconnect", { method: "POST" });
    document.getElementById("telegram-bot-token").value = "";
    showFlash("Saved & reconnecting…");
    setTimeout(loadTelegramStatus, 2000);
  });
}

async function loadTelegramWhitelist() {
  const tbody = document.getElementById("telegram-whitelist-list");
  if (!tbody) return;
  const r = await fetch("/api/telegram-whitelist");
  const rows = await r.json();
  tbody.innerHTML = "";
  if (!rows.length) {
    tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No chat IDs whitelisted</td></tr>";
    return;
  }
  for (const row of rows) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><code>${esc(row.chat_id)}</code></td>
      <td>${esc(row.label || "")}</td>
      <td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
                  onclick="removeTelegramWhitelist('${esc(row.chat_id)}')">Remove</button></td>
    `;
    tbody.appendChild(tr);
  }
}

async function removeTelegramWhitelist(chatId) {
  if (!confirm(`Remove chat ID ${chatId} from whitelist?`)) return;
  await fetch(`/api/telegram-whitelist/${encodeURIComponent(chatId)}`, { method: "DELETE" });
  loadTelegramWhitelist();
}

async function loadTelegramTriggers() {
  const tbody = document.getElementById("telegram-trigger-list");
  if (!tbody) return;
  const r = await fetch("/api/telegram-triggers");
  const rows = await r.json();
  tbody.innerHTML = "";
  if (!rows.length) {
    tbody.innerHTML = "<tr><td colspan='5' style='text-align:center;color:var(--text-dim)'>No trigger rules</td></tr>";
    return;
  }
  for (const t of rows) {
    const tr = document.createElement("tr");
    const statusLabel = t.enabled ? "Enabled" : "Disabled";
    const statusColor = t.enabled ? "var(--green)" : "var(--text-dim)";
    const editBtn   = document.createElement("button");
    const toggleBtn = document.createElement("button");
    const deleteBtn = document.createElement("button");
    editBtn.className   = "btn btn-ghost";   editBtn.style.cssText   = "padding:4px 10px;font-size:12px;margin-right:4px";
    toggleBtn.className = "btn btn-ghost";   toggleBtn.style.cssText = "padding:4px 10px;font-size:12px;margin-right:4px";
    deleteBtn.className = "btn btn-danger";  deleteBtn.style.cssText = "padding:4px 10px;font-size:12px";
    editBtn.textContent   = "Edit";
    toggleBtn.textContent = t.enabled ? "Disable" : "Enable";
    deleteBtn.textContent = "Delete";
    editBtn.addEventListener("click",   () => openTgTriggerModal(t));
    toggleBtn.addEventListener("click", async () => {
      await fetch(`/api/telegram-triggers/${t.id}/toggle`, { method: "POST" });
      loadTelegramTriggers();
    });
    deleteBtn.addEventListener("click", async () => {
      if (!confirm("Delete this trigger rule?")) return;
      await fetch(`/api/telegram-triggers/${t.id}`, { method: "DELETE" });
      loadTelegramTriggers();
    });
    tr.innerHTML = `
      <td><code>${esc(t.trigger_word)}</code></td>
      <td>${esc(t.agent_name || t.agent_id)}</td>
      <td style="color:var(--text-dim)">${esc(t.description || "")}</td>
      <td style="color:${statusColor}">${statusLabel}</td>
      <td></td>
    `;
    const actionCell = tr.querySelector("td:last-child");
    actionCell.appendChild(editBtn);
    actionCell.appendChild(toggleBtn);
    actionCell.appendChild(deleteBtn);
    tbody.appendChild(tr);
  }
}

async function openTgTriggerModal(trigger = null) {
  const r = await fetch("/api/agents");
  const agents = await r.json();
  const sel = document.getElementById("tg-trigger-modal-agent");
  sel.innerHTML = '<option value="">— select agent —</option>';
  for (const a of agents) {
    const opt = document.createElement("option");
    opt.value = a.id; opt.textContent = a.name;
    if (trigger && trigger.agent_id === a.id) opt.selected = true;
    sel.appendChild(opt);
  }
  document.getElementById("tg-trigger-modal-id").value      = trigger ? trigger.id : "";
  document.getElementById("tg-trigger-modal-word").value    = trigger ? trigger.trigger_word : "";
  document.getElementById("tg-trigger-modal-desc").value    = trigger ? trigger.description : "";
  document.getElementById("tg-trigger-modal-enabled").checked = trigger ? !!trigger.enabled : true;
  document.getElementById("tg-trigger-modal-title").textContent = trigger ? "Edit Telegram Trigger" : "Add Telegram Trigger";
  document.getElementById("tg-trigger-modal").classList.remove("hidden");
}

function closeTgTriggerModal() {
  document.getElementById("tg-trigger-modal").classList.add("hidden");
}

async function saveTgTrigger() {
  const id      = document.getElementById("tg-trigger-modal-id").value;
  const word    = document.getElementById("tg-trigger-modal-word").value.trim();
  const agentId = document.getElementById("tg-trigger-modal-agent").value;
  const desc    = document.getElementById("tg-trigger-modal-desc").value.trim();
  const enabled = document.getElementById("tg-trigger-modal-enabled").checked;

  if (!word)    { alert("Please enter a trigger word."); return; }
  if (!agentId) { alert("Please select an agent."); return; }

  const body = { trigger_word: word, agent_id: agentId, description: desc, enabled };
  const url    = id ? `/api/telegram-triggers/${id}` : "/api/telegram-triggers";
  const method = id ? "PUT" : "POST";

  const r = await fetch(url, {
    method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (r.ok) {
    closeTgTriggerModal();
    showFlash("Saved ✓");
    loadTelegramTriggers();
  } else {
    const d = await r.json().catch(() => ({}));
    alert(d.detail || "Error saving trigger rule");
  }
}

async function loadTgDefaultAgent() {
  const sel = document.getElementById("tg-default-agent");
  if (!sel) return;
  const [agentsR, defaultR] = await Promise.all([
    fetch("/api/agents"),
    fetch("/api/telegram/default-agent"),
  ]);
  const agents = await agentsR.json();
  const { agent_id: current } = await defaultR.json();
  sel.innerHTML = '<option value="">— none (drop unmatched messages) —</option>';
  for (const a of agents) {
    const opt = document.createElement("option");
    opt.value = a.id;
    opt.textContent = a.name;
    if (a.id === current) opt.selected = true;
    sel.appendChild(opt);
  }
}

async function saveTgDefaultAgent() {
  const agent_id = document.getElementById("tg-default-agent").value;
  const r = await fetch("/api/telegram/default-agent", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ agent_id }),
  });
  if (r.ok) showFlash(agent_id ? "Default agent saved ✓" : "Default agent cleared ✓");
  else showFlash("Error saving default agent");
}

/* ── end telegram ──────────────────────────────────────────────────────── */

/* ── System prompt (SOUL.md / USER.md) ─────────────────────────────────── */

async function loadSystemPrompts() {
  const [soulR, userR] = await Promise.all([
    fetch("/api/system-prompt/soul"),
    fetch("/api/system-prompt/user"),
  ]);
  const { content: soul } = await soulR.json();
  const { content: user } = await userR.json();
  const soulEl = document.getElementById("sp-soul");
  const userEl = document.getElementById("sp-user");
  if (soulEl) soulEl.value = soul;
  if (userEl) userEl.value = user;
}

async function saveSystemPrompt(key) {
  const el = document.getElementById(`sp-${key}`);
  if (!el) return;
  const r = await fetch(`/api/system-prompt/${key}`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: el.value }),
  });
  if (r.ok) showFlash("Saved ✓");
  else showFlash("Error saving");
}

/* ── end system prompt ──────────────────────────────────────────────────── */

async function loadEmailWhitelist() {
  const r = await fetch("/api/email-whitelist");
  const entries = await r.json();
  const tbody = document.getElementById("email-whitelist-list");
  if (!tbody) return;
  tbody.innerHTML = "";
  if (!entries.length) {
    tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No addresses whitelisted</td></tr>";
    return;
  }
  for (const e of entries) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><code>${esc(e.email)}</code></td>
      <td>${e.daily_limit === 0 ? "Unlimited" : esc(String(e.daily_limit))}</td>
      <td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
                  onclick="deleteEmail('${esc(e.email)}')">Remove</button></td>
    `;
    tbody.appendChild(tr);
  }
}

async function addEmail() {
  const email = document.getElementById("ew-email").value.trim();
  const limit = parseInt(document.getElementById("ew-limit").value || "0", 10);
  if (!email) return;
  const r = await fetch("/api/email-whitelist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, daily_limit: isNaN(limit) ? 0 : limit }),
  });
  if (r.ok) {
    document.getElementById("ew-email").value = "";
    document.getElementById("ew-limit").value = "0";
    showFlash("Added ✓");
    await loadEmailWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error adding email");
  }
}

async function deleteEmail(email) {
  if (!confirm(`Remove "${email}" from whitelist?`)) return;
  const r = await fetch(`/api/email-whitelist/${encodeURIComponent(email)}`, { method: "DELETE" });
  if (r.ok) {
    showFlash("Removed ✓");
    await loadEmailWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error");
  }
}

/* ── Filesystem whitelist ─────────────────────────────────────────────────── */

async function loadFilesystemWhitelist() {
  const r = await fetch("/api/filesystem-whitelist");
  const entries = await r.json();
  const tbody = document.getElementById("fs-whitelist-list");
  if (!tbody) return;
  tbody.innerHTML = "";
  if (!entries.length) {
    tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No directories configured</td></tr>";
    return;
  }
  for (const e of entries) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><code style="font-size:12px">${esc(e.path)}</code></td>
      <td style="color:var(--text-dim);font-size:12px">${esc(e.note || "")}</td>
      <td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
                  onclick="deleteFilesystemPath('${esc(e.path)}')">Remove</button></td>
    `;
    tbody.appendChild(tr);
  }
}

async function addFilesystemPath() {
  const path = document.getElementById("fs-path").value.trim();
  const note = document.getElementById("fs-note").value.trim();
  if (!path) return;
  const r = await fetch("/api/filesystem-whitelist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ path, note }),
  });
  if (r.ok) {
    document.getElementById("fs-path").value = "";
    document.getElementById("fs-note").value = "";
    showFlash("Added ✓");
    await loadFilesystemWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error adding path");
  }
}

async function deleteFilesystemPath(path) {
  if (!confirm(`Remove "${path}" from filesystem sandbox?`)) return;
  const r = await fetch(`/api/filesystem-whitelist/${encodeURIComponent(path)}`, { method: "DELETE" });
  if (r.ok) {
    showFlash("Removed ✓");
    await loadFilesystemWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error");
  }
}

/* ── Filesystem browser modal ─────────────────────────────────────────────── */

let _fsBrowserPath = "/";

async function openFsBrowser() {
  document.getElementById("fs-browser-modal").classList.remove("hidden");
  await navigateFsBrowser(_fsBrowserPath);
}

function closeFsBrowser() {
  document.getElementById("fs-browser-modal").classList.add("hidden");
}

async function navigateFsBrowser(path) {
  _fsBrowserPath = path;
  document.getElementById("fs-breadcrumb").textContent = path;

  const list = document.getElementById("fs-browser-list");
  list.innerHTML = `<div style="padding:12px;color:var(--text-dim);font-size:13px">Loading…</div>`;

  const r = await fetch(`/api/filesystem-browser?path=${encodeURIComponent(path)}`);
  if (!r.ok) {
    list.innerHTML = `<div style="padding:12px;color:var(--text-dim);font-size:13px">Cannot read directory</div>`;
    return;
  }
  const data = await r.json();
  list.innerHTML = "";

  // Up button
  if (data.parent) {
    const up = document.createElement("div");
    up.style.cssText = "padding:8px 14px;cursor:pointer;font-size:13px;border-bottom:1px solid var(--border);color:var(--text-dim)";
    up.textContent = "↑ ..";
    up.onclick = () => navigateFsBrowser(data.parent);
    up.onmouseenter = () => up.style.background = "var(--bg2)";
    up.onmouseleave = () => up.style.background = "";
    list.appendChild(up);
  }

  if (!data.entries.length) {
    const empty = document.createElement("div");
    empty.style.cssText = "padding:12px 14px;font-size:13px;color:var(--text-dim)";
    empty.textContent = "No subdirectories";
    list.appendChild(empty);
    return;
  }

  for (const entry of data.entries) {
    const row = document.createElement("div");
    row.style.cssText = "padding:8px 14px;cursor:pointer;font-size:13px;display:flex;align-items:center;gap:8px";
    row.innerHTML = `<span style="color:var(--text-dim)">📁</span><span>${esc(entry.name)}</span>`;
    row.onclick = () => navigateFsBrowser(entry.path);
    row.onmouseenter = () => row.style.background = "var(--bg2)";
    row.onmouseleave = () => row.style.background = "";
    list.appendChild(row);
  }
}

function selectFsPath() {
  document.getElementById("fs-path").value = _fsBrowserPath;
  closeFsBrowser();
}

async function loadWebWhitelist() {
  const r = await fetch("/api/web-whitelist");
  const entries = await r.json();
  const tbody = document.getElementById("web-whitelist-list");
  if (!tbody) return;
  tbody.innerHTML = "";
  if (!entries.length) {
    tbody.innerHTML = "<tr><td colspan='3' style='text-align:center;color:var(--text-dim)'>No domains whitelisted</td></tr>";
    return;
  }
  for (const e of entries) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td><code>${esc(e.domain)}</code></td>
      <td style="color:var(--text-dim);font-size:12px">${esc(e.note || "")}</td>
      <td><button class="btn btn-danger" style="padding:4px 10px;font-size:12px"
                  onclick="deleteWebDomain('${esc(e.domain)}')">Remove</button></td>
    `;
    tbody.appendChild(tr);
  }
}

async function addWebDomain() {
  const domain = document.getElementById("ww-domain").value.trim();
  const note = document.getElementById("ww-note").value.trim();
  if (!domain) return;
  const r = await fetch("/api/web-whitelist", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ domain, note }),
  });
  if (r.ok) {
    document.getElementById("ww-domain").value = "";
    document.getElementById("ww-note").value = "";
    showFlash("Added ✓");
    await loadWebWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error adding domain");
  }
}

async function deleteWebDomain(domain) {
  if (!confirm(`Remove "${domain}" from whitelist?`)) return;
  const r = await fetch(`/api/web-whitelist/${encodeURIComponent(domain)}`, { method: "DELETE" });
  if (r.ok) {
    showFlash("Removed ✓");
    await loadWebWhitelist();
  } else {
    const d = await r.json();
    alert(d.detail || "Error");
  }
}

let _flashTimer = null;
function showFlash(msg) {
  const el = document.getElementById("flash-msg");
  if (!el) return;
  if (_flashTimer) { clearTimeout(_flashTimer); _flashTimer = null; }
  el.textContent = msg;
  el.style.opacity = "1";
  _flashTimer = setTimeout(() => { el.style.opacity = "0"; _flashTimer = null; }, 3000);
}

/* ══════════════════════════════════════════════════════════════════════════
   TASKS PAGE
══════════════════════════════════════════════════════════════════════════ */

let _availableModels = [];   // populated from /api/models on pages that need it
/* ── Cron preview ────────────────────────────────────────────────────────── */

function cronLabel(expr) {
  if (!expr) return "";
  const parts = expr.trim().split(/\s+/);
  if (parts.length !== 5) return "";
  const [min, hr, dom, mon, dow] = parts;
  const days = ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];
  const pad = n => String(n).padStart(2, "0");
  const isNum = s => /^\d+$/.test(s);
  // Interval patterns — must come before the "daily" catch-all
  if (min.startsWith("*/") && hr === "*" && dom === "*" && mon === "*" && dow === "*")
    return `Every ${min.slice(2)} min`;
  if (hr.startsWith("*/") && isNum(min) && dom === "*" && mon === "*" && dow === "*")
    return `Every ${hr.slice(2)} hr`;
  // Fixed-time patterns — only when hr and min are plain numbers
  if (dom === "*" && mon === "*" && dow === "*" && isNum(hr) && isNum(min))
    return `Daily at ${pad(hr)}:${pad(min)}`;
  if (dom === "*" && mon === "*" && /^\d$/.test(dow) && isNum(hr) && isNum(min))
    return `${days[+dow]} at ${pad(hr)}:${pad(min)}`;
  return "";
}


/* ══════════════════════════════════════════════════════════════════════════
   AGENTS PAGE
══════════════════════════════════════════════════════════════════════════ */

function initAgents() {
  if (!document.getElementById("agents-container")) return;
  _loadAvailableModels().then(() => {
    loadAgents();
    loadAgentRuns();
  });
}

async function _loadAvailableModels() {
  try {
    const r = await fetch("/api/models");
    const data = await r.json();
    _availableModels = data.models || [];
  } catch (e) {
    _availableModels = [];
  }
}

function switchTab(tab) {
  document.getElementById("pane-agents").style.display = tab === "agents" ? "" : "none";
  document.getElementById("pane-status").style.display = tab === "status" ? "" : "none";
  document.getElementById("tab-agents").classList.toggle("active", tab === "agents");
  document.getElementById("tab-status").classList.toggle("active", tab === "status");
}

async function loadAgents() {
  const r = await fetch("/api/agents");
  const agents = await r.json();
  const tbody = document.querySelector("#agents-table tbody");
  tbody.innerHTML = "";

  if (!agents.length) {
    tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-dim)">
      No agents yet — click <strong>+ New Agent</strong> to create one.</td></tr>`;
    return;
  }

  for (const a of agents) {
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>
        <a href="/agents/${a.id}" style="color:var(--accent);text-decoration:none"
           onclick="event.preventDefault();navigateTo('/agents/${a.id}')">${esc(a.name)}</a>
        ${a.description ? `<div style="font-size:11px;color:var(--text-dim)">${esc(a.description)}</div>` : ""}
        ${a.parent_agent_id ? `<div style="font-size:10px;color:var(--text-dim)">sub-agent</div>` : ""}
      </td>
      <td><code style="font-size:11px">${esc(_parseModel(a.model).label || a.model)}</code>
        ${_parseModel(a.model).provider ? `<div style="font-size:10px;color:var(--text-dim)">${esc(_parseModel(a.model).provider)}</div>` : ""}
      </td>
      <td><code>${esc(a.schedule || "—")}</code>
        <div style="font-size:11px;color:var(--text-dim)">${cronLabel(a.schedule)}</div>
      </td>
      <td style="text-align:center">${a.can_create_subagents ? "✓" : "—"}</td>
      <td>
        <button class="btn btn-ghost" style="padding:3px 10px;font-size:12px"
                onclick="toggleAgent('${a.id}', ${a.enabled})">
          ${a.enabled ? "Enabled" : "Disabled"}
        </button>
      </td>
      <td style="font-size:12px;color:var(--text-dim)">${a.last_run_at ? formatDateShort(a.last_run_at) : "—"}</td>
      <td>
        <div style="display:flex;gap:6px;flex-wrap:wrap">
          <button class="btn btn-ghost" style="padding:3px 10px;font-size:12px"
                  onclick="editAgent('${a.id}')">Edit</button>
          <button class="btn btn-primary" style="padding:3px 10px;font-size:12px"
                  onclick="runAgent('${a.id}')">▶ Run</button>
          <button class="btn btn-danger" style="padding:3px 10px;font-size:12px"
                  onclick="deleteAgent('${a.id}', '${esc(a.name)}')">Delete</button>
        </div>
      </td>
    `;
    tbody.appendChild(tr);
  }
}

async function loadAgentRuns() {
  const since = document.getElementById("status-since")?.value || "7d";
  const r = await fetch(`/api/agent-runs?since=${since}`);
  const runs = await r.json();
  const tbody = document.querySelector("#runs-table tbody");
  tbody.innerHTML = "";

  if (!runs.length) {
    tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-dim)">No runs in this period</td></tr>`;
    document.getElementById("runs-totals").textContent = "";
    return;
  }

  let totalIn = 0, totalOut = 0;
  for (const run of runs) {
    totalIn += run.input_tokens || 0;
    totalOut += run.output_tokens || 0;
    const started = run.started_at ? new Date(run.started_at) : null;
    const ended   = run.ended_at   ? new Date(run.ended_at)   : null;
    const dur = (started && ended)
      ? Math.round((ended - started) / 1000) + "s"
      : run.status === "running" ? "running…" : "—";
    const statusClass = run.status === "success" ? "badge-green"
      : run.status === "error" ? "badge-red"
      : run.status === "stopped" ? "badge-red"
      : run.status === "running" ? "badge-blue" : "";
    const tr = document.createElement("tr");
    tr.innerHTML = `
      <td>
        <a href="/agents/${run.agent_id}" style="color:var(--accent);text-decoration:none"
           onclick="event.preventDefault();navigateTo('/agents/${run.agent_id}')">${esc(run.agent_name || run.agent_id.slice(0,8))}</a>
      </td>
      <td style="font-size:12px">${formatDate(run.started_at)}</td>
      <td style="font-size:12px">${esc(dur)}</td>
      <td>${statusClass ? `<span class="badge ${statusClass}">${esc(run.status)}</span>` : esc(run.status)}</td>
      <td style="font-size:12px;text-align:right">${(run.input_tokens || 0).toLocaleString()}</td>
      <td style="font-size:12px;text-align:right">${(run.output_tokens || 0).toLocaleString()}</td>
      <td>
        ${run.status === "running"
          ? `<button class="btn btn-danger" style="padding:3px 10px;font-size:12px"
                    onclick="stopRun('${run.id}')">■ Stop</button>`
          : "—"}
      </td>
    `;
    tbody.appendChild(tr);
  }
  document.getElementById("runs-totals").textContent =
    `Total: ${totalIn.toLocaleString()} input + ${totalOut.toLocaleString()} output tokens`;
}

/* ── Agent modal ─────────────────────────────────────────────────────────── */

function setPromptMode(mode) {
  document.getElementById("a-prompt-mode").value = mode;
  document.querySelectorAll(".pm-btn").forEach(b => b.classList.toggle("active", b.dataset.mode === mode));
  const hints = {
    combined:    "Agent prompt takes priority, followed by the standard system prompt.",
    system_only: "Standard system prompt only — agent prompt used as the task message.",
    agent_only:  "Agent prompt replaces the standard system prompt entirely.",
  };
  document.getElementById("a-pm-hint").textContent = hints[mode] || "";
}

async function showAgentModal(agent, template = null) {
  const prefill = template || agent;
  document.getElementById("agent-modal-title").textContent = template ? `New Agent from Template` : (agent ? "Edit Agent" : "New Agent");
  document.getElementById("a-id").value = agent ? agent.id : "";
  document.getElementById("a-name").value = prefill ? (prefill.name || "") : "";
  document.getElementById("a-desc").value = prefill ? (prefill.description || "") : "";
  document.getElementById("a-prompt").value = prefill ? (prefill.prompt || "") : "";
  document.getElementById("a-schedule").value = prefill ? (prefill.schedule || prefill.suggested_schedule || "") : "";
  document.getElementById("a-subagents").checked = agent ? !!agent.can_create_subagents : false;
  document.getElementById("a-enabled").checked = agent ? !!agent.enabled : true;
  setPromptMode(prefill?.prompt_mode || "combined");

  const mtcEl = document.getElementById("a-max-tool-calls");
  if (mtcEl) mtcEl.value = (agent && agent.max_tool_calls) ? agent.max_tool_calls : "";
  updateAgentCronPreview(document.getElementById("a-schedule").value);
  updateMtcHint();

  const modelSel = document.getElementById("a-model");
  if (modelSel) {
    const defaultModel = prefill?.model || _availableModels[0] || "";
    _fillModelSelect(modelSel, _availableModels, defaultModel);
  }

  // Load tool list dynamically
  // allowed_tools may be a parsed array (from API) or a JSON string (from templates)
  const _raw = prefill ? (prefill.allowed_tools ?? prefill.suggested_tools ?? []) : [];
  const agentTools = Array.isArray(_raw) ? _raw : (typeof _raw === "string" ? JSON.parse(_raw) : []);
  await _renderToolCheckboxes(agentTools);

  document.getElementById("agent-modal").classList.remove("hidden");
}

async function _renderToolCheckboxes(checkedTools = []) {
  const container = document.getElementById("agent-tool-list");
  if (!container) return;
  container.innerHTML = `<span style="color:var(--text-dim);font-size:12px">Loading…</span>`;
  let tools = [];
  try {
    const r = await fetch("/api/tools");
    tools = await r.json();
  } catch { /* fallback to empty */ }
  if (tools.length === 0) {
    container.innerHTML = `<span style="color:var(--text-dim);font-size:12px">No tools available</span>`;
    return;
  }
  // Group: native tools first, then MCP servers (one checkbox per server)
  const native = tools.filter(t => !t.is_mcp);
  const mcp = tools.filter(t => t.is_mcp);

  // Collapse MCP tools into per-server entries
  const mcpServers = {};
  for (const t of mcp) {
    const serverSlug = t.name.split("__")[1];
    if (!mcpServers[serverSlug]) mcpServers[serverSlug] = { displayName: t.server_display_name || serverSlug, tools: [] };
    mcpServers[serverSlug].tools.push(t.name);
  }

  container.innerHTML = "";

  // Native tools
  for (const t of native) {
    const lbl = document.createElement("label");
    lbl.style.cssText = "display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px";
    const cb = document.createElement("input");
    cb.type = "checkbox";
    cb.className = "agent-tool-cb";
    cb.value = t.name;
    cb.checked = checkedTools.includes(t.name);
    lbl.appendChild(cb);
    lbl.appendChild(document.createTextNode(t.name));
    container.appendChild(lbl);
  }

  // MCP servers — one checkbox per server
  if (Object.keys(mcpServers).length) {
    const sep = document.createElement("div");
    sep.style.cssText = "width:100%;font-size:10px;color:var(--accent);text-transform:uppercase;letter-spacing:.05em;padding-top:4px";
    sep.textContent = "MCP Servers";
    container.appendChild(sep);

    for (const [serverSlug, { displayName, tools: toolNames }] of Object.entries(mcpServers)) {
      const serverKey = `mcp__${serverSlug}`;
      const isChecked = checkedTools.includes(serverKey) ||
                        toolNames.some(n => checkedTools.includes(n));
      const lbl = document.createElement("label");
      lbl.style.cssText = "display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px";
      const cb = document.createElement("input");
      cb.type = "checkbox";
      cb.className = "agent-tool-cb";
      cb.value = serverKey;
      cb.checked = isChecked;
      lbl.appendChild(cb);
      lbl.appendChild(document.createTextNode(displayName));
      container.appendChild(lbl);
    }
  }
}

function updateMtcHint() {
  const el = document.getElementById("a-max-tool-calls");
  const hint = document.getElementById("a-mtc-hint");
  if (!el || !hint) return;
  hint.textContent = el.value ? `(overrides system default)` : `(system default)`;
}

function closeAgentModal() {
  document.getElementById("agent-modal").classList.add("hidden");
}

async function editAgent(id) {
  const r = await fetch(`/api/agents/${id}`);
  const agent = await r.json();
  showAgentModal(agent);
}

async function saveAgent() {
  const id = document.getElementById("a-id").value;
  const mtcRaw = document.getElementById("a-max-tool-calls")?.value;
  const mtcVal = mtcRaw ? parseInt(mtcRaw, 10) : null;
  const body = {
    name: document.getElementById("a-name").value.trim(),
    description: document.getElementById("a-desc").value.trim(),
    prompt: document.getElementById("a-prompt").value.trim(),
    model: document.getElementById("a-model").value,
    schedule: document.getElementById("a-schedule").value.trim() || null,
    can_create_subagents: document.getElementById("a-subagents").checked,
    enabled: document.getElementById("a-enabled").checked,
    max_tool_calls: (mtcVal && mtcVal > 0) ? mtcVal : null,
    prompt_mode: document.getElementById("a-prompt-mode").value || "combined",
    allowed_tools: [...document.querySelectorAll(".agent-tool-cb:checked")].map(cb => cb.value),
  };
  const promptRequired = body.prompt_mode !== "system_only";
  if (!body.name || !body.model || (promptRequired && !body.prompt)) {
    alert(promptRequired ? "Name, prompt, and model are required." : "Name and model are required.");
    return;
  }
  const url = id ? `/api/agents/${id}` : "/api/agents";
  const method = id ? "PUT" : "POST";
  const r = await fetch(url, {
    method,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!r.ok) {
    const d = await r.json();
    alert("Error: " + (d.detail || r.statusText));
    return;
  }
  closeAgentModal();
  loadAgents();
}

async function toggleAgent(id, currentlyEnabled) {
  await fetch(`/api/agents/${id}/toggle`, { method: "POST" });
  loadAgents();
}

async function deleteAgent(id, name) {
  if (!confirm(`Delete agent "${name}"?`)) return;
  await fetch(`/api/agents/${id}`, { method: "DELETE" });
  loadAgents();
}

async function runAgent(id) {
  document.getElementById("agent-run-output").textContent = "Starting agent run…";
  document.getElementById("agent-run-modal").classList.remove("hidden");
  const r = await fetch(`/api/agents/${id}/run`, { method: "POST" });
  const data = await r.json();
  document.getElementById("agent-run-output").textContent =
    data.run ? `Run started: ${data.run.id}` : JSON.stringify(data, null, 2);
  loadAgents();
  loadAgentRuns();
}

function closeAgentRunModal() {
  document.getElementById("agent-run-modal").classList.add("hidden");
}

async function showTemplatesModal() {
  document.getElementById("templates-modal").classList.remove("hidden");
  const container = document.getElementById("templates-list");
  container.innerHTML = `<div style="color:var(--text-dim);font-size:13px">Loading…</div>`;
  const r = await fetch("/api/agent-templates");
  const templates = await r.json();
  const categoryColors = { productivity: "#6c8ef5", brain: "#b07aee", utility: "#4caf7d" };
  container.innerHTML = "";
  for (const t of templates) {
    const color = categoryColors[t.category] || "var(--accent)";
    const toolBadges = (t.suggested_tools || [])
      .map(tool => `<span class="badge" style="background:rgba(108,142,245,0.12);color:var(--accent);font-size:10px">${tool}</span>`)
      .join(" ");
    const card = document.createElement("div");
    card.style.cssText = "background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;display:flex;flex-direction:column;gap:8px;cursor:pointer";
    card.innerHTML = `
      <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px">
        <div style="font-weight:600;font-size:14px">${esc(t.name)}</div>
        <span style="font-size:10px;background:rgba(0,0,0,.25);color:${color};padding:2px 7px;border-radius:10px;white-space:nowrap">${esc(t.category)}</span>
      </div>
      <div style="font-size:12px;color:var(--text-dim);line-height:1.5">${esc(t.description)}</div>
      <div style="display:flex;flex-wrap:wrap;gap:4px">${toolBadges}</div>
      ${t.suggested_schedule ? `<div style="font-size:11px;color:var(--text-dim)">Schedule: <code>${t.suggested_schedule}</code></div>` : ""}
      <button class="btn btn-primary" style="margin-top:4px;font-size:12px" onclick="installTemplate('${t.id}')">Use this template</button>`;
    container.appendChild(card);
  }
}

function closeTemplatesModal() {
  document.getElementById("templates-modal").classList.add("hidden");
}

async function installTemplate(id) {
  const r = await fetch(`/api/agent-templates/${id}`);
  const template = await r.json();
  closeTemplatesModal();
  await showAgentModal(null, template);
}

async function stopRun(runId) {
  await fetch(`/api/agent-runs/${runId}/stop`, { method: "POST" });
  loadAgentRuns();
}

function updateAgentCronPreview(val) {
  const el = document.getElementById("a-cron-preview");
  if (el) el.textContent = cronLabel(val);
}

async function editCurrentAgent() {
  const agentId = window.AGENT_ID;
  if (!agentId) return;
  const r = await fetch(`/api/agents/${agentId}`);
  const agent = await r.json();
  openPromptEditor(agent.name, agent.prompt);
}

function openPromptEditor(name, prompt) {
  const overlay = document.getElementById("prompt-editor-overlay");
  if (!overlay) return;
  document.getElementById("pe-agent-name").textContent = name || "";
  const ta = document.getElementById("pe-textarea");
  ta.value = prompt || "";
  overlay.style.display = "flex";
  ta.focus();
  // Place cursor at start
  ta.setSelectionRange(0, 0);
  ta.scrollTop = 0;
}

function closePromptEditor() {
  const overlay = document.getElementById("prompt-editor-overlay");
  if (overlay) overlay.style.display = "none";
}

async function savePromptEditor() {
  const agentId = window.AGENT_ID;
  if (!agentId) return;
  const prompt = document.getElementById("pe-textarea").value;
  const btn = document.getElementById("pe-save-btn");
  if (btn) { btn.disabled = true; btn.textContent = "Saving…"; }
  const r = await fetch(`/api/agents/${agentId}`, {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt }),
  });
  if (btn) { btn.disabled = false; btn.textContent = "Save"; }
  if (r.ok) {
    closePromptEditor();
    loadAgentDetail();
  } else {
    const d = await r.json().catch(() => ({}));
    alert("Save failed: " + (d.detail || r.statusText));
  }
}

async function runCurrentAgent() {
  const agentId = window.AGENT_ID;
  if (!agentId) return;
  await fetch(`/api/agents/${agentId}/run`, { method: "POST" });
  loadAgentDetail();
}

async function stopAgentRun(runId) {
  const r = await fetch(`/api/agent-runs/${runId}/stop`, { method: "POST" });
  if (r.ok) loadAgentDetail();
  else alert("Could not stop run — it may have already finished.");
}

async function saveAgentAndReload() {
  await saveAgent();
  loadAgentDetail();
}

/* ══════════════════════════════════════════════════════════════════════════
   AGENT DETAIL PAGE
══════════════════════════════════════════════════════════════════════════ */

function initAgentDetail() {
  if (!document.getElementById("agent-detail-container")) return;
  _loadAvailableModels().then(() => loadAgentDetail());
  // Keyboard shortcuts for the prompt editor
  document.addEventListener("keydown", function _detailKeys(e) {
    const overlay = document.getElementById("prompt-editor-overlay");
    if (!overlay || overlay.style.display === "none") return;
    if (e.key === "Escape") { e.preventDefault(); closePromptEditor(); }
    if ((e.ctrlKey || e.metaKey) && e.key === "s") { e.preventDefault(); savePromptEditor(); }
  });
}

async function loadAgentDetail() {
  const agentId = window.AGENT_ID;
  if (!agentId) return;

  const [agentR, runsR] = await Promise.all([
    fetch(`/api/agents/${agentId}`),
    fetch(`/api/agents/${agentId}/runs`),
  ]);
  const agent = await agentR.json();
  const runs  = await runsR.json();

  // Fill metadata
  document.getElementById("d-name").textContent        = agent.name;
  document.getElementById("d-model").textContent       = _parseModel(agent.model).label || agent.model;
  document.getElementById("d-schedule").textContent    = agent.schedule || "Manual only";
  document.getElementById("d-subagents").textContent   = agent.can_create_subagents ? "Yes" : "No";
  document.getElementById("d-created-by").textContent  = agent.created_by;
  document.getElementById("d-created-at").textContent  = formatDate(agent.created_at);
  document.getElementById("d-prompt").textContent      = agent.prompt;
  document.getElementById("d-description").textContent = agent.description || "—";

  // Token totals
  const totalIn  = runs.reduce((s, r) => s + (r.input_tokens  || 0), 0);
  const totalOut = runs.reduce((s, r) => s + (r.output_tokens || 0), 0);
  document.getElementById("d-total-tokens").textContent =
    `${totalIn.toLocaleString()} input + ${totalOut.toLocaleString()} output tokens across ${runs.length} run${runs.length !== 1 ? "s" : ""}`;

  // Run history
  const tbody = document.querySelector("#detail-runs-table tbody");
  tbody.innerHTML = "";
  if (!runs.length) {
    tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-dim)">No runs yet</td></tr>`;
    return;
  }
  for (const run of runs) {
    const started = run.started_at ? new Date(run.started_at) : null;
    const ended   = run.ended_at   ? new Date(run.ended_at)   : null;
    const dur = (started && ended) ? Math.round((ended - started) / 1000) + "s" : "—";
    const statusClass = run.status === "success" ? "badge-green"
      : run.status === "error" ? "badge-red"
      : run.status === "stopped" ? "badge-red"
      : run.status === "running" ? "badge-blue" : "";
    const stopBtn = run.status === "running"
      ? `<button class="btn btn-ghost" style="padding:2px 10px;font-size:12px;color:var(--red)"
             onclick="event.stopPropagation();stopAgentRun('${esc(run.id)}')">Stop</button>`
      : `<button class="btn btn-ghost" style="padding:2px 10px;font-size:12px"
             onclick="event.stopPropagation();showRunDetail('${esc(run.id)}')">Detail</button>`;
    const tr = document.createElement("tr");
    tr.style.cursor = "pointer";
    tr.onclick = () => showRunDetail(run.id);
    tr.innerHTML = `
      <td><code style="font-size:11px">${esc(run.id.slice(0, 8))}</code></td>
      <td style="font-size:12px">${formatDate(run.started_at)}</td>
      <td style="font-size:12px">${esc(dur)}</td>
      <td>${statusClass ? `<span class="badge ${statusClass}">${esc(run.status)}</span>` : esc(run.status)}</td>
      <td style="font-size:12px;text-align:right">${(run.input_tokens || 0).toLocaleString()}</td>
      <td style="font-size:12px;text-align:right">${(run.output_tokens || 0).toLocaleString()}</td>
      <td>${stopBtn}</td>
    `;
    tbody.appendChild(tr);
  }
}

async function showRunDetail(runId) {
  const modal = document.getElementById("run-detail-modal");
  if (!modal) return;
  document.getElementById("rd-result").textContent = "Loading…";
  document.getElementById("rd-error").style.display = "none";
  document.getElementById("rd-status-badge").textContent = "";
  document.getElementById("rd-meta").textContent = "";
  modal.style.display = "flex";

  const r = await fetch(`/api/agent-runs/${runId}`);
  if (!r.ok) { document.getElementById("rd-result").textContent = "Failed to load run details."; return; }
  const run = await r.json();

  const statusClass = run.status === "success" ? "badge-green"
    : run.status === "error" || run.status === "stopped" ? "badge-red"
    : "badge-blue";
  const badge = document.getElementById("rd-status-badge");
  badge.className = `badge ${statusClass}`;
  badge.textContent = run.status;

  const started = run.started_at ? new Date(run.started_at) : null;
  const ended   = run.ended_at   ? new Date(run.ended_at)   : null;
  const dur = (started && ended) ? Math.round((ended - started) / 1000) + "s" : "";
  document.getElementById("rd-meta").textContent =
    formatDate(run.started_at) + (dur ? ` · ${dur}` : "");

  if (run.error) {
    const errEl = document.getElementById("rd-error");
    errEl.textContent = run.error;
    errEl.style.display = "";
  }
  document.getElementById("rd-result").textContent = run.result || "(no result text)";

  const link = document.getElementById("rd-audit-link");
  link.href = `/audit?task_id=${encodeURIComponent(runId)}`;
}

function closeRunDetail() {
  const modal = document.getElementById("run-detail-modal");
  if (modal) modal.style.display = "none";
}

/* ══════════════════════════════════════════════════════════════════════════
   MODELS PAGE
══════════════════════════════════════════════════════════════════════════ */

let _allModels = [];
let _modelFilter = "all";
let _modelSort = { key: null, dir: 1 }; // key: "input"|"output"|null, dir: 1=asc -1=desc

function sortModels(key) {
  if (_modelSort.key === key) {
    _modelSort.dir *= -1;
  } else {
    _modelSort.key = key;
    _modelSort.dir = 1;
  }
  // Update header indicators
  ["input", "output"].forEach(k => {
    const th = document.getElementById(`th-${k}`);
    if (!th) return;
    const base = k === "input" ? "Input / 1M" : "Output / 1M";
    th.textContent = k === _modelSort.key
      ? base + (_modelSort.dir === 1 ? " ▲" : " ▼")
      : base;
  });
  filterModels();
}

async function initModels() {
  if (!document.getElementById("models-container")) return;

  // Build filter chips
  const filtersEl = document.getElementById("models-filters");
  const filters = [
    { key: "all",       label: "All" },
    { key: "image_gen", label: "🎨 Image Gen" },
    { key: "vision",    label: "👁 Vision" },
    { key: "tools",     label: "🔧 Tools" },
    { key: "online",    label: "🌐 Online" },
  ];
  filtersEl.innerHTML = filters.map(f => `
    <button class="btn ${f.key === _modelFilter ? "btn-primary" : "btn-ghost"}"
            style="font-size:12px;padding:5px 12px"
            onclick="setModelFilter('${f.key}')"
            id="model-filter-${f.key}">${f.label}</button>
  `).join("");

  // Fetch model info + access tier in parallel
  const tbody = document.querySelector("#models-table tbody");
  try {
    const [infoResp, accessResp] = await Promise.all([
      fetch("/api/models/info"),
      fetch("/api/models"),
    ]);
    _allModels = await infoResp.json();
    const accessData = await accessResp.json();
    _renderAccessNote(document.getElementById("models-access-note"), accessData.access || {});
  } catch (e) {
    tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--red)">Failed to load models: ${esc(String(e))}</td></tr>`;
    return;
  }

  filterModels();
}

function _renderAccessNote(el, access) {
  if (!el) return;
  const notes = [];
  if (access.anthropic_blocked)
    notes.push("Anthropic models are unavailable — add your own Anthropic API key in <a href='/settings'>Settings → API Keys</a> to unlock them.");
  if (access.openrouter_free_only)
    notes.push("Only free OpenRouter models are shown — add your own OpenRouter API key in <a href='/settings'>Settings → API Keys</a> for full access.");
  if (notes.length) {
    el.innerHTML = `<strong style="color:var(--yellow)">⚠ Model Access Restrictions</strong><br>${notes.join("<br>")}`;
    el.style.display = "";
  } else {
    el.style.display = "none";
  }
}

function setModelFilter(key) {
  _modelFilter = key;
  // Update chip styles
  ["all", "image_gen", "vision", "tools", "online"].forEach(k => {
    const btn = document.getElementById(`model-filter-${k}`);
    if (!btn) return;
    btn.className = `btn ${k === key ? "btn-primary" : "btn-ghost"}`;
    btn.style.cssText = "font-size:12px;padding:5px 12px";
  });
  filterModels();
}

function filterModels() {
  const searchEl = document.getElementById("models-search");
  const q = searchEl ? searchEl.value.toLowerCase() : "";
  const visible = _allModels.filter(m => {
    const matchSearch = !q
      || m.id.toLowerCase().includes(q)
      || (m.name || "").toLowerCase().includes(q)
      || (m.bare_id || "").toLowerCase().includes(q);
    const caps = m.capabilities || {};
    const matchFilter = _modelFilter === "all"
      || (_modelFilter === "image_gen" && caps.image_gen)
      || (_modelFilter === "vision"    && caps.vision)
      || (_modelFilter === "tools"     && caps.tools)
      || (_modelFilter === "online"    && caps.online);
    return matchSearch && matchFilter;
  });

  // Sort
  if (_modelSort.key) {
    const priceKey = _modelSort.key === "input" ? "prompt_per_1m" : "completion_per_1m";
    visible.sort((a, b) => {
      const av = a.pricing?.[priceKey] ?? Infinity;
      const bv = b.pricing?.[priceKey] ?? Infinity;
      return (av - bv) * _modelSort.dir;
    });
  }

  const countEl = document.getElementById("models-count");
  if (countEl) {
    countEl.textContent = `Showing ${visible.length} of ${_allModels.length} models`;
  }

  const tbody = document.querySelector("#models-table tbody");
  if (!tbody) return;

  if (visible.length === 0) {
    tbody.innerHTML = `<tr><td colspan="5" style="text-align:center;color:var(--text-dim)">No models match your filter.</td></tr>`;
    return;
  }

  tbody.innerHTML = "";
  for (const m of visible) {
    const tr = document.createElement("tr");
    tr.style.cursor = "pointer";
    tr.onclick = () => showModelModal(m.id);

    const providerBadge = m.provider === "anthropic"
      ? `<span class="badge" style="background:rgba(255,160,80,0.15);color:#e8a060;font-size:10px">anthropic</span>`
      : `<span class="badge" style="background:rgba(108,142,245,0.12);color:var(--accent);font-size:10px">openrouter</span>`;

    const ctx = m.context_length ? _fmtCtx(m.context_length) : "—";
    const inp = _fmtPrice(m.pricing?.prompt_per_1m);
    const out = _fmtPrice(m.pricing?.completion_per_1m);

    const caps = m.capabilities || {};
    const capBadges = [
      caps.image_gen ? `<span class="badge" style="background:rgba(255,165,0,0.12);color:#e8a030;font-size:10px">🎨 Image Gen</span>` : "",
      caps.vision ? `<span class="badge" style="background:rgba(108,142,245,0.12);color:var(--accent);font-size:10px">👁 Vision</span>` : "",
      caps.tools  ? `<span class="badge" style="background:rgba(160,100,245,0.15);color:#b07aee;font-size:10px">🔧 Tools</span>` : "",
      caps.online ? `<span class="badge" style="background:rgba(76,175,125,0.15);color:var(--green);font-size:10px">🌐 Online</span>` : "",
    ].filter(Boolean).join(" ");

    tr.innerHTML = `
      <td>
        <div style="display:flex;align-items:center;gap:8px">
          ${providerBadge}
          <div>
            <div style="font-weight:500">${esc(m.name || m.bare_id)}</div>
            <div style="font-size:11px;color:var(--text-dim);font-family:var(--mono)">${esc(m.bare_id)}</div>
          </div>
        </div>
      </td>
      <td style="color:var(--text-dim);font-size:12px">${esc(ctx)}</td>
      <td style="font-size:12px">${inp}</td>
      <td style="font-size:12px">${out}</td>
      <td style="white-space:nowrap">${capBadges || '<span style="color:var(--text-dim);font-size:11px">—</span>'}</td>
    `;
    tbody.appendChild(tr);
  }
}

function _fmtCtx(n) {
  if (!n) return "—";
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(0) + "M";
  if (n >= 1000) return Math.round(n / 1000) + "K";
  return String(n);
}

function _fmtPrice(v) {
  if (v === null || v === undefined) return '<span style="color:var(--text-dim)">—</span>';
  if (v === 0) return '<span style="color:var(--green)">free</span>';
  return `$${v.toFixed(2)}`;
}

function showModelModal(modelId) {
  const m = _allModels.find(x => x.id === modelId);
  if (!m) return;

  const caps = m.capabilities || {};
  const capBadges = [
    caps.image_gen ? `<span class="badge" style="background:rgba(255,165,0,0.12);color:#e8a030">🎨 Image Gen</span>` : "",
    caps.vision ? `<span class="badge" style="background:rgba(108,142,245,0.12);color:var(--accent)">👁 Vision</span>` : "",
    caps.tools  ? `<span class="badge" style="background:rgba(160,100,245,0.15);color:#b07aee">🔧 Tools</span>` : "",
    caps.online ? `<span class="badge" style="background:rgba(76,175,125,0.15);color:var(--green)">🌐 Online</span>` : "",
  ].filter(Boolean).join(" ");

  const providerLabel = m.provider === "anthropic"
    ? `<span class="badge" style="background:rgba(255,160,80,0.15);color:#e8a060">anthropic</span>`
    : `<span class="badge" style="background:rgba(108,142,245,0.12);color:var(--accent)">openrouter</span>`;

  const arch = m.architecture || {};

  const el = document.getElementById("model-modal-content");
  el.innerHTML = `
    <div style="display:flex;align-items:center;gap:12px;margin-bottom:20px">
      ${providerLabel}
      <h2 style="font-size:18px;font-weight:600">${esc(m.name || m.bare_id)}</h2>
    </div>

    <div style="margin-bottom:16px">
      <div style="font-size:11px;color:var(--text-dim);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px">Model ID</div>
      <div style="font-family:var(--mono);font-size:12px;background:var(--bg3);border:1px solid var(--border);border-radius:4px;padding:8px 10px;display:flex;align-items:center;justify-content:space-between;gap:8px">
        <span>${esc(m.id)}</span>
        <button class="btn btn-ghost" style="padding:3px 8px;font-size:11px" onclick="navigator.clipboard.writeText('${esc(m.id)}');this.textContent='Copied!'">Copy</button>
      </div>
    </div>

    ${m.description ? `
    <div style="margin-bottom:16px">
      <div style="font-size:11px;color:var(--text-dim);margin-bottom:4px;text-transform:uppercase;letter-spacing:0.5px">Description</div>
      <div style="font-size:13px;line-height:1.6;color:var(--text)">${esc(m.description)}</div>
    </div>` : ""}

    <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:16px">
      <div class="meta-card">
        <div class="meta-label">Context window</div>
        <div class="meta-value">${_fmtCtx(m.context_length) || "—"}</div>
      </div>
      <div class="meta-card">
        <div class="meta-label">Input / 1M tokens</div>
        <div class="meta-value">${m.pricing?.prompt_per_1m != null ? (m.pricing.prompt_per_1m === 0 ? "Free" : "$" + m.pricing.prompt_per_1m.toFixed(2)) : "—"}</div>
      </div>
      <div class="meta-card">
        <div class="meta-label">Output / 1M tokens</div>
        <div class="meta-value">${m.pricing?.completion_per_1m != null ? (m.pricing.completion_per_1m === 0 ? "Free" : "$" + m.pricing.completion_per_1m.toFixed(2)) : "—"}</div>
      </div>
    </div>

    ${capBadges ? `
    <div style="margin-bottom:16px">
      <div style="font-size:11px;color:var(--text-dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Capabilities</div>
      <div style="display:flex;gap:8px;flex-wrap:wrap">${capBadges}</div>
    </div>` : ""}

    ${(arch.tokenizer || arch.modality) ? `
    <div>
      <div style="font-size:11px;color:var(--text-dim);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px">Architecture</div>
      <div style="display:flex;gap:16px;flex-wrap:wrap;font-size:12px">
        ${arch.tokenizer ? `<span style="color:var(--text-dim)">Tokenizer: <span style="color:var(--text)">${esc(arch.tokenizer)}</span></span>` : ""}
        ${arch.modality  ? `<span style="color:var(--text-dim)">Modality: <span style="color:var(--text)">${esc(arch.modality)}</span></span>` : ""}
      </div>
    </div>` : ""}
  `;

  document.getElementById("model-modal").classList.remove("hidden");
}

function closeModelModal() {
  document.getElementById("model-modal").classList.add("hidden");
}

/* ══════════════════════════════════════════════════════════════════════════
   Help page
══════════════════════════════════════════════════════════════════════════ */

let _helpObserver = null;

function initHelp() {
  if (!document.querySelector(".help-content")) return;

  // Disconnect any previous observer (SPA re-navigation)
  if (_helpObserver) { _helpObserver.disconnect(); _helpObserver = null; }

  // Mobile: add collapsible TOC toggle
  if (window.innerWidth <= 768) {
    const toc = document.querySelector(".help-toc");
    if (toc && !toc.querySelector(".toc-toggle")) {
      toc.classList.add("toc-collapsed");
      const btn = document.createElement("button");
      btn.className = "toc-toggle";
      btn.innerHTML = "Contents <span class='toc-arrow'>▾</span>";
      btn.addEventListener("click", () => {
        toc.classList.toggle("toc-collapsed");
        btn.querySelector(".toc-arrow").textContent =
          toc.classList.contains("toc-collapsed") ? "▾" : "▴";
      });
      toc.insertBefore(btn, toc.firstChild);
    }
  }

  const tocLinks = document.querySelectorAll(".toc-list a[href^='#']");
  const sections = document.querySelectorAll("section[data-section]");

  // IntersectionObserver — highlight TOC link for the section most in view
  _helpObserver = new IntersectionObserver(entries => {
    entries.forEach(entry => {
      if (!entry.isIntersecting) return;
      const id = entry.target.id;
      tocLinks.forEach(a => {
        a.classList.toggle("toc-active", a.getAttribute("href") === "#" + id);
      });
    });
  }, { rootMargin: "-20% 0px -70% 0px", threshold: 0 });

  sections.forEach(s => _helpObserver.observe(s));

  // Smooth-scroll anchor clicks within the help content
  tocLinks.forEach(a => {
    a.addEventListener("click", e => {
      e.preventDefault();
      const target = document.querySelector(a.getAttribute("href"));
      if (target) target.scrollIntoView({ behavior: "smooth" });
      // On mobile: collapse TOC after selecting a section
      if (window.innerWidth <= 768) {
        const toc = document.querySelector(".help-toc");
        if (toc) {
          toc.classList.add("toc-collapsed");
          const arrow = toc.querySelector(".toc-arrow");
          if (arrow) arrow.textContent = "▾";
        }
      }
    });
  });
}

function helpSearch(query) {
  const q = query.trim().toLowerCase();
  const sections = document.querySelectorAll(".help-content section[data-section]");
  let visibleCount = 0;

  sections.forEach(s => {
    const matches = !q || s.textContent.toLowerCase().includes(q);
    s.style.display = matches ? "" : "none";
    if (matches) visibleCount++;
  });

  // Sync TOC link visibility
  document.querySelectorAll(".toc-list a[href^='#']").forEach(a => {
    const id = a.getAttribute("href").slice(1);
    const el = document.getElementById(id);
    let visible;
    if (!q || !el) {
      visible = true;
    } else if (el.matches("section[data-section]")) {
      visible = el.style.display !== "none";
    } else {
      // Heading inside a section: scan from this element to the next sibling
      // of the same tag to check if the query appears in that content block.
      const tag = el.tagName;
      visible = false;
      let node = el;
      while (node) {
        if (node.textContent.toLowerCase().includes(q)) { visible = true; break; }
        node = node.nextElementSibling;
        if (node && node.tagName === tag) break;
      }
    }
    a.closest("li").style.display = visible ? "" : "none";
  });

  // Show/hide no-results message
  let noResults = document.getElementById("help-no-results");
  if (!noResults) {
    noResults = document.createElement("p");
    noResults.id = "help-no-results";
    noResults.style.cssText = "color:var(--text-dim);font-size:14px;padding:32px 0;text-align:center";
    noResults.textContent = "No matching sections.";
    document.querySelector(".help-content")?.appendChild(noResults);
  }
  noResults.style.display = (q && visibleCount === 0) ? "" : "none";

  // Scroll content back to top so hidden sections are visibly gone
  if (q) document.querySelector(".help-content")?.scrollTo({ top: 0 });
}

/* ── Theme picker ────────────────────────────────────────────────────────── */

async function loadTheme() {
  const container = document.getElementById("theme-picker");
  if (!container) return;
  const r = await fetch("/api/my/theme");
  if (!r.ok) { container.innerHTML = '<span style="color:var(--red)">Failed to load themes.</span>'; return; }
  const { active, themes } = await r.json();
  container.innerHTML = themes.map(t => `
    <button type="button" onclick="setTheme('${esc(t.id)}')"
      title="${esc(t.label)}"
      style="display:flex;flex-direction:column;align-items:center;gap:6px;background:none;border:none;cursor:pointer;padding:4px">
      <span style="
        display:block;width:56px;height:36px;border-radius:8px;
        background:${esc(t.preview.bg)};
        border:2px solid ${t.id === active ? 'var(--accent)' : 'transparent'};
        box-shadow:0 0 0 1px rgba(255,255,255,0.08);
        position:relative;overflow:hidden;
        transition:border-color 0.15s;
      " id="theme-swatch-${esc(t.id)}">
        <span style="
          position:absolute;bottom:4px;right:4px;
          width:16px;height:16px;border-radius:50%;
          background:${esc(t.preview.accent)};
        "></span>
      </span>
      <span style="font-size:11px;color:${t.id === active ? 'var(--accent)' : 'var(--text-dim)'};white-space:nowrap"
        id="theme-label-${esc(t.id)}">${esc(t.label)}</span>
    </button>
  `).join("");
}

async function setTheme(themeId) {
  const r = await fetch("/api/my/theme", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ theme_id: themeId }),
  });
  if (!r.ok) { showFlash("Failed to save theme."); return; }
  // Reload page so server-injected theme CSS takes effect
  location.reload();
}

/* ── Profile (account info + password + MFA) ─────────────────────────────── */

async function loadMyProfile() {
  const r = await fetch("/api/my/profile");
  if (!r.ok) return;
  const d = await r.json();
  const u = document.getElementById("profile-username");
  const e = document.getElementById("profile-email");
  const n = document.getElementById("profile-display-name");
  if (u) u.value = d.username || "";
  if (e) e.value = d.email || "";
  if (n) n.value = d.display_name || "";
}

async function saveMyProfile() {
  const errEl = document.getElementById("profile-save-error");
  if (errEl) errEl.style.display = "none";
  const display_name = document.getElementById("profile-display-name")?.value.trim() || "";
  const r = await fetch("/api/my/profile", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ display_name }),
  });
  if (!r.ok) {
    const d = await r.json().catch(() => ({}));
    if (errEl) { errEl.textContent = d.detail || "Failed to save."; errEl.style.display = ""; }
    return;
  }
  showFlash("Profile saved.");
}

async function changeMyProfilePassword() {
  const errEl = document.getElementById("prof-pw-error");
  errEl.style.display = "none";
  const current = document.getElementById("prof-pw-current").value;
  const pw = document.getElementById("prof-pw-new").value;
  const pw2 = document.getElementById("prof-pw-new2").value;
  if (pw.length < 8) { errEl.textContent = "Password must be at least 8 characters."; errEl.style.display = ""; return; }
  if (pw !== pw2) { errEl.textContent = "Passwords do not match."; errEl.style.display = ""; return; }
  const r = await fetch("/api/users/me/password", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ current_password: current, new_password: pw }),
  });
  if (!r.ok) {
    const d = await r.json().catch(() => ({}));
    errEl.textContent = d.detail || "Failed to change password.";
    errEl.style.display = "";
    return;
  }
  document.getElementById("prof-pw-current").value = "";
  document.getElementById("prof-pw-new").value = "";
  document.getElementById("prof-pw-new2").value = "";
  showFlash("Password changed.");
}

/* ── MFA (TOTP) management ───────────────────────────────────────────────── */

async function loadMfaStatus() {
  const area = document.getElementById("mfa-status-area");
  if (!area) return;
  const r = await fetch("/api/my/mfa/status");
  if (!r.ok) { area.innerHTML = '<span style="color:var(--red)">Failed to load MFA status.</span>'; return; }
  const { enabled } = await r.json();
  if (enabled) {
    area.innerHTML = `
      <div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
        <span class="badge badge-green" style="font-size:13px;padding:4px 10px">&#10003; MFA is active on your account</span>
      </div>
      <button class="btn btn-danger" onclick="showMfaDisableForm()">Disable MFA</button>
      <div id="mfa-disable-form" style="display:none;margin-top:16px;max-width:360px">
        <div class="form-group">
          <label>Current password</label>
          <input type="password" id="mfa-dis-pw" class="form-input" placeholder="Your password">
        </div>
        <div class="form-group">
          <label>Current authenticator code</label>
          <input type="text" id="mfa-dis-code" class="form-input" inputmode="numeric" maxlength="6" placeholder="000000">
        </div>
        <div id="mfa-dis-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
        <div style="display:flex;gap:8px">
          <button class="btn btn-ghost" onclick="document.getElementById('mfa-disable-form').style.display='none'">Cancel</button>
          <button class="btn btn-danger" onclick="confirmMfaDisable()">Confirm Disable</button>
        </div>
      </div>`;
  } else {
    area.innerHTML = `
      <p style="color:var(--text-dim);font-size:13px;margin-bottom:16px">MFA is not enabled on your account.</p>
      <button class="btn btn-primary" onclick="beginMfaSetup()">Enable MFA</button>
      <div id="mfa-setup-area" style="display:none;margin-top:20px"></div>`;
  }
}

function showMfaDisableForm() {
  const f = document.getElementById("mfa-disable-form");
  if (f) { f.style.display = ""; document.getElementById("mfa-dis-pw").focus(); }
}

async function beginMfaSetup() {
  const area = document.getElementById("mfa-setup-area");
  if (!area) return;
  area.innerHTML = '<span style="color:var(--text-dim)">Generating…</span>';
  area.style.display = "";
  const r = await fetch("/api/my/mfa/setup/begin", { method: "POST" });
  if (!r.ok) { area.innerHTML = '<span style="color:var(--red)">Failed to start setup.</span>'; return; }
  const { qr, secret } = await r.json();
  area.innerHTML = `
    <p style="font-size:13px;color:var(--text);margin-bottom:12px">
      Scan this QR code with your authenticator app, then enter the 6-digit code below to confirm.
    </p>
    <img src="${qr}" alt="QR code" style="width:180px;height:180px;display:block;margin-bottom:12px;border-radius:var(--radius)">
    <p style="font-size:11px;color:var(--text-dim);margin-bottom:12px">
      Manual entry key: <code style="font-size:11px;background:var(--bg3);padding:2px 6px;border-radius:4px">${esc(secret)}</code>
    </p>
    <div class="form-group" style="max-width:240px">
      <label>Verification code</label>
      <input type="text" id="mfa-confirm-code" class="form-input" inputmode="numeric" maxlength="6" placeholder="000000" autofocus>
    </div>
    <div id="mfa-confirm-error" style="display:none;color:var(--red);font-size:12px;margin-bottom:10px"></div>
    <button class="btn btn-primary" onclick="confirmMfaSetup()">Verify and Enable</button>`;
}

async function confirmMfaSetup() {
  const code = (document.getElementById("mfa-confirm-code")?.value || "").trim();
  const errEl = document.getElementById("mfa-confirm-error");
  if (errEl) errEl.style.display = "none";
  const r = await fetch("/api/my/mfa/setup/confirm", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ code }),
  });
  if (!r.ok) {
    const d = await r.json().catch(() => ({}));
    if (errEl) { errEl.textContent = d.detail || "Invalid code. Try again."; errEl.style.display = ""; }
    return;
  }
  showFlash("MFA enabled successfully.");
  loadMfaStatus();
}

async function confirmMfaDisable() {
  const pw = document.getElementById("mfa-dis-pw")?.value || "";
  const code = (document.getElementById("mfa-dis-code")?.value || "").trim();
  const errEl = document.getElementById("mfa-dis-error");
  if (errEl) errEl.style.display = "none";
  const r = await fetch("/api/my/mfa/disable", {
    method: "DELETE",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ password: pw, code }),
  });
  if (!r.ok) {
    const d = await r.json().catch(() => ({}));
    if (errEl) { errEl.textContent = d.detail || "Failed to disable MFA."; errEl.style.display = ""; }
    return;
  }
  showFlash("MFA disabled.");
  loadMfaStatus();
}

async function clearUserMfa(userId, username) {
  if (!confirm(`Clear MFA for user "${username}"? They will be able to log in with password only.`)) return;
  const r = await fetch(`/api/users/${userId}/mfa`, { method: "DELETE" });
  if (!r.ok) { alert("Failed to clear MFA."); return; }
  showFlash(`MFA cleared for ${username}.`);
  loadUsers();
}

/* ── Monitors page ───────────────────────────────────────────────────────── */

let _pages = [];
let _feeds = [];
let _editingPageId = null;
let _editingFeedId = null;

function switchMonitorTab(name) {
  ["pages", "rss"].forEach(t => {
    const pane = document.getElementById(`mpane-${t}`);
    const btn  = document.getElementById(`mtab-${t}`);
    if (pane) pane.style.display = t === name ? "" : "none";
    if (btn)  btn.classList.toggle("active", t === name);
  });
}

function initMonitors() {
  loadPages();
  loadFeeds();
}

/* ── Page Watchers ── */

async function loadPages() {
  const tbody = document.getElementById("pages-tbody");
  if (!tbody) return;
  const r = await fetch("/api/watched-pages");
  if (!r.ok) { tbody.innerHTML = '<tr><td colspan="7" style="color:var(--red)">Error loading pages</td></tr>'; return; }
  _pages = await r.json();
  if (!_pages.length) {
    tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text-dim)">No page watchers configured.</td></tr>';
    return;
  }
  tbody.innerHTML = _pages.map(p => {
    const statusColor = p.last_error ? 'var(--red)' : (p.last_content_hash ? 'var(--green)' : 'var(--text-dim)');
    const statusText = p.last_error ? 'error' : (p.last_content_hash ? 'ok' : 'pending');
    return `<tr>
      <td>${esc(p.name)}</td>
      <td style="font-size:12px;color:var(--text-dim);max-width:200px;overflow:hidden;text-overflow:ellipsis">${esc(p.url)}</td>
      <td style="font-size:12px;font-family:var(--mono)">${esc(p.schedule)}</td>
      <td><span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:${statusColor}">${statusText}${!p.enabled ? ' (disabled)' : ''}</span></td>
      <td style="font-size:12px;color:var(--text-dim)">${p.last_checked_at ? formatDateShort(p.last_checked_at) : '—'}</td>
      <td style="font-size:12px;color:var(--text-dim)">${p.last_changed_at ? formatDateShort(p.last_changed_at) : '—'}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="openPageModal('${p.id}')">Edit</button>
        <button class="btn btn-ghost btn-small" onclick="checkPageNow('${p.id}')">Check now</button>
        <button class="btn btn-danger btn-small" onclick="deletePage('${p.id}')">Delete</button>
      </td>
    </tr>`;
  }).join("");
}

async function openPageModal(id) {
  _editingPageId = id || null;
  const p = id ? _pages.find(x => x.id === id) : null;
  document.getElementById("page-modal-title").textContent = id ? "Edit Page Watcher" : "Add Page Watcher";
  document.getElementById("page-modal-id").value = id || "";
  document.getElementById("page-modal-name").value = p?.name || "";
  document.getElementById("page-modal-url").value = p?.url || "";
  document.getElementById("page-modal-selector").value = p?.css_selector || "";
  document.getElementById("page-modal-schedule").value = p?.schedule || "0 * * * *";
  document.getElementById("page-modal-mode").value = p?.notification_mode || "agent";
  await _populateMonitorAgentSelect("page-modal-agent", p?.agent_id);
  document.getElementById("page-modal").style.display = "";
}

function closePageModal() { document.getElementById("page-modal").style.display = "none"; }

async function savePage() {
  const name = document.getElementById("page-modal-name").value.trim();
  const url = document.getElementById("page-modal-url").value.trim();
  if (!name || !url) { showFlash("Name and URL are required."); return; }
  const payload = {
    name, url,
    css_selector: document.getElementById("page-modal-selector").value.trim() || null,
    schedule: document.getElementById("page-modal-schedule").value.trim() || "0 * * * *",
    notification_mode: document.getElementById("page-modal-mode").value,
    agent_id: document.getElementById("page-modal-agent").value || null,
  };
  const id = _editingPageId;
  const r = await fetch(id ? `/api/watched-pages/${id}` : "/api/watched-pages", {
    method: id ? "PUT" : "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  closePageModal();
  showFlash(id ? "Page watcher updated." : "Page watcher added.");
  loadPages();
}

async function deletePage(id) {
  if (!confirm("Delete this page watcher?")) return;
  const r = await fetch(`/api/watched-pages/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadPages(); }
}

async function checkPageNow(id) {
  const r = await fetch(`/api/watched-pages/${id}/check-now`, { method: "POST" });
  const d = await r.json();
  if (d.error) { showFlash("Error: " + d.error); }
  else if (d.first_check) { showFlash("First check done — baseline recorded."); }
  else { showFlash(d.changed ? "Change detected! Agent triggered." : "No change detected."); }
  loadPages();
}

/* ── RSS Feeds ── */

async function loadFeeds() {
  const tbody = document.getElementById("feeds-tbody");
  if (!tbody) return;
  const r = await fetch("/api/rss-feeds");
  if (!r.ok) { tbody.innerHTML = '<tr><td colspan="6" style="color:var(--red)">Error loading feeds</td></tr>'; return; }
  _feeds = await r.json();
  if (!_feeds.length) {
    tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim)">No RSS feeds configured.</td></tr>';
    return;
  }
  tbody.innerHTML = _feeds.map(f => {
    const statusColor = f.last_error ? 'var(--red)' : (f.last_fetched_at ? 'var(--green)' : 'var(--text-dim)');
    const statusText = f.last_error ? 'error' : (f.last_fetched_at ? 'ok' : 'pending');
    return `<tr>
      <td>${esc(f.name)}</td>
      <td style="font-size:12px;color:var(--text-dim);max-width:200px;overflow:hidden;text-overflow:ellipsis">${esc(f.url)}</td>
      <td style="font-size:12px;font-family:var(--mono)">${esc(f.schedule)}</td>
      <td><span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:${statusColor}">${statusText}${!f.enabled ? ' (disabled)' : ''}</span></td>
      <td style="font-size:12px;color:var(--text-dim)">${f.last_fetched_at ? formatDateShort(f.last_fetched_at) : '—'}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="openFeedModal('${f.id}')">Edit</button>
        <button class="btn btn-ghost btn-small" onclick="fetchFeedNow('${f.id}')">Fetch now</button>
        <button class="btn btn-danger btn-small" onclick="deleteFeed('${f.id}')">Delete</button>
      </td>
    </tr>`;
  }).join("");
}

async function openFeedModal(id) {
  _editingFeedId = id || null;
  const f = id ? _feeds.find(x => x.id === id) : null;
  document.getElementById("feed-modal-title").textContent = id ? "Edit RSS Feed" : "Add RSS Feed";
  document.getElementById("feed-modal-id").value = id || "";
  document.getElementById("feed-modal-name").value = f?.name || "";
  document.getElementById("feed-modal-url").value = f?.url || "";
  document.getElementById("feed-modal-schedule").value = f?.schedule || "0 */4 * * *";
  document.getElementById("feed-modal-max-items").value = f?.max_items_per_run || 5;
  document.getElementById("feed-modal-mode").value = f?.notification_mode || "agent";
  await _populateMonitorAgentSelect("feed-modal-agent", f?.agent_id);
  document.getElementById("feed-modal").style.display = "";
}

function closeFeedModal() { document.getElementById("feed-modal").style.display = "none"; }

async function saveFeed() {
  const name = document.getElementById("feed-modal-name").value.trim();
  const url = document.getElementById("feed-modal-url").value.trim();
  if (!name || !url) { showFlash("Name and URL are required."); return; }
  const payload = {
    name, url,
    schedule: document.getElementById("feed-modal-schedule").value.trim() || "0 */4 * * *",
    max_items_per_run: parseInt(document.getElementById("feed-modal-max-items").value) || 5,
    notification_mode: document.getElementById("feed-modal-mode").value,
    agent_id: document.getElementById("feed-modal-agent").value || null,
  };
  const id = _editingFeedId;
  const r = await fetch(id ? `/api/rss-feeds/${id}` : "/api/rss-feeds", {
    method: id ? "PUT" : "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(payload),
  });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  closeFeedModal();
  showFlash(id ? "Feed updated." : "Feed added.");
  loadFeeds();
}

async function deleteFeed(id) {
  if (!confirm("Delete this RSS feed?")) return;
  const r = await fetch(`/api/rss-feeds/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadFeeds(); }
}

async function fetchFeedNow(id) {
  const r = await fetch(`/api/rss-feeds/${id}/fetch-now`, { method: "POST" });
  const d = await r.json();
  if (d.error) { showFlash("Error: " + d.error); }
  else { showFlash(`Fetched. ${d.new_items || 0} new item(s) found.`); }
  loadFeeds();
}

async function _populateMonitorAgentSelect(selectId, selected) {
  const sel = document.getElementById(selectId);
  if (!sel) return;
  sel.innerHTML = '<option value="">— no agent —</option>';
  try {
    const r = await fetch("/api/agents");
    const agents = r.ok ? await r.json() : [];
    agents.forEach(a => {
      const opt = document.createElement("option");
      opt.value = a.id;
      opt.textContent = a.name;
      if (a.id === selected) opt.selected = true;
      sel.appendChild(opt);
    });
  } catch (_) { /* ignore */ }
}


/* ── Webhooks ────────────────────────────────────────────────────────────── */

let _webhooks = [];
let _webhookTargets = [];
let _editingWebhookId = null;
let _editingWebhookTargetId = null;

async function loadWebhooks() {
  const tbody = document.getElementById("webhooks-tbody");
  if (!tbody) return;
  const r = await fetch("/api/webhooks");
  if (!r.ok) { tbody.innerHTML = '<tr><td colspan="6" style="color:var(--red)">Error loading webhooks</td></tr>'; return; }
  _webhooks = await r.json();
  if (!_webhooks.length) {
    tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim)">No webhook endpoints configured.</td></tr>';
    return;
  }
  tbody.innerHTML = _webhooks.map(w => `
    <tr>
      <td>${esc(w.name)}</td>
      <td style="font-size:12px;color:var(--text-dim)">${esc(w.agent_id || '—')}</td>
      <td><span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:${w.enabled ? 'var(--green)' : 'var(--text-dim)'}">${w.enabled ? 'enabled' : 'disabled'}</span></td>
      <td style="font-size:12px">${w.trigger_count || 0}</td>
      <td style="font-size:12px;color:var(--text-dim)">${w.last_triggered_at ? formatDate(w.last_triggered_at) : '—'}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="openWebhookModal('${w.id}')">Edit</button>
        <button class="btn btn-ghost btn-small" onclick="rotateWebhookToken('${w.id}')">Rotate token</button>
        <button class="btn btn-danger btn-small" onclick="deleteWebhook('${w.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

async function openWebhookModal(id) {
  _editingWebhookId = id || null;
  const w = id ? _webhooks.find(x => x.id === id) : null;
  document.getElementById("webhook-modal-title").textContent = id ? "Edit Webhook Endpoint" : "Add Webhook Endpoint";
  document.getElementById("webhook-modal-id").value = id || "";
  document.getElementById("webhook-modal-name").value = w?.name || "";
  document.getElementById("webhook-modal-desc").value = w?.description || "";
  document.getElementById("webhook-modal-allow-get").checked = w ? !!w.allow_get : true;
  await _populateWebhookAgentSelect(w?.agent_id);
  document.getElementById("webhook-modal").style.display = "";
}

async function _populateWebhookAgentSelect(selected) {
  const sel = document.getElementById("webhook-modal-agent");
  if (!sel) return;
  sel.innerHTML = '<option value="">— no agent —</option>';
  try {
    const r = await fetch("/api/agents");
    const agents = r.ok ? await r.json() : [];
    agents.forEach(a => {
      const opt = document.createElement("option");
      opt.value = a.id;
      opt.textContent = a.name;
      if (a.id === selected) opt.selected = true;
      sel.appendChild(opt);
    });
  } catch (_) { /* ignore */ }
}

function closeWebhookModal() {
  document.getElementById("webhook-modal").style.display = "none";
}

async function saveWebhook() {
  const name = document.getElementById("webhook-modal-name").value.trim();
  if (!name) { showFlash("Name is required."); return; }
  const agent_id = document.getElementById("webhook-modal-agent").value;
  const description = document.getElementById("webhook-modal-desc").value.trim();
  const allow_get = document.getElementById("webhook-modal-allow-get").checked;
  const payload = { name, agent_id, description, allow_get };
  const id = _editingWebhookId;
  const url = id ? `/api/webhooks/${id}` : "/api/webhooks";
  const method = id ? "PUT" : "POST";
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  const data = await r.json();
  closeWebhookModal();
  showFlash(id ? "Webhook updated." : "Webhook created.");
  if (!id && data.token) {
    _showWebhookTokenModal(data.token);
  }
  loadWebhooks();
}

async function deleteWebhook(id) {
  if (!confirm("Delete this webhook endpoint?")) return;
  const r = await fetch(`/api/webhooks/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadWebhooks(); }
  else { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); }
}

async function rotateWebhookToken(id) {
  if (!confirm("Rotate the token for this webhook? The old token will stop working immediately.")) return;
  const r = await fetch(`/api/webhooks/${id}/rotate`, { method: "POST" });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  const data = await r.json();
  showFlash("Token rotated.");
  _showWebhookTokenModal(data.token);
}

function _showWebhookTokenModal(token) {
  document.getElementById("webhook-token-value").value = token;
  const base = location.origin;
  document.getElementById("webhook-token-url-post").textContent = `POST ${base}/webhook/${token}`;
  document.getElementById("webhook-token-url-get").textContent = `GET ${base}/webhook/${token}?q=your+message`;
  document.getElementById("webhook-token-modal").style.display = "";
}

function closeWebhookTokenModal() {
  document.getElementById("webhook-token-modal").style.display = "none";
}

function copyWebhookToken() {
  const val = document.getElementById("webhook-token-value").value;
  navigator.clipboard.writeText(val).then(() => showFlash("Copied!"));
}

/* Webhook targets (outbound) */

async function loadWebhookTargets() {
  const tbody = document.getElementById("webhook-targets-tbody");
  if (!tbody) return;
  const r = await fetch("/api/webhook-targets");
  if (!r.ok) { tbody.innerHTML = '<tr><td colspan="4" style="color:var(--red)">Error loading targets</td></tr>'; return; }
  _webhookTargets = await r.json();
  if (!_webhookTargets.length) {
    tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">No outbound targets configured.</td></tr>';
    return;
  }
  tbody.innerHTML = _webhookTargets.map(t => `
    <tr>
      <td><code>${esc(t.name)}</code></td>
      <td style="font-size:12px;color:var(--text-dim)">${esc(t.url)}</td>
      <td><span style="font-size:11px;padding:2px 8px;border-radius:20px;background:var(--bg2);color:${t.enabled ? 'var(--green)' : 'var(--text-dim)'}">${t.enabled ? 'enabled' : 'disabled'}</span></td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="openWebhookTargetModal('${t.id}')">Edit</button>
        <button class="btn btn-danger btn-small" onclick="deleteWebhookTarget('${t.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

function openWebhookTargetModal(id) {
  _editingWebhookTargetId = id || null;
  const t = id ? _webhookTargets.find(x => x.id === id) : null;
  document.getElementById("webhook-target-modal-title").textContent = id ? "Edit Webhook Target" : "Add Webhook Target";
  document.getElementById("webhook-target-modal-id").value = id || "";
  document.getElementById("webhook-target-modal-name").value = t?.name || "";
  document.getElementById("webhook-target-modal-url").value = t?.url || "";
  document.getElementById("webhook-target-modal-secret").value = "";
  document.getElementById("webhook-target-modal").style.display = "";
}

function closeWebhookTargetModal() {
  document.getElementById("webhook-target-modal").style.display = "none";
}

async function saveWebhookTarget() {
  const name = document.getElementById("webhook-target-modal-name").value.trim();
  const url = document.getElementById("webhook-target-modal-url").value.trim();
  if (!name || !url) { showFlash("Name and URL are required."); return; }
  const secret_header = document.getElementById("webhook-target-modal-secret").value;
  const payload = { name, url, secret_header };
  const id = _editingWebhookTargetId;
  const apiUrl = id ? `/api/webhook-targets/${id}` : "/api/webhook-targets";
  const method = id ? "PUT" : "POST";
  const r = await fetch(apiUrl, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  closeWebhookTargetModal();
  showFlash(id ? "Target updated." : "Target created.");
  loadWebhookTargets();
}

async function deleteWebhookTarget(id) {
  if (!confirm("Delete this webhook target?")) return;
  const r = await fetch(`/api/webhook-targets/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadWebhookTargets(); }
  else { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); }
}


/* ── User webhooks (non-admin) ───────────────────────────────────────────── */

let _myWebhooks = [];
let _editingMyWebhookId = null;

async function loadMyWebhooks() {
  const r = await fetch("/api/my/webhooks");
  if (!r.ok) return;
  _myWebhooks = await r.json();
  const tbody = document.getElementById("my-webhooks-tbody");
  if (!tbody) return;
  if (!_myWebhooks.length) {
    tbody.innerHTML = '<tr><td colspan="6" style="color:var(--text-dim)">No webhooks yet.</td></tr>';
    return;
  }
  tbody.innerHTML = _myWebhooks.map(w => `
    <tr>
      <td><strong>${esc(w.name)}</strong>${w.description ? `<br><span style="font-size:11px;color:var(--text-dim)">${esc(w.description)}</span>` : ""}</td>
      <td style="font-size:12px;color:var(--text-dim)">${esc(w.agent_id || "—")}</td>
      <td>${w.enabled ? '<span style="color:var(--green)">Active</span>' : '<span style="color:var(--text-dim)">Disabled</span>'}</td>
      <td style="font-size:12px">${w.trigger_count || 0}</td>
      <td style="font-size:12px;color:var(--text-dim)">${w.last_triggered_at ? formatDateShort(w.last_triggered_at) : "Never"}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="rotateMyWebhookToken('${w.id}')">Rotate token</button>
        <button class="btn btn-ghost btn-small" onclick="openMyWebhookModal('${w.id}')">Edit</button>
        <button class="btn btn-danger btn-small" onclick="deleteMyWebhook('${w.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

async function openMyWebhookModal(id) {
  _editingMyWebhookId = id || null;
  const w = id ? _myWebhooks.find(x => x.id === id) : null;
  document.getElementById("my-webhook-modal-title").textContent = id ? "Edit Webhook" : "Add Webhook";
  document.getElementById("my-webhook-modal-id").value = id || "";
  document.getElementById("my-webhook-modal-name").value = w?.name || "";
  document.getElementById("my-webhook-modal-desc").value = w?.description || "";
  document.getElementById("my-webhook-modal-allow-get").checked = w ? !!w.allow_get : true;
  await _populateMyWebhookAgentSelect(w?.agent_id);
  document.getElementById("my-webhook-modal").style.display = "";
}

async function _populateMyWebhookAgentSelect(selected) {
  const sel = document.getElementById("my-webhook-modal-agent");
  if (!sel) return;
  sel.innerHTML = '<option value="">— no agent —</option>';
  try {
    const r = await fetch("/api/agents");
    const agents = r.ok ? await r.json() : [];
    agents.forEach(a => {
      const opt = document.createElement("option");
      opt.value = a.id;
      opt.textContent = a.name;
      if (a.id === selected) opt.selected = true;
      sel.appendChild(opt);
    });
  } catch (_) {}
}

function closeMyWebhookModal() {
  document.getElementById("my-webhook-modal").style.display = "none";
}

async function saveMyWebhook() {
  const name = document.getElementById("my-webhook-modal-name").value.trim();
  if (!name) { showFlash("Name is required."); return; }
  const agent_id = document.getElementById("my-webhook-modal-agent").value;
  const description = document.getElementById("my-webhook-modal-desc").value.trim();
  const allow_get = document.getElementById("my-webhook-modal-allow-get").checked;
  const id = _editingMyWebhookId;
  const url = id ? `/api/my/webhooks/${id}` : "/api/my/webhooks";
  const method = id ? "PUT" : "POST";
  const r = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, agent_id, description, allow_get }) });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  const data = await r.json();
  closeMyWebhookModal();
  showFlash(id ? "Webhook updated." : "Webhook created.");
  if (!id && data.token) _showWebhookTokenModal(data.token);
  loadMyWebhooks();
}

async function deleteMyWebhook(id) {
  if (!confirm("Delete this webhook endpoint?")) return;
  const r = await fetch(`/api/my/webhooks/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadMyWebhooks(); }
  else showFlash("Error deleting webhook.");
}

async function rotateMyWebhookToken(id) {
  if (!confirm("Rotate token? The old token will stop working immediately.")) return;
  const r = await fetch(`/api/my/webhooks/${id}/rotate`, { method: "POST" });
  if (!r.ok) { showFlash("Error rotating token."); return; }
  const d = await r.json();
  _showWebhookTokenModal(d.token);
  loadMyWebhooks();
}

/* ── User outbound webhook targets ──────────────────────────────────────── */

let _myWebhookTargets = [];
let _editingMyWebhookTargetId = null;

async function loadMyWebhookTargets() {
  const r = await fetch("/api/my/webhook-targets");
  if (!r.ok) return;
  _myWebhookTargets = await r.json();
  const tbody = document.getElementById("my-webhook-targets-tbody");
  if (!tbody) return;
  if (!_myWebhookTargets.length) {
    tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-dim)">No targets yet.</td></tr>';
    return;
  }
  tbody.innerHTML = _myWebhookTargets.map(t => `
    <tr>
      <td><strong>${esc(t.name)}</strong></td>
      <td style="font-size:12px;color:var(--text-dim);word-break:break-all">${esc(t.url)}</td>
      <td>${t.enabled ? '<span style="color:var(--green)">Active</span>' : '<span style="color:var(--text-dim)">Disabled</span>'}</td>
      <td style="text-align:right">
        <button class="btn btn-ghost btn-small" onclick="openMyWebhookTargetModal('${t.id}')">Edit</button>
        <button class="btn btn-danger btn-small" onclick="deleteMyWebhookTarget('${t.id}')">Delete</button>
      </td>
    </tr>`).join("");
}

function openMyWebhookTargetModal(id) {
  _editingMyWebhookTargetId = id || null;
  const t = id ? _myWebhookTargets.find(x => x.id === id) : null;
  document.getElementById("my-webhook-target-modal-title").textContent = id ? "Edit Target" : "Add Outbound Target";
  document.getElementById("my-webhook-target-modal-id").value = id || "";
  document.getElementById("my-webhook-target-modal-name").value = t?.name || "";
  document.getElementById("my-webhook-target-modal-url").value = t?.url || "";
  document.getElementById("my-webhook-target-modal-secret").value = "";
  document.getElementById("my-webhook-target-modal-secret").placeholder = t?.secret_header ? "••••••••" : "Leave blank to omit";
  document.getElementById("my-webhook-target-modal").style.display = "";
}

function closeMyWebhookTargetModal() {
  document.getElementById("my-webhook-target-modal").style.display = "none";
}

async function saveMyWebhookTarget() {
  const name = document.getElementById("my-webhook-target-modal-name").value.trim();
  const url  = document.getElementById("my-webhook-target-modal-url").value.trim();
  if (!name) { showFlash("Name is required."); return; }
  if (!url)  { showFlash("URL is required."); return; }
  const secret = document.getElementById("my-webhook-target-modal-secret").value;
  const id = _editingMyWebhookTargetId;
  const endpoint = id ? `/api/my/webhook-targets/${id}` : "/api/my/webhook-targets";
  const method   = id ? "PUT" : "POST";
  const payload  = { name, url, secret_header: secret };
  const r = await fetch(endpoint, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
  if (!r.ok) { const e = await r.json().catch(() => ({})); showFlash("Error: " + (e.detail || r.status)); return; }
  closeMyWebhookTargetModal();
  showFlash(id ? "Target updated." : "Target created.");
  loadMyWebhookTargets();
}

async function deleteMyWebhookTarget(id) {
  if (!confirm("Delete this outbound target?")) return;
  const r = await fetch(`/api/my/webhook-targets/${id}`, { method: "DELETE" });
  if (r.ok) { showFlash("Deleted."); loadMyWebhookTargets(); }
  else showFlash("Error deleting target.");
}


/* ── Bootstrap ───────────────────────────────────────────────────────────── */

document.addEventListener("DOMContentLoaded", () => {
  initNav();
  _initPage(location.pathname);
});

// Close any open modal on Escape
document.addEventListener("keydown", e => {
  if (e.key !== "Escape") return;
  const overlay = [...document.querySelectorAll(".modal-overlay")]
    .find(el => el.style.display !== "none" && !el.classList.contains("hidden"));
  if (!overlay) return;
  // Find the ✕ close button, or any button whose onclick closes the modal
  const closeBtn = [...overlay.querySelectorAll("button")]
    .find(b => b.textContent.trim() === "✕" || /^close/i.test((b.getAttribute("onclick") || "").trim()));
  closeBtn?.click();
});
