Dashboard 'My Next Step' panel + funded badge
Adds a synthesized next-step panel to /my: the level ladder (you-are-here → upgrade-next), the single clearest action (qualify vs upgrade), and a funded badge that checks the member's wallet against their next upgrade cost. Privacy: the wallet balance is checked server-side (chain.balanceOf via eth_getBalance) and ONLY a funded true/false is returned — the raw balance is never exposed on the ID-addressable dashboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -101,6 +101,14 @@ const pol = wei => Number(wei / 1000000000000n) / 1e6; // 6-decimal POL
|
|||||||
const encU = v => BigInt(v).toString(16).padStart(64, '0');
|
const encU = v => BigInt(v).toString(16).padStart(64, '0');
|
||||||
|
|
||||||
async function ethCall(data) { return await rpc('eth_call', [{ to: CONTRACT, data }, 'latest']); }
|
async function ethCall(data) { return await rpc('eth_call', [{ to: CONTRACT, data }, 'latest']); }
|
||||||
|
// Native POL balance of a wallet (public on-chain data). Used ONLY to derive a
|
||||||
|
// funded / not-funded flag for a member's next upgrade — the API never returns
|
||||||
|
// the raw balance, only the boolean, to keep members' holdings private.
|
||||||
|
async function balanceOf(address) {
|
||||||
|
if (!/^0x[0-9a-fA-F]{40}$/.test(address || '')) return null;
|
||||||
|
try { const hex = await rpc('eth_getBalance', [address, 'latest'], 6000); return Number(BigInt(hex)) / 1e18; }
|
||||||
|
catch (e) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
function decodeMemberStruct(r) {
|
function decodeMemberStruct(r) {
|
||||||
// members(uint48): account, joinedAt, referrerId, uplineId, tier, level, directCount,
|
// members(uint48): account, joinedAt, referrerId, uplineId, tier, level, directCount,
|
||||||
@@ -617,4 +625,4 @@ async function getIncome(id) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getMatrixTree, isInTeam, CONTRACT };
|
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getMatrixTree, isInTeam, balanceOf, CONTRACT };
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<div id="coldIntro" class="hidden cold-intro"><button id="coldIntroClose" class="cold-intro-x" title="Dismiss">×</button><div class="eyebrow">New here? What you're looking at</div><p style="margin:8px 0 12px;line-height:1.6">This is a live member dashboard for the <strong>RM Circle Premium team build</strong> — a team-first strategy on the Polygon blockchain where every payment happens on a public smart contract that nobody can fake or edit. The plan is simple: <strong>get your 2, then help your 2 get their 2.</strong> Every number on this page is this member's real position, read straight from the blockchain.</p><div style="display:flex;gap:10px;flex-wrap:wrap"><a id="coldIntroJoin" class="btn btn-primary btn-sm" href="/join">Read your invitation from this member →</a><a class="btn btn-secondary btn-sm" href="/">How the strategy works</a></div><p class="micro" style="margin:10px 0 0">Independent team training resource • Participation involves risk • No income is guaranteed</p></div>
|
<div id="coldIntro" class="hidden cold-intro"><button id="coldIntroClose" class="cold-intro-x" title="Dismiss">×</button><div class="eyebrow">New here? What you're looking at</div><p style="margin:8px 0 12px;line-height:1.6">This is a live member dashboard for the <strong>RM Circle Premium team build</strong> — a team-first strategy on the Polygon blockchain where every payment happens on a public smart contract that nobody can fake or edit. The plan is simple: <strong>get your 2, then help your 2 get their 2.</strong> Every number on this page is this member's real position, read straight from the blockchain.</p><div style="display:flex;gap:10px;flex-wrap:wrap"><a id="coldIntroJoin" class="btn btn-primary btn-sm" href="/join">Read your invitation from this member →</a><a class="btn btn-secondary btn-sm" href="/">How the strategy works</a></div><p class="micro" style="margin:10px 0 0">Independent team training resource • Participation involves risk • No income is guaranteed</p></div>
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:6px"><div><div class="eyebrow">Member position</div><h1 style="margin:6px 0 0">Position <span class="gold" id="dTitle">#—</span> <span id="dVerified" class="live-badge" style="vertical-align:middle;margin-left:8px"><span class="dot"></span> Verified on-chain</span> <span id="dQualified"></span></h1></div><button id="switchId" class="btn btn-secondary btn-sm">Look up another ID</button></div>
|
<div style="display:flex;justify-content:space-between;align-items:center;gap:14px;flex-wrap:wrap;margin-bottom:6px"><div><div class="eyebrow">Member position</div><h1 style="margin:6px 0 0">Position <span class="gold" id="dTitle">#—</span> <span id="dVerified" class="live-badge" style="vertical-align:middle;margin-left:8px"><span class="dot"></span> Verified on-chain</span> <span id="dQualified"></span></h1></div><button id="switchId" class="btn btn-secondary btn-sm">Look up another ID</button></div>
|
||||||
<div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div>
|
<div id="dFacts" class="facts" style="grid-template-columns:repeat(3,1fr);margin:18px 0"></div>
|
||||||
|
<div id="dNextStep"></div>
|
||||||
<div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div>
|
<div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div>
|
||||||
<div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div>
|
<div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div>
|
||||||
<div class="table-card" style="margin-bottom:18px;border-color:rgba(78,214,203,.35)"><h2 style="margin:0 0 4px">Email me my alerts</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Get an email the moment this position is <strong>paid</strong>, and when it <strong>needs an upgrade</strong> to catch incoming pay — so you never miss one. Opt in with your email; unsubscribe anytime.</p><div id="dAlerts"></div></div>
|
<div class="table-card" style="margin-bottom:18px;border-color:rgba(78,214,203,.35)"><h2 style="margin:0 0 4px">Email me my alerts</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Get an email the moment this position is <strong>paid</strong>, and when it <strong>needs an upgrade</strong> to catch incoming pay — so you never miss one. Opt in with your email; unsubscribe anytime.</p><div id="dAlerts"></div></div>
|
||||||
|
|||||||
@@ -99,6 +99,7 @@
|
|||||||
document.getElementById('dIncome').innerHTML=inc.length
|
document.getElementById('dIncome').innerHTML=inc.length
|
||||||
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
|
?`<div class="table-wrap"><table class="table" style="min-width:520px"><thead><tr><th>When</th><th>From member</th><th>For</th><th>Amount</th></tr></thead><tbody>${inc.map(p=>`<tr><td>${date(p.ts)}</td><td>#${p.fromId}</td><td>${esc(p.desc)}</td><td><strong>${fmt(p.pol)} POL</strong></td></tr>`).join('')}</tbody></table></div>`
|
||||||
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
|
:'<div class="empty">No payments yet — they appear here the moment they land on-chain.</div>';
|
||||||
|
renderNextStep(d);
|
||||||
renderPipeline(d);
|
renderPipeline(d);
|
||||||
renderAlerts(d);
|
renderAlerts(d);
|
||||||
renderShare(d);
|
renderShare(d);
|
||||||
@@ -113,6 +114,33 @@
|
|||||||
}catch(e){return ''}
|
}catch(e){return ''}
|
||||||
}
|
}
|
||||||
const LEVELS=['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
|
const LEVELS=['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
|
||||||
|
// "My Next Step" — the single clearest action + the level ladder + a funded
|
||||||
|
// badge (server tells us funded true/false; it never sends the raw balance).
|
||||||
|
function renderNextStep(d){
|
||||||
|
const el=document.getElementById('dNextStep');if(!el)return;
|
||||||
|
const ns=d.nextStep||{},lvl=d.level||1;
|
||||||
|
const ladder=LEVELS.map((nm,i)=>{
|
||||||
|
const L=i+1;let cls='nlv';
|
||||||
|
if(L<lvl)cls+=' done';else if(L===lvl)cls+=' you';else if(ns.kind==='upgrade'&&L===ns.nextLevel)cls+=' next';
|
||||||
|
const tag=L===lvl?'<span class="nlvt">YOU</span>':(ns.kind==='upgrade'&&L===ns.nextLevel?'<span class="nlvt">NEXT</span>':'');
|
||||||
|
return `<div class="${cls}">${tag}<span class="nlvn">${nm}</span></div>`;
|
||||||
|
}).join('');
|
||||||
|
let head='',body='',badge='';
|
||||||
|
if(ns.kind==='qualify'){
|
||||||
|
head='Your next step — get qualified';
|
||||||
|
body=`You need <strong>${ns.need} more direct${ns.need===1?'':'s'}</strong> (people who join on <em>your</em> link) to unlock upgrade payments. Grab your link from <a href="#dShare" style="color:var(--teal)">Share this position</a> below and go get them.`;
|
||||||
|
}else if(ns.kind==='upgrade'){
|
||||||
|
const nm=LEVELS[ns.nextLevel-1]||('Level '+ns.nextLevel);
|
||||||
|
head=`Your next step — upgrade to ${esc(nm)}`;
|
||||||
|
body=`Stay one level ahead of your deepest active team so the pass-ups from below land on <em>you</em> instead of skipping past. Upgrade with your earned POL when you can.`;
|
||||||
|
if(ns.funded===true)badge='<span class="ns-badge ok">✓ Funded — you can upgrade now</span>';
|
||||||
|
else if(ns.funded===false)badge='<span class="ns-badge no">Keep building — not funded yet</span>';
|
||||||
|
}else if(ns.kind==='max'){
|
||||||
|
head="You're at the top level 🏆";
|
||||||
|
body='Keep helping your team duplicate — every level they climb still pays up to you.';
|
||||||
|
}else{el.innerHTML='';return;}
|
||||||
|
el.innerHTML=`<div class="ns-card"><div class="ns-top"><div class="ns-eyebrow">Your plan</div>${badge}</div><div class="ns-h">${head}</div><p class="ns-p">${body}</p><div class="nladder">${ladder}</div></div>`;
|
||||||
|
}
|
||||||
function renderPipeline(d){
|
function renderPipeline(d){
|
||||||
const el=document.getElementById('dPipeline');
|
const el=document.getElementById('dPipeline');
|
||||||
if(!el)return;
|
if(!el)return;
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh
|
|||||||
.banner-cell .meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:12px 2px 0}
|
.banner-cell .meta{display:flex;justify-content:space-between;align-items:baseline;gap:8px;margin:12px 2px 0}
|
||||||
.banner-cell .sz{font-weight:800;color:var(--text);font-size:15px}.banner-cell .use{color:var(--muted);font-size:12px;text-align:right}
|
.banner-cell .sz{font-weight:800;color:var(--text);font-size:15px}.banner-cell .use{color:var(--muted);font-size:12px;text-align:right}
|
||||||
.banner-cell .acts{display:flex;gap:8px;margin-top:12px}.banner-cell .acts .btn{flex:1}
|
.banner-cell .acts{display:flex;gap:8px;margin-top:12px}.banner-cell .acts .btn{flex:1}
|
||||||
|
/* dashboard "My Next Step" plan panel + mini level ladder */
|
||||||
|
.ns-card{background:linear-gradient(150deg,#12293f,#0b1b2b);border:1px solid #2a4a63;border-radius:16px;padding:18px 20px;margin:0 0 22px}
|
||||||
|
.ns-top{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap;margin-bottom:8px}
|
||||||
|
.ns-eyebrow{color:var(--teal);text-transform:uppercase;letter-spacing:1.6px;font-weight:800;font-size:12px}
|
||||||
|
.ns-badge{font-size:12.5px;font-weight:800;padding:5px 11px;border-radius:999px;white-space:nowrap}
|
||||||
|
.ns-badge.ok{background:rgba(123,224,161,.12);color:var(--ok);border:1px solid rgba(123,224,161,.35)}
|
||||||
|
.ns-badge.no{background:rgba(243,190,67,.1);color:#f2c768;border:1px solid rgba(243,190,67,.3)}
|
||||||
|
.ns-h{font-size:20px;font-weight:900;letter-spacing:-.3px;margin:2px 0 6px}
|
||||||
|
.ns-p{color:var(--muted);font-size:14.5px;line-height:1.55;margin:0 0 15px}.ns-p strong{color:var(--text)}
|
||||||
|
.nladder{display:flex;gap:6px}
|
||||||
|
.nlv{flex:1;text-align:center;background:#0d2135;border:1px solid #203c53;border-radius:9px;padding:9px 3px;position:relative}
|
||||||
|
.nlv .nlvn{font-size:11.5px;font-weight:700;color:#b7c6d5;line-height:1.1;display:block}
|
||||||
|
.nlv.done{opacity:.5}
|
||||||
|
.nlv.you{background:linear-gradient(160deg,#f3be43,#f7d074);border-color:#f3be43}.nlv.you .nlvn{color:#14202e}
|
||||||
|
.nlv.next{background:rgba(78,214,203,.12);border:1.5px solid var(--teal)}.nlv.next .nlvn{color:#7fe9df}
|
||||||
|
.nlvt{position:absolute;top:-9px;left:50%;transform:translateX(-50%);font-size:8.5px;font-weight:900;letter-spacing:.4px;padding:1.5px 6px;border-radius:99px;white-space:nowrap}
|
||||||
|
.nlv.you .nlvt{background:#14202e;color:#f3be43}.nlv.next .nlvt{background:var(--teal);color:#08222b}
|
||||||
|
@media(max-width:560px){.nladder{flex-wrap:wrap}.nlv{flex:0 0 calc(25% - 5px)}}
|
||||||
.pp-passed{color:#f2c768;font-size:12px;margin-top:3px}
|
.pp-passed{color:#f2c768;font-size:12px;margin-top:3px}
|
||||||
.pp-verify{flex:0 0 auto;font-size:13px;color:var(--teal);text-decoration:none;border:1px solid rgba(78,214,203,.35);border-radius:9px;padding:7px 10px;white-space:nowrap}
|
.pp-verify{flex:0 0 auto;font-size:13px;color:var(--teal);text-decoration:none;border:1px solid rgba(78,214,203,.35);border-radius:9px;padding:7px 10px;white-space:nowrap}
|
||||||
.pp-verify:hover{background:rgba(78,214,203,.1)}
|
.pp-verify:hover{background:rgba(78,214,203,.1)}
|
||||||
|
|||||||
@@ -385,6 +385,21 @@ async function handleApi(req,res,pathname){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Next-step plan + funded badge. Wallet balance is checked server-side and
|
||||||
|
// ONLY the boolean (covers next upgrade?) is exposed — never the raw amount.
|
||||||
|
if(r.registered){
|
||||||
|
const lvl=r.level||1, tier=r.tier===2?2:1, qualified=(r.directCount||0)>=2;
|
||||||
|
if(!qualified){
|
||||||
|
r.nextStep={kind:'qualify',need:2-(r.directCount||0)};
|
||||||
|
}else if(lvl<8){
|
||||||
|
const cost=(r.upgradeCosts&&r.upgradeCosts[tier]&&r.upgradeCosts[tier][lvl-1])||0;
|
||||||
|
let funded=null;
|
||||||
|
try{ if(r.account&&cost){ const bal=await chain.balanceOf(r.account); if(bal!=null) funded=bal>=cost; } }catch(e){}
|
||||||
|
r.nextStep={kind:'upgrade',nextLevel:lvl+1,cost,funded};
|
||||||
|
}else{
|
||||||
|
r.nextStep={kind:'max'};
|
||||||
|
}
|
||||||
|
}
|
||||||
memberCache.set(id,{data:r,ts:Date.now()});
|
memberCache.set(id,{data:r,ts:Date.now()});
|
||||||
if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value);
|
if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value);
|
||||||
return json(res,200,r,{'Cache-Control':'public, max-age=60'});
|
return json(res,200,r,{'Cache-Control':'public, max-age=60'});
|
||||||
|
|||||||
Reference in New Issue
Block a user