From a3e9d05f5dbdfa8665dadee8b50d8e20a2428329 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 17 Aug 2026 06:33:59 -0500 Subject: [PATCH] Dashboard 'My Next Step' panel + funded badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- chain.js | 10 +++++++++- public/my.html | 1 + public/my.js | 28 ++++++++++++++++++++++++++++ public/styles.css | 18 ++++++++++++++++++ server.js | 15 +++++++++++++++ 5 files changed, 71 insertions(+), 1 deletion(-) diff --git a/chain.js b/chain.js index cec8924..f572f09 100644 --- a/chain.js +++ b/chain.js @@ -101,6 +101,14 @@ const pol = wei => Number(wei / 1000000000000n) / 1e6; // 6-decimal POL const encU = v => BigInt(v).toString(16).padStart(64, '0'); 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) { // 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 }; diff --git a/public/my.html b/public/my.html index e05217a..0f9d740 100644 --- a/public/my.html +++ b/public/my.html @@ -7,6 +7,7 @@
Member position

Position #— Verified on-chain

+

Your team

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.

✓ qualified (2/2 directs) · P Premium · S Standard · ⬇ everyone below that position (all generations) and the POL they've earned · ↧ spillover = placed there by upline activity; only members who join with a position's own ID count toward its 2/2

Your pipeline

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 next upgrade comes to you.

Email me my alerts

Get an email the moment this position is paid, and when it needs an upgrade to catch incoming pay — so you never miss one. Opt in with your email; unsubscribe anytime.

diff --git a/public/my.js b/public/my.js index e851edc..49c51a1 100644 --- a/public/my.js +++ b/public/my.js @@ -99,6 +99,7 @@ document.getElementById('dIncome').innerHTML=inc.length ?`
${inc.map(p=>``).join('')}
WhenFrom memberForAmount
${date(p.ts)}#${p.fromId}${esc(p.desc)}${fmt(p.pol)} POL
` :'
No payments yet — they appear here the moment they land on-chain.
'; + renderNextStep(d); renderPipeline(d); renderAlerts(d); renderShare(d); @@ -113,6 +114,33 @@ }catch(e){return ''} } 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(LYOU':(ns.kind==='upgrade'&&L===ns.nextLevel?'NEXT':''); + return `
${tag}${nm}
`; + }).join(''); + let head='',body='',badge=''; + if(ns.kind==='qualify'){ + head='Your next step — get qualified'; + body=`You need ${ns.need} more direct${ns.need===1?'':'s'} (people who join on your link) to unlock upgrade payments. Grab your link from Share this position 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 you instead of skipping past. Upgrade with your earned POL when you can.`; + if(ns.funded===true)badge='✓ Funded — you can upgrade now'; + else if(ns.funded===false)badge='Keep building — not funded yet'; + }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=`
Your plan
${badge}
${head}

${body}

${ladder}
`; + } function renderPipeline(d){ const el=document.getElementById('dPipeline'); if(!el)return; diff --git a/public/styles.css b/public/styles.css index 2032ec5..80352d5 100644 --- a/public/styles.css +++ b/public/styles.css @@ -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 .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} +/* 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-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)} diff --git a/server.js b/server.js index cf7ee5d..933aef6 100644 --- a/server.js +++ b/server.js @@ -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()}); if(memberCache.size>500)memberCache.delete(memberCache.keys().next().value); return json(res,200,r,{'Cache-Control':'public, max-age=60'});