Files
rm-circle-team-router/public/my.js
T

635 lines
56 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;});
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);
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>
<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)
upgLog('Reading the exact upgrade cost from the contract…');
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)));
const arr=(d.tier===2)?words.slice(24,32):words.slice(16,24);
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)';
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;
try{
const r=await(await fetch('/api/public/tg-link',{method:'POST'})).json();
if(r.url){window.open(r.url,'_blank','noopener');document.getElementById('tgLinkNote').textContent='Tap START in Telegram to finish linking.';}
else document.getElementById('tgLinkNote').textContent=r.error||'Try again shortly.';
}catch(e){document.getElementById('tgLinkNote').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){err.textContent='No wallet found in this browser. On a phone, open your wallet app (SafePal, Phantom, MetaMask, Trust, Coinbase…), go to its Browser or DApp tab, and type this page\'s address there — your wallet connects automatically.';return;}
const accs=await eth.request({method:'eth_requestAccounts'});const account=accs[0];
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={};
function nudgeBtn(key){return `<br><button class="btn btn-teal btn-sm coach-nudge" data-nkey="${key}" style="margin-top:7px">📨 Send this nudge</button><span class="micro coach-nudge-note" style="display:none;margin-left:8px"></span>`}
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 btn=ev.target.closest&&ev.target.closest('button.coach-nudge');
if(!btn)return;
const rec=nudgeTexts[btn.dataset.nkey];if(!rec)return;
const note=btn.nextElementSibling;
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 ✓';say('Delivered to their 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></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];
if(amt)ready.push({id:n.id,depth,tier,amt,toLevel:LEVELS[depth]||('Level '+(depth+1)),needLevel:depth+1});
}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 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>`;
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){}});
}
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);
})();