LinkSpin test area: InstantAdPay engine fork rebranded, network registry, sponsor carry-over with engine activation and claim window, rotator with /r/ redirects, link-domain mini-sites
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+109
@@ -0,0 +1,109 @@
|
||||
// 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 };
|
||||
Reference in New Issue
Block a user