// Admin member card (Marty, 2026-09-13): one lookup that gathers everything known about a member so // the admin can search, drill down and act without digging through pages. Read side only; the // edits live in server.js (/api/admin/member PATCH/DELETE) and accounts.js. const fs = require('fs'); const path = require('path'); const db = require('./db'); let R = null; // { accounts, ads, chain, tank, legacy, promos, messages, dataDir } function init(refs) { R = refs; } const nameOf = a => a ? (a.username ? '@' + a.username : a.memberId ? 'member #' + a.memberId : a.email) : null; // resolve a search token: email, @username, member #, share code, or wallet address async function resolve(q) { const { accounts } = R; let t = String(q || '').trim(); if (!t) return null; if (t.includes('@') && t.indexOf('@') > 0) { const a = await accounts.byEmail(t.toLowerCase()); if (a) return a; } const u = t.replace(/^@/, ''); if (/^0x[0-9a-f]{40}$/i.test(t)) { const a = await accounts.byAddress(t.toLowerCase()); if (a) return a; const pos = accounts.positionByAddress ? await accounts.positionByAddress(t.toLowerCase()) : null; if (pos && pos.email) return accounts.byEmail(pos.email); } if (/^#?\d+$/.test(t)) { const id = Number(t.replace('#', '')); const a = await accounts.byMemberId(id); if (a) return a; const pos = await accounts.positionByMember(id); if (pos && pos.email) return accounts.byEmail(pos.email); } let a = await accounts.byUsername(u.toLowerCase()); if (a) return a; a = await accounts.byCode(u.toLowerCase()); if (a) return a; return null; } async function view(email) { const { accounts, ads, chain, tank, legacy, promos, messages } = R; const acct = await accounts.byEmail(String(email || '').toLowerCase()); if (!acct) return null; const out = { account: acct }; // sponsor + upline chain (site-side sponsorship, up to 5 levels) const up = []; let cur = acct; const seen = new Set([acct.email]); for (let i = 0; i < 5 && cur; i++) { const s = await accounts.sponsorOf(cur.email).catch(() => null); if (!s || seen.has(s.email)) break; seen.add(s.email); up.push({ email: s.email, name: nameOf(s), memberId: s.memberId || 0 }); cur = s; } out.upline = up; out.sponsorName = up[0] ? up[0].name : null; // positions (extra wallets) + every on-chain id this account owns const positions = await accounts.positions(acct.email).catch(() => []); out.positions = positions; const ids = new Set([acct.memberId, ...positions.map(p => p.memberId)].filter(Boolean)); out.ids = [...ids]; // on-chain out.chain = null; if (acct.memberId) { try { const m = await chain.member(acct.memberId); out.chain = { memberId: acct.memberId, sponsorId: m.sponsorId, buyerCount: m.buyerCount, activated: m.activated, account: m.account, level: m.buyerCount >= 5 ? 3 : m.buyerCount >= 2 ? 2 : 1 }; } catch (e) { out.chain = { memberId: acct.memberId, readError: true }; } } const purchases = [], received = [], qualified = new Set(); let receivedWei = 0n, spentWei = 0n, spentCents = 0; if (ids.size) { for (const ev of chain.recentEvents(1e9)) { if (ev.type === 'Purchase' && ids.has(ev.buyerId)) { purchases.push({ ts: ev.ts, buyerId: ev.buyerId, priceCents: ev.priceCents, paidWei: ev.paidWei, credits: ev.creditAmount, tx: ev.tx }); spentWei += BigInt(ev.paidWei || 0); spentCents += Number(ev.priceCents || 0); } if (ev.type === 'TierPaid' && ids.has(ev.recipientId)) { received.push({ ts: ev.ts, buyerId: ev.buyerId, tier: ev.tier, amountWei: ev.amountWei, tx: ev.tx }); receivedWei += BigInt(ev.amountWei || 0); } if (ev.type === 'BuyerCounted' && ids.has(ev.sponsorId) && ev.newBuyerId) qualified.add(ev.newBuyerId); } } purchases.sort((a, b) => b.ts - a.ts); received.sort((a, b) => b.ts - a.ts); out.purchases = purchases; out.received = received.slice(0, 50); out.totals = { purchases: purchases.length, spentCents, spentWei: spentWei.toString(), receivedWei: receivedWei.toString(), payoutsIn: received.length }; const buyerNames = await accounts.namesForMembers([...new Set([...purchases.map(p => p.buyerId), ...received.map(r => r.buyerId)])]).catch(() => ({})); out.names = buyerNames; // credits try { out.credits = await ads.balances([...ids], acct.email); } catch (e) { out.credits = null; } try { out.earnedSplit = await ads.earnedSplit(acct.email); } catch (e) {} // campaigns try { out.campaigns = (await ads.listCampaigns(acct.email)).map(c => ({ id: c.id, type: c.type, status: c.status, name: c.name || c.title || '', budget: c.budget, spent: c.spent, created: c.created, views: c.views, clicks: c.clicks })); } catch (e) { out.campaigns = []; } // line (3 levels) with wallet / bought / qualified per person const levels = await accounts.downline(acct.email, 3).catch(() => []); const boughtIds = new Set(chain.recentEvents(1e9).filter(ev => ev.type === 'Purchase').map(ev => ev.buyerId)); out.line = levels.map(L => ({ level: L.level, members: L.members.map(m => ({ email: m.email, name: nameOf(m), username: m.username, memberId: m.memberId || 0, wallet: !!m.address, joined: m.created, lastSeen: m.lastSeen || 0, bought: !!(m.memberId && boughtIds.has(m.memberId)), qualified: !!(m.memberId && qualified.has(m.memberId)) })) })); out.lineCounts = out.line.map(L => L.members.length); // tank / adoptions try { const tv = await tank.adminView(); out.tank = { waiting: !!tv.waiting.find(w => w.email === acct.email), adoptedBy: tv.adoptions.filter(a => a.adoptee === acct.email).map(a => ({ name: a.adopterName, email: a.adopter, created: a.created, status: a.status || a.state || '' })), adopted: tv.adoptions.filter(a => a.adopter === acct.email).map(a => ({ name: a.adopteeName, email: a.adoptee, created: a.created, status: a.status || a.state || '' })) }; } catch (e) { out.tank = null; } // legacy + promo + drip + earning + messages try { const rec = legacy.lookup(acct.email); let g = null; try { g = JSON.parse(fs.readFileSync(path.join(R.dataDir, 'legacy-grants.json'), 'utf8'))[acct.email] || null; } catch (e) {} out.legacy = rec ? { brand: rec.b, seg: rec.s === 'a' ? 'advertiser' : 'earner', grant: g } : null; } catch (e) { out.legacy = null; } try { if (db.enabled()) out.promos = await db.q('SELECT code, credits, via, ts FROM promo_redemptions WHERE email=? ORDER BY ts DESC', [acct.email]); else out.promos = (await promos.adminView()).recent.filter(r => r.email === acct.email); } catch (e) { out.promos = []; } try { out.drip = db.enabled() ? (await db.q('SELECT step, next_at, started, stopped, ref, angle FROM drips WHERE email=?', [acct.email]))[0] || null : null; } catch (e) { out.drip = null; } try { const vs = await ads.viewStatus(acct.email); out.earning = { today: vs.views || vs.viewsToday || 0, claimed: !!vs.claimed, streakDay: vs.streakDay || 0 }; } catch (e) { out.earning = null; } try { if (db.enabled()) { const since = new Date(Date.now() - 14 * 86400000).toISOString().slice(0, 10); const dv = await db.q('SELECT day, views, claimed, video_count FROM daily_views WHERE email=? AND day>=? ORDER BY day DESC', [acct.email, since]); out.days = dv; out.activeDays14 = dv.filter(d => d.views > 0 || d.video_count > 0).length; out.claims14 = dv.filter(d => d.claimed).length; const mc = await db.q('SELECT COUNT(*) n FROM sponsor_messages WHERE from_email=? OR to_email=?', [acct.email, acct.email]); out.messageCount = Number(mc[0].n) || 0; const vv = await db.q('SELECT COUNT(*) n FROM visit_seen WHERE email=?', [acct.email]).catch(() => [{ n: 0 }]); out.visits = Number(vv[0].n) || 0; const vw = await db.q('SELECT COUNT(*) n FROM video_seen WHERE email=?', [acct.email]).catch(() => [{ n: 0 }]); out.videos = Number(vw[0].n) || 0; } } catch (e) {} return out; } module.exports = { init, resolve, view, nameOf };