diff --git a/chain.js b/chain.js index 34872b2..1981ec2 100644 --- a/chain.js +++ b/chain.js @@ -521,6 +521,52 @@ function getOwnerUpgradeNeeds(ids) { 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 — // share of members and share of all POL paid. Computed from in-memory state. 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 }; diff --git a/public/admin.html b/public/admin.html index 37a4923..cb61f25 100644 --- a/public/admin.html +++ b/public/admin.html @@ -5,7 +5,7 @@

Traffic & Conversions

First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.

SourceBridge viewsStart viewsTraining viewsInvite viewsJoin clicksStart → Join

Member ID Submissions

New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.

WhenName / HandleNew IDJoined underSourceOn-chain

Your Organization vs. the Network

How your team — rooted at your top ID — stacks up against the entire Crypto Team Build Network smart contract. Live on-chain.

Reading the blockchain…
-

My Positions — Income

Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.

+

My Positions — Income

Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.

On-Chain Member Lookup

Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.

Matrix View

The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.

Add Sponsor

diff --git a/public/admin.js b/public/admin.js index d1d381a..2597228 100644 --- a/public/admin.js +++ b/public/admin.js @@ -88,6 +88,21 @@ async function loadIncome(){ const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—'; const needs=d.upgradeNeeds||[]; document.getElementById('incomeAlert').innerHTML=needs.length?needs.map(n=>`
⏫ Upgrade #${n.id} to ${esc(n.neededLevelName)}${n.reason==='qualify'?' (and get 2 directs)':''} — ${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.
`).join(''):''; + const rt=d.routing; + document.getElementById('incomeRouting').innerHTML=(rt&&rt.ready)?( + `
`+ + `
Your positions catch${fmt(rt.ownedPol)} POL
`+ + `
Team (in-org) catches${fmt(rt.teamPol)} POL
healthy
`+ + `
Leaking above #${rt.rootId}${fmt(rt.leakPol)} POL
`+ + `
To fees${fmt(rt.feePol)} POL
`+ + `
`+ + `

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).

`+ + (rt.leaks&&rt.leaks.length? + `
⚠ ${rt.leaks.length} payment${rt.leaks.length===1?'':'s'} escaping your org — show
`+ + rt.leaks.map(l=>``).join('')+ + `
Member's next upgradeEscapes to (outside)Amount
#${l.from}#${l.to} (L${l.toLevel})${fmt(l.amt)} POL
` + :`
✓ Nothing is escaping your org right now — every next-upgrade payment stays with you or your team.
`) + ):''; sum.innerHTML=`
Total received (all)${fmt(d.grandEarnedPol)} POL
`+ d.ids.map(id=>{const p=d.perId[id]||{};return `
#${id}${p.registered&&p.levelName?' · '+esc(p.levelName):''}${p.registered?fmt(p.totalEarnedPol)+' POL':'not registered'}${p.registered?`
${p.count} payment${p.count===1?'':'s'}
`:''}
`}).join(''); out.innerHTML=d.rows.length?`
${d.rows.map(r=>``).join('')}
When (UTC)ToFromForAmount
${date(r.ts)}#${r.toId}#${r.fromId}${esc(r.desc||'')}${fmt(r.pol)} POL

${d.rows.length} payment${d.rows.length===1?'':'s'} across your positions, newest first.

`:'
No payments recorded to these positions yet.
'; diff --git a/server.js b/server.js index 2512b5f..913d2dc 100644 --- a/server.js +++ b/server.js @@ -457,7 +457,8 @@ async function handleApi(req,res,pathname){ } rows.sort((a,b)=>(b.ts||0)-(a.ts||0)); 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'})} } if(req.method==='GET'&&pathname==='/api/admin/member-lookup'){