Admin: payment-routing / leak map for the org
New chain.getOrgRouting(rootId, ownerIds) simulates the contract's _payUpline routing for every member below the org root on their NEXT upgrade, classifying where the POL lands: your positions, a teammate in-org (healthy), an outsider above the org (true LEAK), or fees. Surfaced on /api/admin/income and rendered as a panel in "My Positions — Income": 4-stat summary + expandable list of payments escaping the org. Distinguishes real leaks (money leaving the team) from teammates earning — which the old upgrade-needs alert conflated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -521,6 +521,52 @@ function getOwnerUpgradeNeeds(ids) {
|
|||||||
return { ready: true, needs };
|
return { ready: true, needs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Payment-routing / leak map: simulates the contract's _payUpline routing for
|
||||||
|
// every member below `rootId` on their NEXT upgrade, classifying where the POL
|
||||||
|
// lands — a position you own, a teammate inside your org, an outsider above the
|
||||||
|
// org (a true LEAK), or fees. Answers "is money escaping my org?".
|
||||||
|
function getOrgRouting(rootId, ownerIds) {
|
||||||
|
if (!state || !state.snapshotAt || !costs) return { ready: false };
|
||||||
|
const M = state.members;
|
||||||
|
const CROOT = 1; // contract root position (#1); payments passing it go to fees
|
||||||
|
const owned = new Set((ownerIds || []).map(Number));
|
||||||
|
const lvl = id => (M[id] && M[id].level) || 1;
|
||||||
|
const catcher = (fromId, li) => {
|
||||||
|
let up = M[fromId] && M[fromId].uplineId;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
if (!up || up === CROOT) return null;
|
||||||
|
const u = M[up]; if (!u) return null;
|
||||||
|
if (i < li) { up = u.uplineId; continue; }
|
||||||
|
if ((u.level || 1) > li && (u.directCount || 0) >= 2) return up;
|
||||||
|
up = u.uplineId;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
const inOrg = id => { let up = M[id] && M[id].uplineId, g = 0; while (up && up !== CROOT && g++ < 48) { if (up === rootId) return true; up = M[up].uplineId; } return false; };
|
||||||
|
const ids = Object.keys(M).map(Number).filter(id => id !== rootId && inOrg(id));
|
||||||
|
let ownedPol = 0, teamPol = 0, leakPol = 0, feePol = 0;
|
||||||
|
const leaks = [];
|
||||||
|
for (const id of ids) {
|
||||||
|
const L = lvl(id); if (L >= 8) continue;
|
||||||
|
const li = L - 1;
|
||||||
|
const tier = (M[id].tier === 2) ? 2 : 1;
|
||||||
|
const amt = (costs.up[tier] || [])[li] || 0;
|
||||||
|
if (!amt) continue;
|
||||||
|
const c = catcher(id, li);
|
||||||
|
if (c == null) feePol += amt;
|
||||||
|
else if (owned.has(c)) ownedPol += amt;
|
||||||
|
else if (inOrg(c)) teamPol += amt;
|
||||||
|
else { leakPol += amt; leaks.push({ from: id, to: c, toLevel: lvl(c), amt: +amt.toFixed(2) }); }
|
||||||
|
}
|
||||||
|
leaks.sort((a, b) => b.amt - a.amt);
|
||||||
|
return {
|
||||||
|
ready: true, rootId, members: ids.length,
|
||||||
|
ownedPol: +ownedPol.toFixed(2), teamPol: +teamPol.toFixed(2),
|
||||||
|
leakPol: +leakPol.toFixed(2), feePol: +feePol.toFixed(2),
|
||||||
|
leaks: leaks.slice(0, 40)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// How an organization rooted at `rootId` stacks up against the whole contract —
|
// How an organization rooted at `rootId` stacks up against the whole contract —
|
||||||
// share of members and share of all POL paid. Computed from in-memory state.
|
// share of members and share of all POL paid. Computed from in-memory state.
|
||||||
function getOrgShare(rootId) {
|
function getOrgShare(rootId) {
|
||||||
@@ -559,4 +605,4 @@ async function getIncome(id) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgShare, getMatrixTree, isInTeam, CONTRACT };
|
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getMatrixTree, isInTeam, CONTRACT };
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & Conversions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.</p></div><div id="funnelStats" class="funnel"></div><div class="table-wrap"><table class="table"><thead><tr><th>Source</th><th>Bridge views</th><th>Start views</th><th>Training views</th><th>Invite views</th><th>Join clicks</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & Conversions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.</p></div><div id="funnelStats" class="funnel"></div><div class="table-wrap"><table class="table"><thead><tr><th>Source</th><th>Bridge views</th><th>Start views</th><th>Training views</th><th>Invite views</th><th>Join clicks</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Member ID Submissions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.</p></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>Name / Handle</th><th>New ID</th><th>Joined under</th><th>Source</th><th>On-chain</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Member ID Submissions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.</p></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>Name / Handle</th><th>New ID</th><th>Joined under</th><th>Source</th><th>On-chain</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div>
|
||||||
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Your Organization vs. the Network</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">How your team — rooted at your top ID — stacks up against the entire Crypto Team Build Network smart contract. Live on-chain.</p></div><form id="orgShareForm" style="display:flex;gap:8px"><input id="orgRoot" class="input" style="max-width:110px" placeholder="21" inputmode="numeric"><button class="btn btn-secondary btn-sm">Refresh</button></form></div><div id="orgShare"><div class="empty">Reading the blockchain…</div></div></div>
|
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Your Organization vs. the Network</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">How your team — rooted at your top ID — stacks up against the entire Crypto Team Build Network smart contract. Live on-chain.</p></div><form id="orgShareForm" style="display:flex;gap:8px"><input id="orgRoot" class="input" style="max-width:110px" placeholder="21" inputmode="numeric"><button class="btn btn-secondary btn-sm">Refresh</button></form></div><div id="orgShare"><div class="empty">Reading the blockchain…</div></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">My Positions — Income</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.</p></div><form id="incomeForm" style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"><input id="incomeIds" class="input" style="max-width:260px" placeholder="21,24,25" inputmode="numeric"><button class="btn btn-teal">Load</button></form><div id="incomeAlert"></div><div id="incomeSummary" class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px"></div><div id="incomeResult"></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">My Positions — Income</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.</p></div><form id="incomeForm" style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"><input id="incomeIds" class="input" style="max-width:260px" placeholder="21,24,25" inputmode="numeric"><button class="btn btn-teal">Load</button></form><div id="incomeAlert"></div><div id="incomeRouting" style="margin-bottom:14px"></div><div id="incomeSummary" class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px"></div><div id="incomeResult"></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">On-Chain Member Lookup</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.</p></div><form id="lookupForm" style="display:flex;gap:10px;margin-bottom:14px"><input id="lookupId" class="input" style="max-width:220px" placeholder="Member ID e.g. 46" inputmode="numeric"><button class="btn btn-teal">Look Up</button></form><div id="lookupResult"></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">On-Chain Member Lookup</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.</p></div><form id="lookupForm" style="display:flex;gap:10px;margin-bottom:14px"><input id="lookupId" class="input" style="max-width:220px" placeholder="Member ID e.g. 46" inputmode="numeric"><button class="btn btn-teal">Look Up</button></form><div id="lookupResult"></div></div>
|
||||||
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Matrix View</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.</p></div><div style="display:flex;gap:8px"><button id="treeLoadBtn" class="btn btn-secondary btn-sm">Load Matrix</button><button id="treeToggleBtn" class="btn btn-secondary btn-sm hidden">List view</button></div></div><div id="matrixNav" class="hidden" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="matrixTree"></div></div></div>
|
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Matrix View</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.</p></div><div style="display:flex;gap:8px"><button id="treeLoadBtn" class="btn btn-secondary btn-sm">Load Matrix</button><button id="treeToggleBtn" class="btn btn-secondary btn-sm hidden">List view</button></div></div><div id="matrixNav" class="hidden" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="matrixTree"></div></div></div>
|
||||||
<div class="stack"><div class="table-card"><h2 style="margin-top:0">Add Sponsor</h2><form id="addSponsorForm"><div class="form-grid"><div class="field"><label>ID</label><input name="id" class="input" required></div><div class="field"><label>Name</label><input name="name" class="input" required></div><div class="field"><label>Parent ID</label><input name="parentId" class="input"></div><div class="field"><label>Level</label><select name="level" class="select"><option>Scintilla</option><option>Ascensus</option><option>Fabrica</option><option>Culmen</option><option>Apex</option><option>Fastigium</option><option>Vertex</option><option>Corona</option></select></div></div><div class="field"><label>Contact email (optional)</label><input name="email" class="input" type="email" placeholder="member@example.com"></div><div class="field"><label>Notes</label><input name="notes" class="input"></div><button class="btn btn-teal" style="width:100%">Add to Queue</button></form></div>
|
<div class="stack"><div class="table-card"><h2 style="margin-top:0">Add Sponsor</h2><form id="addSponsorForm"><div class="form-grid"><div class="field"><label>ID</label><input name="id" class="input" required></div><div class="field"><label>Name</label><input name="name" class="input" required></div><div class="field"><label>Parent ID</label><input name="parentId" class="input"></div><div class="field"><label>Level</label><select name="level" class="select"><option>Scintilla</option><option>Ascensus</option><option>Fabrica</option><option>Culmen</option><option>Apex</option><option>Fastigium</option><option>Vertex</option><option>Corona</option></select></div></div><div class="field"><label>Contact email (optional)</label><input name="email" class="input" type="email" placeholder="member@example.com"></div><div class="field"><label>Notes</label><input name="notes" class="input"></div><button class="btn btn-teal" style="width:100%">Add to Queue</button></form></div>
|
||||||
|
|||||||
@@ -88,6 +88,21 @@ async function loadIncome(){
|
|||||||
const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—';
|
const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—';
|
||||||
const needs=d.upgradeNeeds||[];
|
const needs=d.upgradeNeeds||[];
|
||||||
document.getElementById('incomeAlert').innerHTML=needs.length?needs.map(n=>`<div class="callout warning" style="margin-bottom:10px">⏫ <strong>Upgrade #${n.id} to ${esc(n.neededLevelName)}${n.reason==='qualify'?' (and get 2 directs)':''}</strong> — ${n.members.map(m=>'#'+m).join(', ')} ${n.members.length===1?'is':'are'} one upgrade from paying you ~${fmt(n.amountAtRisk)} POL, but #${n.id} (${esc(n.levelName)}) can't catch it yet. Upgrade before they do or it passes up.</div>`).join(''):'';
|
document.getElementById('incomeAlert').innerHTML=needs.length?needs.map(n=>`<div class="callout warning" style="margin-bottom:10px">⏫ <strong>Upgrade #${n.id} to ${esc(n.neededLevelName)}${n.reason==='qualify'?' (and get 2 directs)':''}</strong> — ${n.members.map(m=>'#'+m).join(', ')} ${n.members.length===1?'is':'are'} one upgrade from paying you ~${fmt(n.amountAtRisk)} POL, but #${n.id} (${esc(n.levelName)}) can't catch it yet. Upgrade before they do or it passes up.</div>`).join(''):'';
|
||||||
|
const rt=d.routing;
|
||||||
|
document.getElementById('incomeRouting').innerHTML=(rt&&rt.ready)?(
|
||||||
|
`<div class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:8px">`+
|
||||||
|
`<div class="fact" style="border-color:rgba(123,224,161,.4)"><small>Your positions catch</small><strong style="color:var(--ok)">${fmt(rt.ownedPol)} POL</strong></div>`+
|
||||||
|
`<div class="fact"><small>Team (in-org) catches</small><strong style="color:var(--teal)">${fmt(rt.teamPol)} POL</strong><div class="micro" style="margin-top:2px">healthy</div></div>`+
|
||||||
|
`<div class="fact" style="border-color:${rt.leakPol>0?'rgba(255,126,107,.5)':'rgba(255,255,255,.12)'}"><small>Leaking above #${rt.rootId}</small><strong style="color:${rt.leakPol>0?'#ff7e6b':'var(--muted)'}">${fmt(rt.leakPol)} POL</strong></div>`+
|
||||||
|
`<div class="fact"><small>To fees</small><strong>${fmt(rt.feePol)} POL</strong></div>`+
|
||||||
|
`</div>`+
|
||||||
|
`<p class="micro" style="margin:0 0 8px">Next-upgrade routing for ${rt.members} members below #${rt.rootId} — where each imminent upgrade payment lands. "Team" = teammates earning (good). "Leaking" = escaping to outsiders above you (often structural, not fixable by upgrading).</p>`+
|
||||||
|
(rt.leaks&&rt.leaks.length?
|
||||||
|
`<details><summary style="cursor:pointer;color:#ff7e6b;font-weight:700">⚠ ${rt.leaks.length} payment${rt.leaks.length===1?'':'s'} escaping your org — show</summary><div class="table-wrap" style="margin-top:8px"><table class="table" style="min-width:420px"><thead><tr><th>Member's next upgrade</th><th>Escapes to (outside)</th><th>Amount</th></tr></thead><tbody>`+
|
||||||
|
rt.leaks.map(l=>`<tr><td>#${l.from}</td><td><strong style="color:#ff7e6b">#${l.to}</strong> (L${l.toLevel})</td><td>${fmt(l.amt)} POL</td></tr>`).join('')+
|
||||||
|
`</tbody></table></div></details>`
|
||||||
|
:`<div class="callout" style="border-color:rgba(123,224,161,.3)">✓ Nothing is escaping your org right now — every next-upgrade payment stays with you or your team.</div>`)
|
||||||
|
):'';
|
||||||
sum.innerHTML=`<div class="fact" style="border-color:rgba(123,224,161,.4)"><small>Total received (all)</small><strong style="color:var(--ok);font-size:18px">${fmt(d.grandEarnedPol)} POL</strong></div>`+
|
sum.innerHTML=`<div class="fact" style="border-color:rgba(123,224,161,.4)"><small>Total received (all)</small><strong style="color:var(--ok);font-size:18px">${fmt(d.grandEarnedPol)} POL</strong></div>`+
|
||||||
d.ids.map(id=>{const p=d.perId[id]||{};return `<div class="fact"><small>#${id}${p.registered&&p.levelName?' · '+esc(p.levelName):''}</small><strong>${p.registered?fmt(p.totalEarnedPol)+' POL':'<span style=\"color:var(--danger)\">not registered</span>'}</strong>${p.registered?`<div class="micro" style="margin-top:2px">${p.count} payment${p.count===1?'':'s'}</div>`:''}</div>`}).join('');
|
d.ids.map(id=>{const p=d.perId[id]||{};return `<div class="fact"><small>#${id}${p.registered&&p.levelName?' · '+esc(p.levelName):''}</small><strong>${p.registered?fmt(p.totalEarnedPol)+' POL':'<span style=\"color:var(--danger)\">not registered</span>'}</strong>${p.registered?`<div class="micro" style="margin-top:2px">${p.count} payment${p.count===1?'':'s'}</div>`:''}</div>`}).join('');
|
||||||
out.innerHTML=d.rows.length?`<div class="table-wrap"><table class="table" style="min-width:640px"><thead><tr><th>When (UTC)</th><th>To</th><th>From</th><th>For</th><th>Amount</th></tr></thead><tbody>${d.rows.map(r=>`<tr><td>${date(r.ts)}</td><td><strong class="mt-id">#${r.toId}</strong></td><td>#${r.fromId}</td><td>${esc(r.desc||'')}</td><td><strong>${fmt(r.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div><p class="micro" style="margin:8px 0 0">${d.rows.length} payment${d.rows.length===1?'':'s'} across your positions, newest first.</p>`:'<div class="empty">No payments recorded to these positions yet.</div>';
|
out.innerHTML=d.rows.length?`<div class="table-wrap"><table class="table" style="min-width:640px"><thead><tr><th>When (UTC)</th><th>To</th><th>From</th><th>For</th><th>Amount</th></tr></thead><tbody>${d.rows.map(r=>`<tr><td>${date(r.ts)}</td><td><strong class="mt-id">#${r.toId}</strong></td><td>#${r.fromId}</td><td>${esc(r.desc||'')}</td><td><strong>${fmt(r.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div><p class="micro" style="margin:8px 0 0">${d.rows.length} payment${d.rows.length===1?'':'s'} across your positions, newest first.</p>`:'<div class="empty">No payments recorded to these positions yet.</div>';
|
||||||
|
|||||||
@@ -457,7 +457,8 @@ async function handleApi(req,res,pathname){
|
|||||||
}
|
}
|
||||||
rows.sort((a,b)=>(b.ts||0)-(a.ts||0));
|
rows.sort((a,b)=>(b.ts||0)-(a.ts||0));
|
||||||
let upgradeNeeds=[];try{upgradeNeeds=chain.getOwnerUpgradeNeeds(ids).needs;}catch(e){}
|
let upgradeNeeds=[];try{upgradeNeeds=chain.getOwnerUpgradeNeeds(ids).needs;}catch(e){}
|
||||||
return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2),upgradeNeeds});
|
let routing=null;try{routing=chain.getOrgRouting(Number(getConfig().orgRootId)||21,ids);}catch(e){}
|
||||||
|
return json(res,200,{ids,perId,rows:rows.slice(0,500),grandEarnedPol:+grand.toFixed(2),grandListedPol:+grandListed.toFixed(2),upgradeNeeds,routing});
|
||||||
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
}catch(e){return json(res,502,{error:e.message||'Lookup failed'})}
|
||||||
}
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
|
if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){
|
||||||
|
|||||||
Reference in New Issue
Block a user