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:
martbost
2026-08-15 18:35:48 -05:00
parent 0496584fb2
commit 9717ddbd85
4 changed files with 65 additions and 3 deletions
+15
View File
@@ -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=>`<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>`+
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>';