Files
rm-circle-team-router/public/my.js
T
martbost 747977b368 Catch eligibility: catcher needs level >= the level the buyer is LEAVING, not the level being bought
Matches _payUpline (level > levelIndex, levelIndex = buyer.level-1). The old
depth+1 rule told #21 (Culmen) it could not catch #49's Apex buy; #21 is #49's
4th matrix upline and does catch it. Fixed in the owner alert, coaching scan,
dashboard pipeline, AI prompt, leader digest, chatbot and how-pay-works copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:51:30 -05:00

731 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function(){
const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
const fmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:2});
const date=ts=>ts?new Date(ts*1000).toLocaleDateString(undefined,{year:'numeric',month:'short',day:'numeric'}):'—';
const prompt=document.getElementById('idPrompt'),dash=document.getElementById('dash');
let openDashTab=false; // which tab opens on load — set by the form/URL; default = pitch
// Two-purpose page: a "Team Build" pitch tab (for cold visitors who clicked a
// promoted link) and the "Position Dashboard" tab. Persistent — the pitch is
// always one tap away, never dismissed.
function setTab(name){
document.querySelectorAll('.mp-tab').forEach(t=>t.classList.toggle('on',t.dataset.tab===name));
const tp=document.getElementById('tabPitch'),td=document.getElementById('tabDash');
if(tp)tp.classList.toggle('on',name==='pitch');
if(td)td.classList.toggle('on',name==='dash');
}
document.querySelectorAll('.mp-tab').forEach(t=>t.addEventListener('click',()=>{setTab(t.dataset.tab);window.scrollTo({top:0,behavior:'smooth'});}));
function renderPitch(d){
const set=(id,v)=>{const el=document.getElementById(id);if(el)el.textContent=v;};
set('pInvId','#'+d.id); set('pProofId','#'+d.id);
set('pEarned',Math.round(d.totalEarnedPol||0).toLocaleString());
set('pTeam',(d.subtree&&d.subtree.downCount!=null)?String(d.subtree.downCount):'—');
set('pLevel',d.levelName||'—');
const jb=document.getElementById('pJoinBtn'); if(jb){jb.href='/join-now?ref='+d.id;jb.textContent='Join under #'+d.id+' →';}
const hj=document.getElementById('pHeroJoin'); if(hj)hj.href='/join-now?ref='+d.id;
const pp=document.getElementById('pPayouts');
if(pp){
const inc=(d.income||[]).slice(0,3);
pp.innerHTML=inc.length
? '<div class="pitch-l-lab">Real payouts landing on this team — verifiable on Polygonscan</div>'+inc.map(p=>`<div class="pitch-l-row"><span>${esc(p.desc)} — from #${p.fromId}</span><b>+${fmt(p.pol)} POL</b></div>`).join('')
: '';
}
}
function pathId(){const m=location.pathname.match(/^\/my\/(\d+)$/);if(m)return m[1];const q=new URLSearchParams(location.search).get('id');if(q&&/^\d+$/.test(q))return q;try{return localStorage.getItem('ctb.myId')||''}catch(e){return ''}}
const treeUI={byId:{},parent:{},homeId:0,rootId:0,mode:'pyramid'};
function indexTree(n,par){if(!n)return;treeUI.byId[n.id]=n;if(par)treeUI.parent[n.id]=par.id;indexTree(n.left,n);indexTree(n.right,n)}
function isSpill(n){return !!(n&&treeUI.parent[n.id]&&n.referrerId&&n.referrerId!==treeUI.parent[n.id])}
function card(n,focus){
if(!n)return '<div class="mtp-card mtp-open">open<br>slot</div>';
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
const qmark=n.directCount>=2?'<span class="mtp-qmark" title="Qualified — 2/2 directs">✓</span>':'';
const spill=isSpill(n)?`<span class="mtp-spill" title="Placed here by spillover — referred by #${n.referrerId}">↧ spillover · ref #${n.referrerId}</span>`:'';
const down=n.downCount?`<span class="mtp-down">⬇ ${n.downCount} below · ${fmt(n.downPol)} POL</span>`:'<span class="mtp-down mtp-down-none">no downline yet</span>';
return `<button class="mtp-card${focus?' mtp-focus':''}" data-tid="${n.id}">${qmark}${badge} <span class="mt-id">#${n.id}</span><span class="mtp-lvl">${esc(n.levelName)}</span><span class="mtp-sub">${n.directCount}/2 · ${fmt(n.earnedPol)} POL</span>${spill}${down}</button>`;
}
function renderPyramid(){
const out=document.getElementById('dTree'),nav=document.getElementById('dTreeNav');
const root=treeUI.byId[treeUI.rootId];
if(!root){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';nav.innerHTML='';return}
const rows=[[root]];
for(let g=1;g<4;g++)rows.push(rows[g-1].flatMap(n=>n?[n.left||null,n.right||null]:[null,null]));
if(!rows[3].some(Boolean))rows.pop();
out.innerHTML=`<div class="mtp">${rows.map((r,i)=>`<div class="mtp-row">${r.map(n=>card(n,i===0)).join('')}</div>`).join('')}</div>`;
const path=[];let cur=treeUI.rootId;
while(cur){path.unshift(cur);if(cur===treeUI.homeId)break;cur=treeUI.parent[cur]}
nav.innerHTML=path.length>1?`<span class="micro" style="margin:0">Viewing:</span> `+path.map((id,i)=>i===path.length-1?`<strong class="mt-id">#${id}</strong>`:`<button class="btn btn-secondary btn-sm" data-tid="${id}">#${id}</button>`).join(' <span style="color:var(--muted)">›</span> ')+` <button class="btn btn-secondary btn-sm" data-tid="${treeUI.parent[treeUI.rootId]}">↑ Up one</button>`:'';
const wire=el=>el.querySelectorAll('[data-tid]').forEach(b=>b.addEventListener('click',()=>{const t=Number(b.dataset.tid);if(t&&treeUI.byId[t]){treeUI.rootId=t;renderTree()}}));
wire(out);wire(nav);
}
function renderList(){
const out=document.getElementById('dTree');
document.getElementById('dTreeNav').innerHTML='';
const home=treeUI.byId[treeUI.homeId];
if(!home){out.innerHTML='<div class="empty">Team view is still indexing — check back in a few minutes.</div>';return}
const node=(n,depth)=>{
if(!n)return '';
const ch=[n.left,n.right].filter(Boolean);
const qmark=n.directCount>=2?'<span class="mtp-qmark" style="position:static;display:inline-grid;width:16px;height:16px;font-size:10px;vertical-align:middle">✓</span> ':'';
const badge=n.tier===2?'<span class="mt-badge mt-prem">P</span>':'<span class="mt-badge">S</span>';
const label=`${qmark}${badge} <span class="mt-id">#${n.id}</span> <span class="mt-meta">${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL${n.downCount?` · ⬇ ${n.downCount} below (${fmt(n.downPol)} POL)`:''}${isSpill(n)?` · <span style="color:var(--teal)">↧ spillover (ref #${n.referrerId})</span>`:''}</span>`;
const kids=ch.length?`<ul class="mt-kids">${ch.map(c=>node(c,depth+1)).join('')}</ul>`:'';
return ch.length?`<li class="mt-node"><details${depth<3?' open':''}><summary>${label}</summary>${kids}</details></li>`:`<li class="mt-node mt-leaf">${label}</li>`;
};
out.innerHTML=`<ul class="mt-tree">${node(home,0)}</ul>`;
}
function renderOrgBar(d){
const el=document.getElementById('dOrgBar');
if(!el)return;
const st=d.subtree;
if(!st||!st.downCount){el.innerHTML='';return}
let gens=0,layer=[st];
while(layer.length&&gens<60){
const next=[];
layer.forEach(n=>{if(n.left)next.push(n.left);if(n.right)next.push(n.right);});
if(!next.length)break;gens++;layer=next;
}
const q=Object.values(treeUI.byId).filter(n=>n.id!==d.id&&n.directCount>=2).length;
el.innerHTML=`<div style="display:flex;gap:10px;flex-wrap:wrap;margin:0 0 14px">`+
`<div class="fact" style="flex:1;min-width:130px"><small>Your organization</small><strong style="font-size:22px;color:var(--gold)">${st.downCount.toLocaleString()} member${st.downCount===1?'':'s'}</strong></div>`+
`<div class="fact" style="flex:1;min-width:110px"><small>Generations deep</small><strong style="font-size:22px">${gens}</strong></div>`+
`<div class="fact" style="flex:1;min-width:110px"><small>Qualified below you</small><strong style="font-size:22px">${q}</strong></div>`+
`<div class="fact" style="flex:1;min-width:140px"><small>Earned below you</small><strong style="font-size:22px;color:var(--ok)">${fmt(st.downPol)} POL</strong></div>`+
(d.polUsd>0?`<div class="fact" style="flex:1;min-width:140px"><small>≈ US dollars (POL @ $${d.polUsd<0.1?d.polUsd.toFixed(4):d.polUsd.toFixed(2)})</small><strong style="font-size:22px;color:var(--ok)">$${(st.downPol*d.polUsd).toLocaleString(undefined,{maximumFractionDigits:0})}</strong></div>`:'')+
`</div>`;
}
function renderTree(){
const tb=document.getElementById('dTreeToggle');
if(tb)tb.textContent=treeUI.mode==='pyramid'?'List view':'Pyramid view';
if(treeUI.mode==='pyramid')renderPyramid();else renderList();
}
document.getElementById('dTreeToggle').addEventListener('click',()=>{
treeUI.mode=treeUI.mode==='pyramid'?'list':'pyramid';
renderTree();
});
function render(d){
prompt.classList.add('hidden');dash.classList.remove('hidden');
// default tab: shared /my/<id> link visits open on the Team Build pitch; a
// form lookup, a returning member on bare /my, or a #dash link opens the
// dashboard (openDashTab is set by those paths before load()).
renderPitch(d);
setTab(openDashTab?'dash':'pitch');
document.getElementById('dTitle').textContent='#'+d.id;
// hand the member's id to the tools page so every promo asset arrives pre-personalized
document.querySelectorAll('a[href="/tools"]').forEach(function(a){a.href='/tools?id='+d.id;});
document.querySelectorAll('a[href="/fast-start"]').forEach(function(a){a.href='/fast-start?id='+d.id;});
document.querySelectorAll('a[href="/weekly-rhythm"]').forEach(function(a){a.href='/weekly-rhythm?id='+d.id;});
document.querySelectorAll('a[href="/flyers"]').forEach(function(a){a.href='/flyers?id='+d.id;});
const qb=document.getElementById('dQualified');
if(qb)qb.outerHTML=d.directCount>=2
?'<span id="dQualified" class="q-badge q-yes">★ Qualified</span>'
:`<span id="dQualified" class="q-badge q-no">${2-d.directCount} more direct${2-d.directCount===1?'':'s'} to qualify</span>`;
document.getElementById('dFacts').innerHTML=
`<div class="fact"><small>Tier</small><strong>${esc(d.tierName)}</strong></div>`+
`<div class="fact"><small>Level</small><strong>${esc(d.levelName)}</strong></div>`+
`<div class="fact"><small>Directs</small><strong>${d.directCount}/2${d.directCount>=2?' ✓ qualified':''}</strong></div>`+
`<div class="fact"><small>Total received</small><strong style="color:var(--ok)">${fmt(d.totalEarnedPol)} POL</strong></div>`+
`<div class="fact"><small>Team below you</small><strong>${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}</strong></div>`+
`<div class="fact"><small>Member since</small><strong>${date(d.joinedAt)}</strong></div>`;
treeUI.byId={};treeUI.parent={};treeUI.homeId=d.id;treeUI.rootId=d.id;
indexTree(d.subtree,null);
renderOrgBar(d);
renderTree();
// spillover explainer — shown exactly when the confusing state exists:
// positions sitting under you while you're not yet qualified
const note=document.getElementById('dSpillNote');
if(note){
const kids=d.subtree?[d.subtree.left,d.subtree.right].filter(Boolean):[];
const spills=kids.filter(k=>k.referrerId&&k.referrerId!==d.id).length;
if(d.directCount<2&&kids.length&&spills){
note.innerHTML=`<div class="callout" style="margin-top:14px"><strong>Why am I not qualified when people sit under me?</strong> ${spills===kids.length?'The positions':'Some positions'} under you arrived by <strong>spillover</strong> — when your upline recruits, the contract fills the next open slot downward, which can land members in your matrix. Spillover grows your team and your future level payments, but qualification only counts your <strong>directs</strong> — people who join using <em>your</em> ID. You have ${d.directCount}/2 directs; share your link above to get ${2-d.directCount===1?'your last one':'your 2'}.</div>`;
note.classList.remove('hidden');
}else{note.classList.add('hidden');note.innerHTML='';}
}
const inc=d.income||[];
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="empty">No payments yet — they appear here the moment they land on-chain.</div>';
// each panel renders independently — one panel's error must never blank
// the ones after it (bit us: share-card QR vanished behind an earlier throw)
[renderNextStep,renderUpgradeCTA,renderGens,renderPipeline,renderCoach,renderMessages,renderAlerts,renderShare].forEach(function(f){try{f(d);}catch(e){try{console.error('panel',f.name,e);}catch(_){}}});
document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length
?`<strong style="color:var(--text)">#${d.id}</strong> → ${d.uplineChain.map(i=>'#'+i).join(' → ')} <span style="color:#8498aa">(root)</span>`
:'You are at the top of your line.';
}
function qrSvg(url){
try{
const q=qrcode(0,'M');q.addData(url);q.make();
return q.createSvgTag({cellSize:4,margin:3,scalable:true});
}catch(e){return ''}
}
const LEVELS=['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona'];
// Team depth — members per generation below this position, with the level
// whose upgrade routes each generation's payment to this position (gen D
// pays on the upgrade OUT of level D, i.e. buying level D+1).
function renderGens(d){
const el=document.getElementById('dGens');if(!el)return;
const root=d.subtree;
if(!root||(!root.left&&!root.right)){el.innerHTML='';return;}
const gens=[];let layer=[root];
while(layer.length&&gens.length<60){
const next=[];
layer.forEach(n=>{if(n.left)next.push(n.left);if(n.right)next.push(n.right);});
if(!next.length)break;gens.push(next.length);layer=next;
}
const rows=gens.map((c,i)=>{
const cap=Math.pow(2,i+1);
const pays=i<7?`pays you at their <strong style="color:var(--text)">${LEVELS[i+1]}</strong> upgrade`:'beyond the 8 pay levels';
return `<div style="display:flex;align-items:center;gap:10px;margin:5px 0;font-size:13px;flex-wrap:wrap"><span style="width:46px;color:var(--muted)">Gen ${i+1}</span><div class="progress-line" style="flex:1;min-width:90px;height:9px"><span style="width:${Math.min(100,Math.round(100*c/cap))}%"></span></div><span style="width:88px;text-align:right"><strong>${c}</strong><span style="color:var(--muted)">${cap<=1024?' of '+cap:''}</span></span><span class="micro" style="color:var(--muted);width:230px">${pays}</span></div>`;
}).join('');
el.innerHTML=`<div style="border-top:1px solid var(--line);padding-top:12px"><small style="text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-size:11px">Team depth — members per generation</small><div style="margin-top:8px">${rows}</div><p class="micro" style="margin:8px 0 0">Full generations duplicate: each one can hold twice the last. A generation pays this position at exactly one level — stay qualified and at that level to catch it.</p></div>`;
}
// On-page upgrade: connect the wallet that OWNS this position, read the
// exact next-level cost from the contract (BigInt — never float-derived),
// send upgrade(). Wrong value would simply revert; ownership is enforced by
// matching the connected account to the position's on-chain account.
const UPG={CONTRACT:'0x33bdaeefd6d17d80ae53816c916dfb26c4fb2daf',POLYGON:'0x89',SEL_UPGRADE:'0xd55ec697',SEL_GETCOSTS:'0x735f87b9'};
let upEth=null;
function renderUpgradeCTA(d){
const el=document.getElementById('dUpgrade');if(!el)return;
const ns=d.nextStep||{};
if(ns.kind!=='upgrade'||d.directCount<2){el.innerHTML='';return;}
const name=LEVELS[ns.nextLevel-1]||('Level '+ns.nextLevel);
// What this rung buys, in their own numbers. A price with no reason attached
// is just a bill — this is the reason.
const op=ns.opens||{};
let why='';
if(op.perCatch>0){
const gname=LEVELS[op.gen]||('Level '+(op.gen+1));
const have=(op.genSize!=null&&op.genSize>0)
? `You already have <strong style="color:var(--text)">${op.genSize} ${op.genSize===1?'person':'people'}</strong> there.`
: `That generation is still filling.`;
why=`<div style="background:rgba(78,214,203,.07);border:1px solid rgba(78,214,203,.25);border-radius:12px;padding:13px 15px;margin:0 0 12px;font-size:13.5px;line-height:1.6">
<strong style="color:var(--teal)">What this unlocks:</strong> ${esc(name)} extends your reach to
<strong style="color:var(--text)">generation ${op.gen}</strong>. ${have}
Each of them pays you <strong style="color:var(--gold)">${fmt(op.perCatch)} POL</strong> when they buy ${esc(gname)}.
<div style="margin-top:7px;color:var(--muted)">Your first catch at this level is <strong style="color:var(--text)">${fmt(op.perCatch)} POL</strong> —
exactly <strong style="color:var(--text)">double</strong> what this upgrade costs, because every rung is twice the last.
That is the whole idea behind <a href="/training#lesson-8" style="color:var(--teal)">Lesson 8 — time your upgrades to your catches</a>.</div>
</div>`;
}
el.innerHTML=`<div class="table-card" style="margin:0 0 18px;border-color:rgba(240,197,109,.45)">
<h2 style="margin:0 0 4px">⬆️ Upgrade to ${esc(name)} — right from this page</h2>
${why}
<p style="color:var(--muted);font-size:13px;margin:0 0 12px">Exact cost <strong>${fmt(ns.cost)} POL</strong>, read live from the contract. Connect the wallet that owns position #${d.id}, confirm once, done. ${ns.funded?'<strong style="color:var(--ok)">Your wallet already covers it ✓</strong>':'Your wallet does not cover it yet — you can time it to your next catches, or top up below.'}</p>
<button id="upgGo" class="btn btn-primary">Connect &amp; upgrade to ${esc(name)} →</button>
${ns.funded?'':`<button id="upgFund" class="btn btn-secondary" style="margin-left:8px">💳 Buy POL with card</button>`}
<div id="upgHelp" style="display:none;margin-top:10px" class="micro">📱 On a phone with no wallet in this browser? Open this page inside your wallet app: <a id="upgMM" href="#">MetaMask</a> · <a id="upgTW" href="#">Trust Wallet</a></div>
<div id="upgLog" class="micro" style="margin-top:10px"></div>
<p class="micro" style="margin:10px 0 0">Upgrades are optional and self-paced — never use funds you can't afford to lose. If anything is off, the contract simply rejects it and you keep your POL (minus tiny gas).</p>
</div>`;
const go=document.getElementById('upgGo');if(go)go.addEventListener('click',function(){doUpgrade(d,ns);});
const fu=document.getElementById('upgFund');if(fu)fu.addEventListener('click',async function(){
try{const need=Math.max(62,Math.ceil(ns.cost- (0))+5);const r=await(await fetch('/api/public/moonpay-url?address='+encodeURIComponent(d.account||'')+'&pol='+need)).json();if(r&&r.url)window.open(r.url,'_blank','noopener');}catch(e){}
});
}
function upgLog(msg,bad){const l=document.getElementById('upgLog');if(l){l.innerHTML=msg;l.style.color=bad?'var(--danger)':'var(--muted)';}}
async function doUpgrade(d,ns){
try{
upgLog('Looking for your wallet…');
upEth=await window.RMCWallet.pick();
if(!upEth){
const h=document.getElementById('upgHelp');if(h){h.style.display='block';
const here=location.host+location.pathname+location.search;
document.getElementById('upgMM').href='https://metamask.app.link/dapp/'+here;
document.getElementById('upgTW').href='https://link.trustwallet.com/open_url?coin_id=966&url='+encodeURIComponent(location.href);}
upgLog('No wallet in this browser — use the wallet-app links above, or install MetaMask on desktop.',true);return;
}
const accs=await upEth.request({method:'eth_requestAccounts'});const account=(accs[0]||'').toLowerCase();
if(!d.account||account!==String(d.account).toLowerCase()){
upgLog('This wallet ('+account.slice(0,6)+'…'+account.slice(-4)+') does not own position #'+d.id+' — connect the wallet that registered it ('+String(d.account||'').slice(0,6)+'…'+String(d.account||'').slice(-4)+').',true);return;
}
// network
const cid=await upEth.request({method:'eth_chainId'});
if(cid!==UPG.POLYGON){
upgLog('Switching to Polygon…');
try{await upEth.request({method:'wallet_switchEthereumChain',params:[{chainId:UPG.POLYGON}]});}
catch(e){if(e&&e.code===4902){await upEth.request({method:'wallet_addEthereumChain',params:[{chainId:UPG.POLYGON,chainName:'Polygon Mainnet',nativeCurrency:{name:'POL',symbol:'POL',decimals:18},rpcUrls:['https://polygon-rpc.com'],blockExplorerUrls:['https://polygonscan.com']}]});}else{throw e;}}
}
// exact cost from the contract (BigInt). Costs are immutable constants,
// so if the WALLET's internal RPC hiccups (MetaMask iOS: "Load failed")
// we fall back to the verified baked table instead of failing the flow.
upgLog('Reading the exact upgrade cost from the contract…');
const UP_STD=[310701203558346391552n,621402407116692783104n,1242804814233385566208n,2485609628466771132416n,4971219256933542264832n,9942438513867084529664n,19884877027734169059328n,0n];
const UP_PREM=[621402407116692783104n,1242804814233385566208n,2485609628466771132416n,4971219256933542264832n,9942438513867084529664n,19884877027734169059328n,39769754055468338118656n,0n];
let arr;
try{
const r=await upEth.request({method:'eth_call',params:[{to:UPG.CONTRACT,data:UPG.SEL_GETCOSTS},'latest']});
const words=[];for(let i=2;i<r.length;i+=64)words.push(BigInt('0x'+r.slice(i,i+64)));
arr=(d.tier===2)?words.slice(24,32):words.slice(16,24);
}catch(e){ arr=(d.tier===2)?UP_PREM:UP_STD; }
const val=arr[(d.level||1)-1];
if(!val||val<=0n){upgLog('Could not read the upgrade cost — try again in a minute.',true);return;}
upgLog('Upgrading to '+esc(LEVELS[ns.nextLevel-1]||'')+' for '+(Number(val)/1e18).toFixed(4)+' POL — confirm in your wallet…');
const tx=await upEth.request({method:'eth_sendTransaction',params:[{from:account,to:UPG.CONTRACT,value:'0x'+val.toString(16),data:UPG.SEL_UPGRADE}]});
upgLog('Submitted — waiting for confirmation… <a href="https://polygonscan.com/tx/'+tx+'" target="_blank" rel="noopener">view tx</a>');
for(let i=0;i<60;i++){
try{const rc=await upEth.request({method:'eth_getTransactionReceipt',params:[tx]});
if(rc){if(rc.status==='0x0'){upgLog('⚠️ The upgrade reverted — your POL was returned (minus gas). Wait a moment and try again.',true);return;}
upgLog('🎉 <strong>Upgraded!</strong> Refreshing your dashboard…');try{sessionStorage.setItem('rmc.fresh','1');}catch(e){}setTimeout(function(){location.reload();},3000);return;}
}catch(e){}
await new Promise(function(s){setTimeout(s,3000);});
}
upgLog('Still pending — it will land shortly; refresh in a minute.');
}catch(e){upgLog('Upgrade failed / rejected: '+esc(e.message||String(e)),true);}
}
// Wallet-verified messaging: sign-in = one free personal_sign; identity is
// the wallet that owns a position; permissions follow the matrix lines.
function renderMessages(d){
const el=document.getElementById('dMsg'),bell=document.getElementById('msgBell');
if(!el)return;
fetch('/api/public/msg-unread?id='+d.id).then(r=>r.json()).then(u=>{
if(bell&&u&&u.count>0){bell.textContent='🔔 '+u.count+' new';bell.classList.remove('hidden');}
}).catch(()=>{});
loadMsgUI(d);
}
async function loadMsgUI(d){
const el=document.getElementById('dMsg');
let me=null;
try{const r=await fetch('/api/public/msg-me');if(r.ok)me=await r.json();}catch(e){}
if(!me){
el.innerHTML='<p class="micro" style="margin:0 0 10px">Sign in once with the wallet that owns your position — one free signature; it can\'t move funds or approve anything.</p><button id="msgAuthBtn" class="btn btn-primary">🔐 Connect wallet &amp; sign in</button><div id="msgAuthErr" class="micro" style="color:var(--danger);margin-top:8px"></div>';
const b=document.getElementById('msgAuthBtn');if(b)b.addEventListener('click',function(){msgAuth(d);});
return;
}
let data;
try{data=await(await fetch('/api/public/msg-inbox')).json();}catch(e){el.innerHTML='<div class="empty">Could not load messages — refresh to retry.</div>';return;}
const mine=Number(me.id)===Number(d.id);
const banner=mine?'':`<div class="callout" style="margin-bottom:10px">You're signed in as <strong>#${me.id}</strong> — this inbox is yours. (You're viewing #${d.id}'s page; the "to" box is pre-filled for them.)</div>`;
const rows=(data.inbox||[]).map(m=>`<div class="pp-row" style="padding:9px 12px${m.read?'':';border-color:rgba(240,197,109,.55)'}"><div class="pp-icon">${m.org?'📣':'✉️'}</div><div class="pp-body"><strong>From #${m.fromId}</strong> <span class="pp-meta" style="display:inline">· ${new Date(m.ts).toLocaleString()}${m.org?' · team broadcast':''}${m.read?'':' · <strong style="color:var(--gold)">NEW</strong>'}</span><div style="white-space:pre-wrap;margin-top:4px">${esc(m.body)}</div></div></div>`).join('')||'<div class="empty">No messages yet.</div>';
const sent=(data.sent||[]).slice(0,3).map(m=>`<div class="micro" style="margin:3px 0">→ ${m.org?'whole team':'#'+m.toId} · ${new Date(m.ts).toLocaleString()}: ${esc(m.body.slice(0,90))}${m.body.length>90?'…':''}</div>`).join('');
el.innerHTML=banner+rows+
(sent?`<div style="margin-top:10px"><span class="micro" style="text-transform:uppercase;letter-spacing:.08em">Recently sent</span>${sent}</div>`:'')+
`<div style="border-top:1px solid var(--line);margin-top:12px;padding-top:12px"><div style="font-weight:800;margin-bottom:6px">Send a message</div>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:8px">
<input id="msgTo" inputmode="numeric" placeholder="Member #" value="${mine?'':esc(String(d.id))}" style="max-width:110px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:#08192880;color:var(--text);font-size:14px">
<label class="micro" style="display:flex;gap:6px;align-items:center;cursor:pointer"><input type="checkbox" id="msgOrg"> send to my whole team instead</label>
</div>
<textarea id="msgBody" maxlength="1500" rows="3" placeholder="Plain text, up to 1500 characters. You can message your team and your upline." style="width:100%;padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:#08192880;color:var(--text);font-size:14px"></textarea>
<div style="display:flex;gap:10px;align-items:center;margin-top:8px"><button id="msgSendBtn" class="btn btn-primary">Send →</button><span id="msgStatus" class="micro"></span></div></div>`;
const unreadIds=(data.inbox||[]).filter(m=>!m.read).map(m=>m.mid);
if(unreadIds.length)fetch('/api/public/msg-read',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({mids:unreadIds})}).catch(()=>{});
const sb=document.getElementById('msgSendBtn');
if(sb)sb.addEventListener('click',async function(){
const st=document.getElementById('msgStatus');st.textContent='Sending…';sb.disabled=true;
try{
const payload={org:document.getElementById('msgOrg').checked,toId:(document.getElementById('msgTo').value||'').trim(),body:document.getElementById('msgBody').value};
const r=await(await fetch('/api/public/msg-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})).json();
if(r.error){st.textContent=r.error;sb.disabled=false;return;}
st.textContent='Sent ✓';setTimeout(function(){loadMsgUI(d);},700);
}catch(e){st.textContent='Send failed — try again.';sb.disabled=false;}
});
// Telegram companion link — payout pings + team messages in Telegram
try{
const foot=document.createElement('div');
foot.style.cssText='margin-top:12px;padding-top:10px;border-top:1px solid var(--line, #24425d)';
// Already connected? Say so and stop asking. Offering to connect someone
// who IS connected is how a working link looks broken.
if(data&&data.tgLinked){
foot.innerHTML='<div class="micro" style="color:var(--teal)">✈️ <b>Telegram connected.</b> '
+'Payout pings, team messages and join alerts arrive in your chat with the bot. '
+'Open the bot and tap ☰ for your dashboard inside Telegram.</div>';
el.appendChild(foot);
return;
}
foot.innerHTML='<button id="tgLinkBtn" class="btn btn-secondary btn-sm">✈️ Connect Telegram — payout pings + team chat</button><span class="micro" id="tgLinkNote" style="margin-left:8px"></span>';
el.appendChild(foot);
const tb=document.getElementById('tgLinkBtn');
tb.addEventListener('click',async function(){
tb.disabled=true;
const note=document.getElementById('tgLinkNote');
try{
const r=await(await fetch('/api/public/tg-link',{method:'POST'})).json();
if(!r.url){ note.textContent=r.error||'Try again shortly.'; tb.disabled=false; return; }
// Do NOT call window.open here. Wallet DApp browsers (Trust, SafePal,
// MetaMask) block programmatic window.open, so the call silently does
// nothing and the member is left looking at an unchanged screen with
// no idea anything happened. A real anchor the member taps is handled
// by the OS, which is what actually launches the Telegram app —
// and a copy button covers the webviews that refuse even that.
note.textContent='';
foot.insertAdjacentHTML('beforeend',
'<div id="tgLinkOut" style="margin-top:10px;line-height:1.7">' +
'<a id="tgOpen" class="btn btn-primary btn-sm" href="'+r.url+'" target="_blank" rel="noopener">✈️ Open Telegram to finish</a> ' +
'<button id="tgCopy" class="btn btn-secondary btn-sm">Copy the link</button>' +
'<div class="micro" id="tgCopied" style="margin-top:7px">Tap the button above, then press <b>START</b> in Telegram. ' +
'If nothing opens, copy the link and paste it into Telegram instead.</div></div>');
tb.style.display='none';
document.getElementById('tgCopy').addEventListener('click',function(){
const done=function(){document.getElementById('tgCopied').textContent='Copied — paste it into Telegram, then press START.';};
if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(r.url).then(done,function(){fb();});}
else fb();
function fb(){
const t=document.createElement('textarea');
t.value=r.url; t.style.cssText='position:fixed;left:0;top:0;opacity:0';
document.body.appendChild(t); t.focus(); t.select();
try{document.execCommand('copy');done();}catch(e){}
setTimeout(function(){document.body.removeChild(t);},100);
}
});
}catch(e){note.textContent='Network hiccup — try again.';tb.disabled=false;}
});
}catch(e){}
}
async function msgAuth(d){
const err=document.getElementById('msgAuthErr');
try{
const eth=await window.RMCWallet.pick();
if(!eth){{var _ia=(window.RMCInApp&&window.RMCInApp.notice('/my'))||'';if(_ia){err.innerHTML=_ia;if(err.style)err.style.display='block';window.RMCInApp.bind('/my');}else{err.textContent='This browser has no wallet in it. Open this page INSIDE your wallet app instead: open Trust, MetaMask, SafePal or Coinbase, find its Browser (or DApps) tab, and type rmcircle.team/my into that address bar. On iPhone, Trust no longer has a browser — use MetaMask or SafePal, or ask your sponsor to link you instead.';}}return;}
const accs=await eth.request({method:'eth_requestAccounts'});const account=accs[0];
// Match the wallet's active chain to the SIWE message's Chain ID (137),
// or the wallet rejects the signature ("chain ID does not match").
try{await window.RMCWallet.ensureChain(eth);}catch(ce){err.textContent='Please switch your wallet to the Polygon network, then tap Connect again. (Your wallet blocks sign-in when it\'s on a different network.)';return;}
const ch=await(await fetch('/api/public/msg-challenge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address:account})})).json();
if(!ch.message)throw new Error(ch.error||'Could not start sign-in.');
let hex='0x';for(const b of new TextEncoder().encode(ch.message))hex+=b.toString(16).padStart(2,'0');
const sig=await eth.request({method:'personal_sign',params:[hex,account]});
const v=await(await fetch('/api/public/msg-verify',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({address:account,signature:sig})})).json();
if(!v.ok)throw new Error(v.error||'Verification failed.');
loadMsgUI(d);
}catch(e){if(err)err.textContent=e.message||String(e);}
}
// "Coach your team" — the same triage the team admin runs, scoped to THIS
// position's leg: who below could use a nudge, and exactly what to tell them.
// One-tap nudges: pre-written message per coach recommendation, sent over
// the wallet-verified Messages rails (and bridged to Telegram if linked).
const nudgeTexts={};
// Nudges used to send silently with only a "Delivered" note, so nobody could
// see WHAT went out or to whom. A member went hunting in her clipboard, found
// the unrelated "copy my summary" text still sitting there, and reasonably
// concluded the nudge had sent her own position instead of the recipient's.
// Preview before, and the exact sent text after, removes the guesswork.
function nudgeBtn(key){return `<div class="coach-nudge-wrap" style="margin-top:7px">`+
`<button class="btn btn-teal btn-sm coach-nudge" data-nkey="${key}">📨 Send this nudge</button>`+
`<button class="btn btn-secondary btn-sm coach-nudge-preview" data-nkey="${key}" style="margin-left:6px">👁 Preview</button>`+
`<span class="micro coach-nudge-note" style="display:none;margin-left:8px"></span>`+
`<div class="coach-nudge-prev" style="display:none;margin-top:8px;background:rgba(78,214,203,.06);`+
`border:1px solid rgba(78,214,203,.28);border-radius:10px;padding:10px 12px;font-size:12.5px;`+
`line-height:1.6;color:var(--muted);white-space:pre-wrap"></div></div>`}
function regNudge(id,kind,text){const key=id+':'+kind;nudgeTexts[key]={to:id,text};return nudgeBtn(key)}
function renderCoach(d){
const card=document.getElementById('dCoachCard'),el=document.getElementById('dCoach');
if(!card||!el)return;
const c=d.coach;
if(!c||!c.ready||!c.scanned||((c.rollForward||[]).length+(c.atRisk||[]).length+(c.oneAway||[]).length)===0){card.style.display='none';return;}
const link=id=>`<a href="/my/${id}" style="color:var(--teal)">#${id}</a>`;
const rows=[];
(c.rollForward||[]).forEach(r=>rows.push(`<div class="pp-row" style="padding:9px 12px"><div class="pp-icon">💬</div><div class="pp-body">${link(r.id)} is <strong>qualified but still Scintilla</strong> with ${fmt(r.earnedPol)} POL of entry rewards — their Ascensus upgrade (${fmt(r.ascensusCost)} POL) is already covered and catches their team's first payments. Tell them — then show them how you spotted it, so they can spot it for their two.<br><span class="micro">📚 The Method play: send them <a href="/training#lesson-8" style="color:var(--teal)">Lesson 8 — Timing Upgrades to Catches</a></span>${regNudge(r.id,'roll',`Great news from my coaching panel: your Ascensus upgrade (~${Math.round(r.ascensusCost)} POL) is already fully covered by the ${Math.round(r.earnedPol)} POL of entry rewards you've caught. Upgrading now means your team's first payments land on YOU instead of passing by. The how and why is Lesson 8: https://rmcircle.team/training#lesson-8 — and after you upgrade, open the "Coach your team" card on YOUR dashboard and run this same check for your two. That's the whole system.`)}</div></div>`));
(c.atRisk||[]).filter(r=>r.qualified&&r.level>1).forEach(r=>rows.push(`<div class="pp-row" style="padding:9px 12px"><div class="pp-icon">⚠️</div><div class="pp-body">${link(r.id)} (${esc(r.levelName)}) has <strong style="color:var(--gold)">${fmt(r.atRiskPol)} POL forming</strong> below them but needs <strong>${esc(r.neededLevelName)}</strong> to catch it — worth a heads-up before it passes them.<br><span class="micro">📚 The Method play: send them <a href="/training#lesson-8" style="color:var(--teal)">Lesson 8 — Timing Upgrades to Catches</a></span>${regNudge(r.id,'arq',`Friendly heads-up from my coaching panel: about ${Math.round(r.atRiskPol)} POL is forming below you, but it needs ${r.neededLevelName} to catch when it arrives — you have time to get ahead of it. How the timing works is Lesson 8: https://rmcircle.team/training#lesson-8. Your own dashboard (https://rmcircle.team/my/${r.id}) shows the same numbers on the pipeline panel — and show your two how to read theirs!`)}</div></div>`));
(c.atRisk||[]).filter(r=>!r.qualified).forEach(r=>rows.push(`<div class="pp-row" style="padding:9px 12px"><div class="pp-icon">⏰</div><div class="pp-body">${link(r.id)} has <strong style="color:var(--gold)">${fmt(r.atRiskPol)} POL forming</strong> below but isn't qualified yet (${r.directCount}/2) — help them find their ${r.directCount===1?'last direct':'2 directs'}.<br><span class="micro">📚 The Method play: send them <a href="/training#lesson-2" style="color:var(--teal)">Lesson 2 — Your Warm List</a></span>${regNudge(r.id,'arn',`Money is already forming below you (about ${Math.round(r.atRiskPol)} POL) — it just can't land until you're qualified (you're at ${r.directCount}/2 directs). You're ${r.directCount===1?'one good conversation':'two good conversations'} away. Lesson 2 makes the warm list painless: https://rmcircle.team/training#lesson-2 — and I'm happy to help you work it. Reply here anytime.`)}</div></div>`));
(c.oneAway||[]).forEach(r=>rows.push(`<div class="pp-row" style="padding:9px 12px"><div class="pp-icon">🎯</div><div class="pp-body">${link(r.id)} is <strong>one direct away</strong> from qualifying — introduce them to one good person and their whole position activates.<br><span class="micro">📚 The Method play: send them <a href="/training#lesson-3" style="color:var(--teal)">Lesson 3 — The Conversation</a></span>${regNudge(r.id,'one',`You're ONE direct away from qualifying — one person, and your whole position activates. Lesson 3 gives you the exact conversation, word for word: https://rmcircle.team/training#lesson-3. Want to pick who to talk to first together? Reply here — I've got you.`)}</div></div>`));
if(!rows.length){card.style.display='none';return;}
el.innerHTML=rows.slice(0,10).join('')+`<p class="micro" style="margin:10px 0 0"><strong style="color:var(--text)">The team rule: don't just tell them — teach them to coach.</strong> Everyone on this list has this same panel on their own dashboard. When you reach out, walk them through THEIR "Coach your team" card so they run this exact play for their two. You're not just coaching two people — you're teaching two coaches. That's what builds depth that pays. And remember: every upgrade below you either pays your position directly or builds the depth that will. Not sure how to walk someone through their dashboard? <a href="/training" style="color:var(--teal)">Video 7 in the training</a> is the full tour — send it.</p>`;
card.style.display='';
if(!el.__nudgeBound){
el.__nudgeBound=true;
el.addEventListener('click',async ev=>{
const pv=ev.target.closest&&ev.target.closest('button.coach-nudge-preview');
const btn=ev.target.closest&&ev.target.closest('button.coach-nudge');
if(!pv&&!btn)return;
const rec=nudgeTexts[(pv||btn).dataset.nkey];if(!rec)return;
const wrap=(pv||btn).closest('.coach-nudge-wrap');
const note=wrap&&wrap.querySelector('.coach-nudge-note');
const prev=wrap&&wrap.querySelector('.coach-nudge-prev');
const showPrev=(label)=>{if(!prev)return;prev.textContent=label+'\n\n'+rec.text;prev.style.display='';};
// Preview: show exactly what will be sent, and to whom, before sending.
if(pv){
if(prev&&prev.style.display!=='none'){prev.style.display='none';pv.textContent='\u{1F441} Preview';}
else{showPrev('This will be sent to #'+rec.to+':');pv.textContent='\u{1F441} Hide preview';}
return;
}
const say=(t,color)=>{if(note){note.style.display='';note.style.color=color||'var(--muted)';note.textContent=t;}};
btn.disabled=true;const old=btn.textContent;btn.textContent='Sending…';
try{
const me=await fetch('/api/public/msg-me');
if(me.status===401){
btn.textContent=old;btn.disabled=false;
say('Sign in to Messages first (one free wallet signature) — then tap again.','var(--gold)');
const mc=document.getElementById('dMsg');if(mc&&mc.scrollIntoView)mc.scrollIntoView({behavior:'smooth',block:'center'});
return;
}
const r=await fetch('/api/public/msg-send',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({toId:rec.to,body:rec.text})});
const dd=await r.json().catch(()=>({}));
if(r.ok&&!dd.error){btn.textContent='Sent ✓';showPrev('✅ Sent to #'+rec.to+' — this is exactly what they received:');const pb=wrap&&wrap.querySelector('.coach-nudge-preview');if(pb)pb.style.display='none';say('Delivered to #'+rec.to+'’s dashboard — and straight to their Telegram if they’ve linked it.');}
else{btn.textContent=old;btn.disabled=false;say(dd.error||'Could not send — try again.','var(--danger)');}
}catch(e){btn.textContent=old;btn.disabled=false;say('Network hiccup — try again.','var(--danger)');}
});
}
}
// "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;
// Per-level upgrade cost: upgradeCosts[tier][k] = POL to go from level k+1
// to k+2, so the cost to REACH level L is index L-2. Scintilla (L1) is the
// entry level, not an upgrade.
const upc=(d.upgradeCosts&&d.upgradeCosts[d.tier===2?2: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>':'');
const cost=L===1?null:upc[L-2];
const costTxt=L===1?'entry level':(cost?fmt(cost)+' POL':'');
const usd=(cost&&d.polUsd>0)?' title="≈ $'+Math.round(cost*d.polUsd).toLocaleString()+' to reach '+nm+'"':'';
const costLine=costTxt?`<span class="nlvc"${usd}>${costTxt}</span>`:'';
return `<div class="${cls}">${tag}<span class="nlvn">${nm}</span>${costLine}</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><p class="micro" style="margin:10px 0 0"><a href="/how-pay-works#level-scale" style="color:var(--teal)">See what each level opens up →</a></p></div>`;
}
function renderPipeline(d){
const el=document.getElementById('dPipeline');
if(!el)return;
if(!d.subtree||(!d.subtree.left&&!d.subtree.right)){el.innerHTML='<div class="empty">Your pipeline starts when your first team members are placed below you.</div>';return}
if(!d.upgradeCosts){el.innerHTML='<div class="empty">Pipeline data is loading — check back in a minute.</div>';return}
const up=d.upgradeCosts;
// walk the subtree with depth: a member at depth D pays THIS position when
// they buy the upgrade OUT of level D (cost index D-1) — first in line
const ready=[],building=[],passedCount={n:0};
(function walk(n,depth){
if(!n||depth>16)return;
const lvl=n.level||1,tier=n.tier===2?2:1;
if(depth>=1){
if(lvl===depth){
const amt=(up[tier]||[])[depth-1];
// catcher needs level >= the level the buyer is LEAVING (= depth), per _payUpline
if(amt)ready.push({id:n.id,depth,tier,amt,toLevel:LEVELS[depth]||('Level '+(depth+1)),needLevel:depth});
}else if(lvl<depth)building.push({id:n.id,depth,steps:depth-lvl});
else passedCount.n++;
}
walk(n.left,depth+1);walk(n.right,depth+1);
})(d.subtree,0);
ready.sort((a,b)=>a.depth-b.depth||a.id-b.id);
const eligible=r=>d.directCount>=2&&(d.level||1)>=r.needLevel;
const readyTotal=ready.filter(eligible).reduce((s,r)=>s+r.amt,0);
let html='';
if(ready.length){
html+=`<p style="margin:0 0 8px;font-weight:800;color:var(--ok)">Ready now — one upgrade from your wallet:</p>`;
html+=ready.slice(0,10).map(r=>{
const warn=!eligible(r)?(d.directCount<2?' <span style="color:var(--danger)">⚠ needs you qualified (2 directs)</span>':` <span style="color:var(--danger)">⚠ needs you at ${esc(LEVELS[r.needLevel-1])}+</span>`):'';
return `<div class="pp-row" style="padding:10px 14px"><div class="pp-icon">⏳</div><div class="pp-body"><strong>#${r.id}</strong> <span class="pp-meta" style="display:inline">· gen ${r.depth} · their ${esc(r.toLevel)} upgrade pays you <strong style="color:var(--gold)">${fmt(r.amt)} POL</strong>${warn}</span></div></div>`;
}).join('');
if(ready.length>10)html+=`<p class="micro" style="margin:6px 0 0">…and ${ready.length-10} more.</p>`;
if(readyTotal>0){
const myNext=(up[d.tier===2?2:1]||[])[(d.level||1)-1];
const fundNote=myNext?` Your next upgrade (${esc(LEVELS[d.level]||'max')}) costs ${fmt(myNext)} — ${readyTotal>=myNext?'the pipeline above covers it':'these payments put you '+fmt(readyTotal)+' toward it'}.`:'';
html+=`<p style="margin:12px 0 0;color:var(--muted)"><strong style="color:var(--text)">${fmt(readyTotal)} POL</strong> is one upgrade away from your wallet.${fundNote}</p>`;
}
}else html+='<p style="margin:0 0 8px;color:var(--muted)">Nobody is at their pay-you milestone yet — the members below are still climbing toward it.</p>';
if(building.length)html+=`<p class="micro" style="margin:10px 0 0">${building.length} more member${building.length===1?' is':'s are'} building toward their pay-you level${passedCount.n?`; ${passedCount.n} already passed theirs`:''}. Helping your leg upgrade IS your income.</p><p class="micro" style="margin:8px 0 0">💡 <strong style="color:var(--text)">Someone below you not upgrading?</strong> That's fine — upgrades are optional, and a paused member never blocks money: payments they can't catch pass straight UP to the next ready position (often yours), and everyone below them keeps flowing normally. Their pause only delays the single payment they themselves would send. Coach the movers; the pausers usually return once their own entry rewards stack up and the upgrade funds itself. <a href="/training#lesson-7" style="color:var(--teal)">Lesson 7 covers this in three minutes.</a></p>`;
else if(passedCount.n)html+=`<p class="micro" style="margin:10px 0 0">${passedCount.n} member${passedCount.n===1?' has':'s have'} already passed their pay-you milestone.</p>`;
html+=`<div style="margin-top:14px;display:flex;gap:8px;flex-wrap:wrap;align-items:center"><button id="dCopySummary" class="btn btn-secondary btn-sm">📋 Copy plain-English summary</button><span id="dCopyMsg" class="micro" style="margin:0"></span></div>`;
el.innerHTML=html;
const cs=document.getElementById('dCopySummary');
if(cs)cs.addEventListener('click',async()=>{
try{await navigator.clipboard.writeText(pipelineSummaryText(d));document.getElementById('dCopyMsg').textContent='Copied — paste it into your chat.';setTimeout(()=>{document.getElementById('dCopyMsg').textContent=''},4000);}catch(e){document.getElementById('dCopyMsg').textContent='Copy failed — long-press to select instead.'}
});
}
// Plain-English, chat-ready explainer of a member's upgrade pipeline — the
// "will I miss upgrades?" question answered from their real position.
function pipelineSummaryText(d){
const LV=LEVELS, up=(d.upgradeCosts&&d.upgradeCosts[d.tier===2?2:1])||[];
const money=n=>Math.round(n).toLocaleString();
const gens={};
(function walk(n,depth){if(!n||depth>8)return;if(depth>=1)(gens[depth]=gens[depth]||[]).push(n);walk(n.left,depth+1);walk(n.right,depth+1);})(d.subtree,0);
const myLevel=d.level||1, team=(d.subtree&&d.subtree.downCount)||0;
const L=[];
L.push(`RM Circle — your position #${d.id}, in plain English 👇`);
L.push('');
L.push(d.directCount>=2
? `✅ YOUR SPOT: ${d.tierName}, ${d.levelName} level, qualified (2/2 directs). ${team} ${team===1?'person':'people'} in your team so far.`
: `⚠️ YOUR SPOT: ${d.tierName}, ${d.levelName} level — NOT qualified yet (${d.directCount}/2 directs). Get ${2-d.directCount} more direct${2-d.directCount===1?'':'s'} of your own first — that's the ONE thing that unlocks everything below.`);
L.push('');
L.push(`HOW UPGRADES PAY YOU (the one rule that clears up all the confusion):`);
L.push(`Every layer of your team pays you ONCE, at ONE level. The people directly under you pay you when they upgrade to Ascensus. The next layer down pays you at Fabrica. The layer below that at Culmen. Each layer deeper pays you at the next level up the ladder.`);
L.push('');
L.push(`To CATCH each layer's payment, you only need to be that level yourself (and stay qualified). Your ladder right now:`);
const maxG=Math.min(6,Math.max(2,...Object.keys(gens).map(Number).concat([2])));
for(let g=1;g<=maxG;g++){
const amt=up[g-1]; if(!amt)continue;
const buys=LV[g]||'the top'; const need=LV[g]||'the top';
const have=d.directCount>=2&&myLevel>=g+1;
const cnt=(gens[g]||[]).length;
L.push(`• Layer ${g}${cnt?` (${cnt} ${cnt===1?'person':'people'})`:''}: their ${buys} upgrade pays you ${money(amt)} POL each — needs you at ${need}${have?' ✅ you already are':' ← get here to catch it'}`);
}
L.push('');
L.push(`WILL YOU MISS THEM? Only if your team climbs past a level before you do. But nobody skips levels — they go up one at a time — so you always get warning. Stay one step ahead of your team's growth and you catch every single one. Fall behind and that one payment passes you to the next person up (it doesn't come back), so the whole game is simply: keep your level ahead of your deepest active layer.`);
L.push('');
// next concrete action
const myNext=up[myLevel-1];
const nextName=LV[myLevel]||'max level';
// shallowest layer you can't yet catch
let gap=0;
for(let g=1;g<=8;g++){ if((gens[g]||[]).length && !(d.directCount>=2&&myLevel>=g+1)){ gap=g; break; } }
// is anyone in the gap layer already close (at/near their pay-you level)?
const gapClose=gap&&(gens[gap]||[]).some(n=>(n.level||1)>=gap-1);
if(d.directCount<2) L.push(`YOUR NEXT MOVE: get your 2 directs. Until then none of this pipeline can pay you — spillover fills your team but only your own 2 qualify you.`);
else if(gap) L.push(`YOUR NEXT MOVE: your next uncovered layer is layer ${gap} — to catch their ${LV[gap]||'top'} upgrades you'll need to be at ${LV[gap]||'the top'} yourself (next step: ${nextName}${myNext?', '+money(myNext)+' POL':''}). They're still climbing toward it${gapClose?', and getting close — worth upgrading soon':", so no rush — just get there before they do"}. You're already eligible for everything shallower.`);
else if(myNext) L.push(`YOUR NEXT MOVE: you're currently eligible for every active layer. As your team goes deeper, upgrade to ${nextName} (${money(myNext)} POL) before that new layer climbs to its pay-you level.`);
L.push('');
L.push(`See it live and get a red alert the moment you'd miss one: rmcircle.team/my/${d.id}`);
return L.join('\n');
}
async function renderAlerts(d){
const el=document.getElementById('dAlerts');
if(!el)return;
let st={subscribed:false};
try{st=await (await fetch('/api/public/alert-status?id='+d.id)).json();}catch(e){}
function subscribedView(email){
el.innerHTML=`<p style="margin:0 0 10px"><span class="live-badge"><span class="dot"></span> Alerts ON</span> for <strong>${esc(email||'your email')}</strong></p><button id="alertOff" class="btn btn-secondary btn-sm">Turn off alerts</button><span id="alertMsg" class="micro" style="margin-left:8px"></span>`;
document.getElementById('alertOff').addEventListener('click',async()=>{
try{await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email:''})});renderAlerts(d);}catch(e){document.getElementById('alertMsg').textContent='Try again.';}
});
}
function offView(){
el.innerHTML=`<form id="alertForm" style="display:flex;gap:8px;flex-wrap:wrap;max-width:460px"><input name="email" class="input" type="email" placeholder="you@example.com" required style="flex:1;min-width:200px"><button class="btn btn-primary btn-sm">Turn on alerts</button></form><div id="alertMsg" class="micro" style="margin-top:8px"></div>`;
document.getElementById('alertForm').addEventListener('submit',async e=>{
e.preventDefault();
const email=e.currentTarget.elements.email.value.trim(),msg=document.getElementById('alertMsg');
msg.style.color='var(--muted)';msg.textContent='Turning on…';
try{
const r=await fetch('/api/public/alert-signup',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:d.id,email})});
const j=await r.json();
if(!r.ok)throw new Error(j.error||'Failed');
msg.style.color='var(--ok)';msg.textContent='Done — check your inbox for a confirmation.';
setTimeout(()=>renderAlerts(d),1200);
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message;}
});
}
if(st.subscribed)subscribedView(st.email);else offView();
}
function renderShare(d){
const el=document.getElementById('dShare');
if(!el)return;
const dashUrl=`https://rmcircle.team/join/${d.id}`;
const directUrl=`${dashUrl}?direct=1`;
const q2=d.directCount>=2, nx=d.nextInLine;
// The personal QR is ALWAYS shown: /join/<id> self-rotates via the moving
// link, so a scan is always placed correctly — even after 2/2. This is the
// person-to-person flow: open your page, tap the QR, they scan, they see
// your pitch page with the video and the join button.
const guidance=q2
?(nx?`<span class="q-badge q-yes" style="margin-right:6px">★ Qualified</span> Your page now routes new joins to <strong style="color:var(--text)">#${nx.id}</strong> (${nx.directCount||0}/2) — the team play moving down your leg automatically. Anyone who scans still lands on YOUR pitch page; placement is handled for you. A signup on your own link is always a bonus: the entry reward is yours.`
:`<span class="q-badge q-yes" style="margin-right:6px">★ Qualified</span> Your whole leg is qualified — your page routes new joins to the team rotation automatically.`)
:`Share this with your 2 — anyone who scans or clicks lands on your personal pitch page (video, live proof, join button) and joins under <strong style="color:var(--gold)">you</strong>.`;
el.innerHTML=`<p style="color:var(--muted);font-size:14px;line-height:1.6;margin:6px 0 14px">${guidance}</p>
<div style="display:flex;gap:18px;align-items:center;flex-wrap:wrap">
<button id="dQrBtn" title="Tap to enlarge for scanning" style="background:#fff;border:0;border-radius:14px;padding:10px;cursor:pointer;line-height:0"><span id="dQr" style="display:block;width:150px;height:150px"></span></button>
<div style="min-width:220px;flex:1">
<div style="font-weight:800;margin-bottom:6px">📲 Show this QR to anyone, anywhere.</div>
<p class="micro" style="margin:0 0 10px">Tap the code to blow it up full-screen — they scan it with their phone camera and land on your invite page.</p>
<div style="display:flex;gap:8px;flex-wrap:wrap"><button id="copyShare" class="btn btn-secondary btn-sm">Copy my page link</button><a class="btn btn-teal btn-sm" href="${esc(dashUrl)}" target="_blank" rel="noopener">Preview my page →</a></div>
<p class="micro" style="margin:10px 0 0;word-break:break-all">${esc(dashUrl)}</p>
</div>
</div>
<div style="margin-top:16px;padding-top:14px;border-top:1px solid var(--line)">
<div style="font-weight:800;font-size:14px;margin-bottom:4px">🔀 Two ways to share</div>
<p class="micro" style="margin:0 0 8px">The link above is your <strong style="color:var(--text)">Team link</strong> ${q2?'— now that you’re qualified it routes new joins to the next teammate who needs directs, building your team in order (recommended).':'— it credits you and builds your first two.'} Want new people to land <strong style="color:var(--text)">under YOU</strong> as spillover instead? Use your <strong style="color:var(--text)">Direct link</strong>:</p>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center"><button id="copyDirect" class="btn btn-secondary btn-sm">Copy Direct link</button><span class="micro" style="word-break:break-all;color:var(--muted)">${esc(directUrl)}</span></div>
<p class="micro" style="margin:8px 0 0"><strong style="color:var(--text)">Team link</strong> helps your team qualify. <strong style="color:var(--text)">Direct link</strong> places every join under you (own spillover). Either way you keep the entry reward on anyone you personally bring.</p>
</div>`;
const qr=document.getElementById('dQr');if(qr)qr.innerHTML=qrSvg(dashUrl);
const qb=document.getElementById('dQrBtn');
if(qb)qb.addEventListener('click',function(){
const ov=document.createElement('div');
ov.style.cssText='position:fixed;inset:0;background:#fff;z-index:99999;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;padding:24px;cursor:pointer';
const big=document.createElement('div');
big.style.cssText='width:min(78vw,78vh);height:min(78vw,78vh)';
big.innerHTML=qrSvg(dashUrl);
const cap=document.createElement('div');
cap.style.cssText='color:#0a1620;font-weight:800;font-size:18px;text-align:center';
cap.textContent='Scan to see the RM Circle team build — join page for #'+d.id;
const hint=document.createElement('div');
hint.style.cssText='color:#667;font-size:13px';
hint.textContent='Tap anywhere to close';
ov.appendChild(big);ov.appendChild(cap);ov.appendChild(hint);
ov.addEventListener('click',function(){try{document.body.removeChild(ov);}catch(e){}});
document.body.appendChild(ov);
});
const cp=document.getElementById('copyShare');
if(cp)cp.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(dashUrl);cp.textContent='Copied ✓';setTimeout(()=>cp.textContent='Copy my page link',1400)}catch(e){}});
const cd=document.getElementById('copyDirect');
if(cd)cd.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(directUrl);cd.textContent='Copied ✓';setTimeout(()=>cd.textContent='Copy Direct link',1400)}catch(e){}});
}
async function load(id){
try{
// after our own join/upgrade tx, force one cache-bypassing read so the
// dashboard reflects the transaction immediately instead of the cache
let fresh='';try{if(sessionStorage.getItem('rmc.fresh')){fresh='&fresh=1&_t='+Date.now();sessionStorage.removeItem('rmc.fresh');}}catch(e){}
const r=await fetch('/api/public/member?id='+id+fresh);
const d=await r.json();
if(!r.ok)throw new Error(d.error||'Lookup failed');
if(!d.registered)throw new Error(`ID ${id} isn't registered on the smart contract — double-check the number.`);
try{d._priorId=localStorage.getItem('ctb.myId');localStorage.setItem('ctb.myId',String(id))}catch(e){}
if(!/^\/my\/\d+$/.test(location.pathname))history.replaceState(null,'','/my/'+id);
render(d);
}catch(x){
prompt.classList.remove('hidden');dash.classList.add('hidden');
document.getElementById('idError').textContent=x.message;
}
}
document.getElementById('dJoinForm').addEventListener('submit',async e=>{
e.preventDefault();
const form=e.currentTarget,msg=document.getElementById('dJoinMsg');
const newId=form.elements.newId.value.trim(),memberName=form.elements.memberName.value.trim();
if(!/^\d{1,10}$/.test(newId)){msg.style.color='var(--danger)';msg.textContent='Numbers only — the ID shown by the RM Circle dApp.';return}
if(!memberName){msg.style.color='var(--danger)';msg.textContent='Add your name or Telegram handle.';return}
msg.style.color='var(--muted)';msg.textContent='Verifying on the blockchain…';
try{
const pageId=(location.pathname.match(/^\/my\/(\d+)$/)||[])[1]||'?';
const r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,memberName,sponsorId:pageId,source:'dashboard-'+pageId,clickid:window.ctbGetClickId?window.ctbGetClickId():''})});
const d=await r.json();
if(!r.ok)throw new Error(d.error||'Submission failed');
form.classList.add('hidden');
msg.style.color='var(--ok)';
const dash=`<br><a href="/my/${newId}" style="color:var(--gold)">Open your own dashboard →</a>`;
if(d.duplicate)msg.innerHTML=`✓ ID <strong>${newId}</strong> was already submitted — you're on the list.${dash}`;
else if(d.onchain&&d.onchain.registered)msg.innerHTML=`✓ Verified on the blockchain! ID <strong>${newId}</strong> is registered under sponsor <strong>#${d.onchain.referrerId}</strong> (${d.onchain.tier}). The team has been notified — welcome aboard.${dash}`;
else if(d.path==='notfound'){msg.style.color='var(--danger)';msg.innerHTML=`⚠ ID <strong>${newId}</strong> isn't on the smart contract yet — double-check the number from the RM Circle dApp. Your submission is saved and the team will verify it manually.`;}
else msg.innerHTML=`✓ Got it — submitted. The team will verify your placement.${dash}`;
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message}
});
document.getElementById('idForm').addEventListener('submit',e=>{
e.preventDefault();
const v=document.getElementById('memberId').value.trim();
if(!/^\d{1,15}$/.test(v)){document.getElementById('idError').textContent='Numbers only — the ID from the RM Circle dApp.';return}
openDashTab=true; // they actively looked up an ID — open the dashboard
load(v);
});
document.getElementById('switchId').addEventListener('click',()=>{
try{localStorage.removeItem('ctb.myId')}catch(e){}
history.replaceState(null,'','/my');
dash.classList.add('hidden');prompt.classList.remove('hidden');
document.getElementById('memberId').value='';
});
try{ if(location.hash==='#dash'||new URLSearchParams(location.search).get('tab')==='dash') openDashTab=true; }catch(e){}
const pathHasId=/^\/my\/\d+$/.test(location.pathname);
const id=pathId();
if(id&&!pathHasId)openDashTab=true; // bare /my resolved from storage = returning to your own page
if(id)load(id);
})();