(function(){ const esc=s=>String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[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'); if(name==='dash'&&treeUI.mode==='pyramid')setTimeout(applyMatrixZoom,30); // fit once the matrix is visible } 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 ? '
Real payouts landing on this team — verifiable on Polygonscan
'+inc.map(p=>`
${esc(p.desc)} — from #${p.fromId}+${fmt(p.pol)} POL
`).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',zoom:null,_applied:1}; // Zoom the matrix so it fits small screens. treeUI.zoom: null = auto-fit (shrink // to fit width, never enlarge), or a manual factor. Uses CSS `zoom` so the layout // reflows cleanly (no leftover box, centering + scroll keep working). function applyMatrixZoom(){ const bar=document.getElementById('dZoom'); const mtp=document.querySelector('#dTree .mtp'); if(!mtp){if(bar)bar.classList.add('hidden');return} mtp.style.zoom='1'; const availW=mtp.clientWidth; if(!availW){if(bar)bar.classList.add('hidden');return} // in a hidden tab — apply when it opens if(bar)bar.classList.remove('hidden'); const natW=mtp.scrollWidth||1; let z=treeUI.zoom; if(z==null)z=Math.min(1,availW/natW); // Fit / default: shrink only z=Math.max(.4,Math.min(1.6,z)); mtp.style.zoom=String(z); treeUI._applied=z; const lbl=document.getElementById('zLbl');if(lbl)lbl.textContent=Math.round(z*100)+'%'; } (function wireZoom(){ const out=document.getElementById('zOut'),inn=document.getElementById('zIn'),fit=document.getElementById('zFit'); if(out)out.addEventListener('click',()=>{treeUI.zoom=Math.max(.4,(treeUI._applied||1)-0.15);applyMatrixZoom()}); if(inn)inn.addEventListener('click',()=>{treeUI.zoom=Math.min(1.6,(treeUI._applied||1)+0.15);applyMatrixZoom()}); if(fit)fit.addEventListener('click',()=>{treeUI.zoom=null;applyMatrixZoom()}); let t;window.addEventListener('resize',()=>{clearTimeout(t);t=setTimeout(()=>{if(treeUI.mode==='pyramid')applyMatrixZoom()},160)}); })(); 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 '
open
slot
'; const badge=n.tier===2?'P':'S'; const qmark=n.directCount>=2?'✓':''; const spill=isSpill(n)?`↧ spillover · ref #${n.referrerId}`:''; const down=n.downCount?`⬇ ${n.downCount} below · ${fmt(n.downPol)} POL`:'no downline yet'; return ``; } function renderPyramid(){ const out=document.getElementById('dTree'),nav=document.getElementById('dTreeNav'); const root=treeUI.byId[treeUI.rootId]; if(!root){out.innerHTML='
Team view is still indexing — check back in a few minutes.
';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=`
${rows.map((r,i)=>`
${r.map(n=>card(n,i===0)).join('')}
`).join('')}
`; 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?`Viewing: `+path.map((id,i)=>i===path.length-1?`#${id}`:``).join(' › ')+` `:''; 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); applyMatrixZoom(); } function renderList(){ const out=document.getElementById('dTree'); document.getElementById('dTreeNav').innerHTML=''; const zb=document.getElementById('dZoom');if(zb)zb.classList.add('hidden'); const home=treeUI.byId[treeUI.homeId]; if(!home){out.innerHTML='
Team view is still indexing — check back in a few minutes.
';return} const node=(n,depth)=>{ if(!n)return ''; const ch=[n.left,n.right].filter(Boolean); const qmark=n.directCount>=2?'✓ ':''; const badge=n.tier===2?'P':'S'; const label=`${qmark}${badge} #${n.id} ${esc(n.levelName)} · ${n.directCount}/2 directs · ${fmt(n.earnedPol)} POL${n.downCount?` · ⬇ ${n.downCount} below (${fmt(n.downPol)} POL)`:''}${isSpill(n)?` · ↧ spillover (ref #${n.referrerId})`:''}`; const kids=ch.length?``:''; return ch.length?`
  • ${label}${kids}
  • `:`
  • ${label}
  • `; }; out.innerHTML=``; } 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=`
    `+ `
    Your organization${st.downCount.toLocaleString()} member${st.downCount===1?'':'s'}
    `+ `
    Generations deep${gens}
    `+ `
    Qualified below you${q}
    `+ `
    Earned below you${fmt(st.downPol)} POL
    `+ (d.polUsd>0?`
    ≈ US dollars (POL @ $${d.polUsd<0.1?d.polUsd.toFixed(4):d.polUsd.toFixed(2)})$${(st.downPol*d.polUsd).toLocaleString(undefined,{maximumFractionDigits:0})}
    `:'')+ `
    `; } 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/ 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 ?'★ Qualified' :`${2-d.directCount} more direct${2-d.directCount===1?'':'s'} to qualify`; document.getElementById('dFacts').innerHTML= `
    Tier${esc(d.tierName)}
    `+ `
    Level${esc(d.levelName)}
    `+ `
    Directs${d.directCount}/2${d.directCount>=2?' ✓ qualified':''}
    `+ `
    Total received${fmt(d.totalEarnedPol)} POL
    `+ `
    Team below you${d.subtree?`${d.subtree.downCount} member${d.subtree.downCount===1?'':'s'}`:'—'}
    `+ `
    Member since${date(d.joinedAt)}
    `; 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=`
    Why am I not qualified when people sit under me? ${spills===kids.length?'The positions':'Some positions'} under you arrived by spillover — 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 directs — people who join using your ID. You have ${d.directCount}/2 directs; share your link above to get ${2-d.directCount===1?'your last one':'your 2'}.
    `; note.classList.remove('hidden'); }else{note.classList.add('hidden');note.innerHTML='';} } const inc=d.income||[]; 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.
    '; // 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 ?`#${d.id} → ${d.uplineChain.map(i=>'#'+i).join(' → ')} (root)` :'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 ${LEVELS[i+1]} upgrade`:'beyond the 8 pay levels'; return `
    Gen ${i+1}
    ${c}${cap<=1024?' of '+cap:''}${pays}
    `; }).join(''); el.innerHTML=`
    Team depth — members per generation
    ${rows}

    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.

    `; } // 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 ${op.genSize} ${op.genSize===1?'person':'people'} there.` : `That generation is still filling.`; why=`
    What this unlocks: ${esc(name)} extends your reach to generation ${op.gen}. ${have} Each of them pays you ${fmt(op.perCatch)} POL when they buy ${esc(gname)}.
    Your first catch at this level is ${fmt(op.perCatch)} POL — exactly double what this upgrade costs, because every rung is twice the last. That is the whole idea behind Lesson 8 — time your upgrades to your catches.
    `; } el.innerHTML=`

    ⬆️ Upgrade to ${esc(name)} — right from this page

    ${why}

    Exact cost ${fmt(ns.cost)} POL, read live from the contract. Connect the wallet that owns position #${d.id}, confirm once, done. ${ns.funded?'Your wallet already covers it ✓':'Your wallet does not cover it yet — you can time it to your next catches, or top up below.'}

    ${ns.funded?'':``}

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

    `; 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;iview tx'); 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('🎉 Upgraded! 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='

    Sign in once with the wallet that owns your position — one free signature; it can\'t move funds or approve anything.

    '; 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='
    Could not load messages — refresh to retry.
    ';return;} const mine=Number(me.id)===Number(d.id); const banner=mine?'':`
    You're signed in as #${me.id} — this inbox is yours. (You're viewing #${d.id}'s page; the "to" box is pre-filled for them.)
    `; const rows=(data.inbox||[]).map(m=>`
    ${m.org?'📣':'✉️'}
    From #${m.fromId} · ${new Date(m.ts).toLocaleString()}${m.org?' · team broadcast':''}${m.read?'':' · NEW'}
    ${esc(m.body)}
    `).join('')||'
    No messages yet.
    '; const sent=(data.sent||[]).slice(0,3).map(m=>`
    → ${m.org?'whole team':'#'+m.toId} · ${new Date(m.ts).toLocaleString()}: ${esc(m.body.slice(0,90))}${m.body.length>90?'…':''}
    `).join(''); el.innerHTML=banner+rows+ (sent?`
    Recently sent${sent}
    `:'')+ `
    Send a message
    `; 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='
    ✈️ Telegram connected. ' +'Payout pings, team messages and join alerts arrive in your chat with the bot. ' +'Open the bot and tap ☰ for your dashboard inside Telegram.
    '; el.appendChild(foot); return; } foot.innerHTML=''; 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', '
    ' + '✈️ Open Telegram to finish ' + '' + '
    Tap the button above, then press START in Telegram. ' + 'If nothing opens, copy the link and paste it into Telegram instead.
    '); 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.'); try{ if(window.RMCProfile)await window.RMCProfile.require({onlyForId:d&&d.id}); }catch(ge){} 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 `
    `+ ``+ ``+ ``+ `
    `} 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=>`#${id}`; const rows=[]; (c.rollForward||[]).forEach(r=>rows.push(`
    💬
    ${link(r.id)} is qualified but still Scintilla 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.
    📚 The Method play: send them Lesson 8 — Timing Upgrades to Catches${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.`)}
    `)); (c.atRisk||[]).filter(r=>r.qualified&&r.level>1).forEach(r=>rows.push(`
    ⚠️
    ${link(r.id)} (${esc(r.levelName)}) has ${fmt(r.atRiskPol)} POL forming below them but needs ${esc(r.neededLevelName)} to catch it — worth a heads-up before it passes them.
    📚 The Method play: send them Lesson 8 — Timing Upgrades to Catches${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!`)}
    `)); (c.atRisk||[]).filter(r=>!r.qualified).forEach(r=>rows.push(`
    ⏰
    ${link(r.id)} has ${fmt(r.atRiskPol)} POL forming below but isn't qualified yet (${r.directCount}/2) — help them find their ${r.directCount===1?'last direct':'2 directs'}.
    📚 The Method play: send them Lesson 2 — Your Warm List${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.`)}
    `)); (c.oneAway||[]).forEach(r=>rows.push(`
    🎯
    ${link(r.id)} is one direct away from qualifying — introduce them to one good person and their whole position activates.
    📚 The Method play: send them Lesson 3 — The Conversation${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.`)}
    `)); if(!rows.length){card.style.display='none';return;} el.innerHTML=rows.slice(0,10).join('')+`

    The team rule: don't just tell them — teach them to coach. 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? Video 7 in the training is the full tour — send it.

    `; 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(LYOU':(ns.kind==='upgrade'&&L===ns.nextLevel?'NEXT':''); 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?`${costTxt}`:''; return `
    ${tag}${nm}${costLine}
    `; }).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}

    See what each level opens up →

    `; } function renderPipeline(d){ const el=document.getElementById('dPipeline'); if(!el)return; if(!d.subtree||(!d.subtree.left&&!d.subtree.right)){el.innerHTML='
    Your pipeline starts when your first team members are placed below you.
    ';return} if(!d.upgradeCosts){el.innerHTML='
    Pipeline data is loading — check back in a minute.
    ';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(lvla.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+=`

    Ready now — one upgrade from your wallet:

    `; html+=ready.slice(0,10).map(r=>{ const warn=!eligible(r)?(d.directCount<2?' ⚠ needs you qualified (2 directs)':` ⚠ needs you at ${esc(LEVELS[r.needLevel-1])}+`):''; return `
    ⏳
    #${r.id} · gen ${r.depth} · their ${esc(r.toLevel)} upgrade pays you ${fmt(r.amt)} POL${warn}
    `; }).join(''); if(ready.length>10)html+=`

    …and ${ready.length-10} more.

    `; 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+=`

    ${fmt(readyTotal)} POL is one upgrade away from your wallet.${fundNote}

    `; } }else html+='

    Nobody is at their pay-you milestone yet — the members below are still climbing toward it.

    '; if(building.length)html+=`

    ${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.

    💡 Someone below you not upgrading? 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. Lesson 7 covers this in three minutes.

    `; else if(passedCount.n)html+=`

    ${passedCount.n} member${passedCount.n===1?' has':'s have'} already passed their pay-you milestone.

    `; html+=`
    `; 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; // A signed-in owner manages ONE identity here: the verified profile email is // the alert address, and both it and the username are editable in place. // Everyone else keeps the original opt-in form, so nothing existing breaks. try{ const pr=window.RMCProfile?await window.RMCProfile.status():null; if(pr&&pr.signedIn!==false&&Number(pr.id)===Number(d.id)&&pr.profile&&pr.profile.complete){ const hd=document.getElementById('dAlertsHead'),intro=document.getElementById('dAlertsIntro'); if(hd)hd.textContent='Your member profile'; if(intro)intro.innerHTML='How your team leader reaches you, and where your payout alerts go. Your email is never shown to other members and never sold.'; el.innerHTML=`
    `+ `
    Username
    @${esc(pr.profile.username)}
    `+ `
    Email ✓ confirmed
    ${esc(pr.profile.email)}
    `+ `
    `+ ``+ `

    Payout alerts go to this address. Changing it here changes both.

    `; const again=()=>renderAlerts(d); const bu=document.getElementById('pgEditUser'),bm=document.getElementById('pgEditMail'); if(bu)bu.addEventListener('click',async()=>{await window.RMCProfile.edit('username');again();}); if(bm)bm.addEventListener('click',async()=>{await window.RMCProfile.edit('email');again();}); return; } }catch(e){} let st={subscribed:false}; try{st=await (await fetch('/api/public/alert-status?id='+d.id)).json();}catch(e){} function subscribedView(email){ el.innerHTML=`

    Alerts ON for ${esc(email||'your email')}

    `; 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=`
    `; 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/ 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?`★ Qualified Your page now routes new joins to #${nx.id} (${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.` :`★ Qualified 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 you.`; el.innerHTML=`

    ${guidance}

    📲 Show this QR to anyone, anywhere.

    Tap the code to blow it up full-screen — they scan it with their phone camera and land on your invite page.

    Preview my page →

    ${esc(dashUrl)}

    🔀 Two ways to share

    The link above is your Team link ${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 under YOU as spillover instead? Use your Direct link:

    ${esc(directUrl)}

    Team link helps your team qualify. Direct link places every join under you (own spillover). Either way you keep the entry reward on anyone you personally bring.

    `; 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=`
    Open your own dashboard →`; if(d.duplicate)msg.innerHTML=`✓ ID ${newId} was already submitted — you're on the list.${dash}`; else if(d.onchain&&d.onchain.registered)msg.innerHTML=`✓ Verified on the blockchain! ID ${newId} is registered under sponsor #${d.onchain.referrerId} (${d.onchain.tier}). The team has been notified — welcome aboard.${dash}`; else if(d.path==='notfound'){msg.style.color='var(--danger)';msg.innerHTML=`⚠ ID ${newId} 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); // Required member profile (username + verified email). No-ops for a visitor who // has not proved they own a position: the API 401s and the gate never shows. try{ if(window.RMCProfile&&id)window.RMCProfile.require({onlyForId:id}); }catch(e){} })();