const loginView=document.getElementById('loginView'),adminView=document.getElementById('adminView'),rows=document.getElementById('sponsorRows'),toast=document.getElementById('toast');let state=null; const LEVELS=['Scintilla','Ascensus','Fabrica','Culmen','Apex','Fastigium','Vertex','Corona']; function levelSelect(s){const opts=(LEVELS.includes(s.level)?LEVELS:[s.level,...LEVELS]).map(l=>`${esc(l)}`).join('');return ``} function showToast(msg){toast.textContent=msg;toast.classList.add('show');setTimeout(()=>toast.classList.remove('show'),1800)} async function api(url,opts={}){const r=await fetch(url,{headers:{'Content-Type':'application/json',...(opts.headers||{})},...opts});const d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.error||'Request failed');return d} async function loadState(){try{state=await api('/api/admin/state');loginView.classList.add('hidden');adminView.classList.remove('hidden');render()}catch(e){loginView.classList.remove('hidden');adminView.classList.add('hidden')}} function statusClass(s){return `status status-${s}`} function pct(n,d){return d?`${Math.round((n/d)*100)}%`:'—'} function renderAnalytics(){ const src=(state.analytics&&state.analytics.sources)||{}; const tot={bridge:0,start:0,click:0,training:0,postback:0,purchase:0,join:0,joinnow:0}; const list=Object.entries(src).map(([name,r])=>{tot.bridge+=r.bridge||0;tot.start+=r.start||0;tot.click+=r.click||0;tot.training+=r.training||0;tot.postback+=r.postback||0;tot.purchase+=r.purchase||0;tot.join+=r.join||0;tot.joinnow+=r.joinnow||0;return{name,bridge:r.bridge||0,start:r.start||0,click:r.click||0,training:r.training||0,join:r.join||0,joinnow:r.joinnow||0,purchase:r.purchase||0}}) .sort((a,b)=>b.click-a.click||b.start-a.start||b.bridge-a.bridge); document.getElementById('funnelStats').innerHTML= `
Bridge views${tot.bridge}
`+ `
Start views${tot.start}
`+ `
Join clicks${tot.click}
`+ `
Bridge → Start${pct(tot.start,tot.bridge)}
`+ `
Start → Join${pct(tot.click,tot.start)}
`+ (tot.join?`
Invite-page views${tot.join}
`:'')+ (tot.joinnow?`
Join-now views${tot.joinnow}
`:'')+ (tot.purchase?`
Confirmed joins${tot.purchase}
`:'')+ (tot.joinnow&&tot.purchase?`
Join-now → Confirmed${pct(tot.purchase,tot.joinnow)}
`:'')+ (tot.postback?`
BeMob postbacks${tot.postback}
`:''); const subs=state.submissions||[]; document.getElementById('submissionRows').innerHTML=subs.map(s=>{ const path=s.path==='rotation'?'rotation ':s.path==='leg'?'leg ':''; const oc=s.onchain?(s.onchain.registered?`${path}✓ ${esc(s.onchain.tier||'')} · ref #${esc(String(s.onchain.referrerId??'?'))}`:'✗ not found'):'—'; return `${esc((s.ts||'').replace('T',' ').slice(0,16))}${esc(s.memberName||'—')}${esc(s.newId)}ID ${esc(s.sponsorId)}${esc(s.source)}${oc}`; }).join('')||'No submissions yet.'; document.getElementById('trafficRows').innerHTML=list.map(r=>`${esc(r.name)}${r.bridge}${r.start}${r.training}${r.join}${r.joinnow}${r.click}${r.purchase?`${r.purchase}`:0}${pct(r.click,r.start)}`).join('')||'No traffic recorded yet.'; } function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))} function render(){ const sponsors=[...state.sponsors].sort((a,b)=>a.sortOrder-b.sortOrder);rows.innerHTML=sponsors.map((s,i)=>`${i+1}
ID ${esc(s.id)}
${s.email?`
✉ ${esc(s.email)}
`:''}${esc(s.parentId||'—')}${s.directs}/2${levelSelect(s)}${esc(s.status)}${s.clicks||0}
${s.status!=='qualified'?``:``}${s.status==='waiting'?``:''}
`).join('')||'No sponsors in the queue.'; renderAnalytics(); const ai=state.aiChat||{};document.getElementById('aiModel').textContent=ai.model||'OpenRouter';document.getElementById('aiStatus').innerHTML=ai.configured?'● AI chat is ON — key is set':'○ AI chat is OFF — using built-in answers'; const em=state.email||{};document.getElementById('emailStatus').innerHTML=em.configured?'● Payment emails are ON — key is set':'○ Payment emails are OFF — no SendGrid key'; const incEl=document.getElementById('incomeIds');if(incEl&&!incEl.value)incEl.value=state.config.ownerIds||'21,24,25'; if(!incomeAutoLoaded&&incEl&&incEl.value){incomeAutoLoaded=true;loadIncome();} const orgEl=document.getElementById('orgRoot');if(orgEl&&!orgEl.value)orgEl.value=state.config.orgRootId||'21'; if(!orgShareAutoLoaded&&orgEl&&orgEl.value){orgShareAutoLoaded=true;loadOrgShare();} if(!coachAutoLoaded){coachAutoLoaded=true;loadCoaching();} const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||''; const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress; } document.getElementById('loginForm').addEventListener('submit',async e=>{e.preventDefault();const err=document.getElementById('loginError');err.textContent='';try{await api('/api/admin/login',{method:'POST',body:JSON.stringify({password:document.getElementById('password').value})});document.getElementById('password').value='';await loadState()}catch(x){err.textContent=x.message}}); document.getElementById('logoutBtn').addEventListener('click',async()=>{await api('/api/admin/logout',{method:'POST'});location.reload()}); document.getElementById('addSponsorForm').addEventListener('submit',async e=>{e.preventDefault();const form=e.currentTarget,obj=Object.fromEntries(new FormData(form));try{const d=await api('/api/admin/sponsors',{method:'POST',body:JSON.stringify(obj)});state.sponsors=d.sponsors;form.reset();render();showToast('Sponsor added')}catch(x){showToast(x.message)}}); document.getElementById('configForm').addEventListener('submit',async e=>{e.preventDefault();const f=e.currentTarget,obj=Object.fromEntries(new FormData(f));obj.showSponsorName=f.elements.showSponsorName.checked;obj.showQueueProgress=f.elements.showQueueProgress.checked;obj.premiumEntryPol=Number(obj.premiumEntryPol);try{const d=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify(obj)});state.config=d.config;render();showToast('Settings saved')}catch(x){showToast(x.message)}}); rows.addEventListener('click',async e=>{const b=e.target.closest('button[data-action]');if(!b)return;const id=b.dataset.id,a=b.dataset.action;try{let d;if(a==='rename'){const cur=(state.sponsors.find(s=>s.id===id)||{}).name||'';const nm=prompt(`Name for sponsor ID ${id}:`,cur);if(nm===null||!nm.trim())return;d=await api(`/api/admin/sponsors/${id}`,{method:'PATCH',body:JSON.stringify({name:nm.trim()})});}else if(a==='email'){const cur=(state.sponsors.find(s=>s.id===id)||{}).email||'';const em=prompt(`Contact email for sponsor ID ${id} (leave empty to clear):`,cur);if(em===null)return;d=await api(`/api/admin/sponsors/${id}`,{method:'PATCH',body:JSON.stringify({email:em.trim()})});}else if(a==='delete'){if(!confirm(`Delete sponsor ID ${id}?`))return;d=await api(`/api/admin/sponsors/${id}`,{method:'DELETE'})}else if(a==='inc')d=await api(`/api/admin/sponsors/${id}/increment`,{method:'POST'});else if(a==='qualify')d=await api(`/api/admin/sponsors/${id}/qualify`,{method:'POST'});else if(a==='activate')d=await api(`/api/admin/sponsors/${id}/activate`,{method:'POST'});else if(a==='reset')d=await api(`/api/admin/sponsors/${id}/reset`,{method:'POST'});else if(a==='up'||a==='down')d=await api(`/api/admin/sponsors/${id}/move`,{method:'POST',body:JSON.stringify({direction:a})});if(d&&d.sponsors){state.sponsors=d.sponsors;render();showToast('Updated')}}catch(x){showToast(x.message)}}); document.getElementById('aiKeyForm').addEventListener('submit',async e=>{e.preventDefault();const inp=e.currentTarget.elements.key,key=inp.value.trim();if(!key){showToast('Paste a key first');return}try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key})});state.aiChat={...(state.aiChat||{}),configured:d.configured};inp.value='';render();showToast('AI chat enabled')}catch(x){showToast(x.message)}}); document.getElementById('aiKeyClear').addEventListener('click',async()=>{if(!confirm('Turn off AI chat and go back to built-in answers?'))return;try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key:''})});state.aiChat={...(state.aiChat||{}),configured:d.configured};render();showToast('AI chat disabled')}catch(x){showToast(x.message)}}); rows.addEventListener('change',async e=>{const sel=e.target.closest('select[data-action="level"]');if(!sel)return;try{const d=await api(`/api/admin/sponsors/${sel.dataset.id}`,{method:'PATCH',body:JSON.stringify({level:sel.value})});state.sponsors=d.sponsors;render();showToast('Level updated')}catch(x){showToast(x.message);render()}}); document.getElementById('sgKeyForm').addEventListener('submit',async e=>{e.preventDefault();const inp=e.currentTarget.elements.key,key=inp.value.trim();if(!key){showToast('Paste a key first');return}try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key})});state.email={...(state.email||{}),configured:d.configured};inp.value='';render();showToast('Payment emails enabled')}catch(x){showToast(x.message)}}); document.getElementById('sgKeyClear').addEventListener('click',async()=>{if(!confirm('Disable "you\'ve been paid" emails?'))return;try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key:''})});state.email={...(state.email||{}),configured:d.configured};render();showToast('Payment emails disabled')}catch(x){showToast(x.message)}}); document.getElementById('emailFromSave').addEventListener('click',async()=>{const v=document.getElementById('emailFromInput').value.trim();try{const d=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({emailFrom:v})});state.config=d.config;showToast('From address saved')}catch(x){showToast(x.message)}}); let orgShareAutoLoaded=false; async function loadOrgShare(){ const root=(document.getElementById('orgRoot').value||'').trim(); const out=document.getElementById('orgShare'); if(!root||!/^\d+$/.test(root)){out.innerHTML='
Enter a numeric top ID (e.g. 21).
';return} out.innerHTML='
Reading the blockchain…
'; try{ try{const c=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({orgRootId:root})});state.config=c.config;}catch(_){} const d=await api('/api/admin/org-share?root='+encodeURIComponent(root)); if(!d.ready){out.innerHTML='
Snapshot not ready yet — try again in a minute.
';return} if(!d.found){out.innerHTML=`
ID ${esc(root)} isn't in the indexed matrix yet.
`;return} const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0}); out.innerHTML= `
`+ `
${d.memberPct}%
of all members
`+ `
${d.polPct}%
of all POL paid
`+ `
`+ `
`+ `

Your organization rooted at #${d.rootId} is ${d.orgMembers} of ${d.totalMembers} members in the whole contract (${d.orgBelow} below you + your position), and has earned ${fmt(d.orgPol)} of ${fmt(d.totalPol)} POL ever paid.

`+ `
Your org members${d.orgMembers}
Whole network${d.totalMembers}
Your org earned${fmt(d.orgPol)} POL
Network paid${fmt(d.totalPol)} POL
`+ genBreakdownHtml(d.genCounts,d.rootId); }catch(x){out.innerHTML=`
${esc(x.message)}
`} } function genBreakdownHtml(gc,rootId){ if(!gc||!gc.length)return ''; const rows=gc.map((c,i)=>{ const cap=Math.pow(2,i+1), pctFill=Math.min(100,Math.round(100*c/cap)); const capLabel=cap<=1024?` of ${cap} slots`:''; return `
Gen ${i+1}
${c}${capLabel}
`; }).join(''); return `
Depth — members per generation below #${rootId}
${rows}
`; } document.getElementById('orgShareForm').addEventListener('submit',e=>{e.preventDefault();loadOrgShare()}); async function loadCoaching(){ const out=document.getElementById('coachOut');if(!out)return; const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0}); out.innerHTML='
Scanning the org…
'; try{ const root=(document.getElementById('orgRoot')&&document.getElementById('orgRoot').value.trim())||(state&&state.config&&state.config.orgRootId)||'21'; const d=await api('/api/admin/coaching?root='+encodeURIComponent(root)); if(!d.ready){out.innerHTML='
Chain snapshot not ready — try again in a minute.
';return} const nm=r=>r.name?`${esc(r.name)} (#${r.id})`:`#${r.id}`; const link=r=>`${nm(r)}`; const mineRisk=d.atRisk.filter(r=>r.owned),mineRoll=d.rollForward.filter(r=>r.owned); d.atRisk=d.atRisk.filter(r=>!r.owned);d.rollForward=d.rollForward.filter(r=>!r.owned);d.oneAway=d.oneAway.filter(r=>!r.owned); const mineHtml=(mineRisk.length||mineRoll.length)? `

👑 Your positions — your own action items

`+ mineRisk.map(r=>`
👑
${link(r)} — ${esc(r.levelName)}, ${fmt(r.atRiskPol)} POL forming; needs ${esc(r.neededLevelName||'qualification')} to catch it${r.earnedPol>=(r.upgradeCost||0)&&r.upgradeCost?' — next step covered by its earnings':r.upgradeCost?` (earned ${fmt(r.earnedPol)} vs ${fmt(r.upgradeCost)} next step)`:''}.
`).join('')+ mineRoll.map(r=>`
👑
${link(r)} — qualified at Scintilla, Ascensus (${fmt(r.ascensusCost)} POL) covered by its ${fmt(r.earnedPol)} POL earned, whenever you choose.
`).join('') :''; const t1=d.rollForward.map(r=>`
💬
${link(r)} — qualified, still Scintilla, sitting on ${fmt(r.earnedPol)} POL (Ascensus costs ${fmt(r.ascensusCost)}). One message: “your entry rewards already cover the upgrade that catches your team's first payments.”
`).join('')||'
Nobody stuck at qualified-Scintilla. 🎉
'; const t2=d.atRisk.filter(r=>r.qualified&&r.level>1).map(r=>`
⚠️
${link(r)} — ${esc(r.levelName)}, needs ${esc(r.neededLevelName)} to catch ${fmt(r.atRiskPol)} POL forming under them (from ${r.fromIds.map(f=>'#'+f).join(', ')}).
`).join('')||'
Everyone leveled is ahead of their wave. 🎉
'; const t3=d.oneAway.map(r=>`
🎯
${link(r)} — 1/2 directs, one placement from qualifying.
`).join('')||'
Nobody stranded at 1/2.
'; const unq=d.atRisk.filter(r=>!r.qualified).map(r=>`
⏰
${link(r)} — NOT qualified (${r.directCount}/2) with ${fmt(r.atRiskPol)} POL already forming below. Qualification is urgent for them.
`).join(''); out.innerHTML= `
Members scanned${d.scanned}
POL at risk${fmt(d.totals.atRiskPol)}
Easy wins${d.totals.rollForwardCount}
One direct away${d.totals.oneAwayCount}
`+ mineHtml+ `

Tier 1 · Easy wins — one message each

${t1}`+ `

Tier 2 · Money at risk — coach the upgrade

${t2}`+ (unq?`

Tier 2b · Money forming, not yet qualified

${unq}`:'')+ `

Tier 3 · One placement from qualified

${t3}`+ `

Teach-forward rule: every nudge should end with "now open YOUR dashboard's Coach Your Team panel and run this same play for your two." You're not coaching members — you're training coaches.

`; }catch(x){out.innerHTML=`
${esc(x.message)}
`} } const cr=document.getElementById('coachRefresh');if(cr)cr.addEventListener('click',loadCoaching); let coachAutoLoaded=false; let incomeAutoLoaded=false; async function loadIncome(){ const ids=(document.getElementById('incomeIds').value||'').trim(); const out=document.getElementById('incomeResult'),sum=document.getElementById('incomeSummary'); if(!ids||!/^[\d,\s]+$/.test(ids)){out.innerHTML='
Enter comma-separated numeric IDs (e.g. 21,24,25).
';return} out.innerHTML='
Reading the blockchain…
';sum.innerHTML=''; try{ try{const c=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({ownerIds:ids})});state.config=c.config;}catch(_){} const d=await api('/api/admin/income?ids='+encodeURIComponent(ids)); const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:2}); const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—'; const needs=d.upgradeNeeds||[]; document.getElementById('incomeAlert').innerHTML=needs.length?needs.map(n=>`
⏫ Upgrade #${n.id} to ${esc(n.neededLevelName)}${n.reason==='qualify'?' (and get 2 directs)':''} — ${n.members.map(m=>'#'+m).join(', ')} ${n.members.length===1?'is':'are'} one upgrade from paying you ~${fmt(n.amountAtRisk)} POL, but #${n.id} (${esc(n.levelName)}) can't catch it yet. Upgrade before they do or it passes up.
`).join(''):''; const rt=d.routing; document.getElementById('incomeRouting').innerHTML=(rt&&rt.ready)?( `
`+ `
Your positions catch${fmt(rt.ownedPol)} POL
`+ `
Team (in-org) catches${fmt(rt.teamPol)} POL
healthy
`+ `
Leaking above #${rt.rootId}${fmt(rt.leakPol)} POL
`+ `
To fees${fmt(rt.feePol)} POL
`+ `
`+ `

Next-upgrade routing for ${rt.members} members below #${rt.rootId} — where each imminent upgrade payment lands. "Team" = teammates earning (good). "Leaking" = escaping to outsiders above you (often structural, not fixable by upgrading).

`+ (rt.leaks&&rt.leaks.length? `
⚠ ${rt.leaks.length} payment${rt.leaks.length===1?'':'s'} escaping your org — show
`+ rt.leaks.map(l=>``).join('')+ `
Member's next upgradeEscapes to (outside)Amount
#${l.from}#${l.to} (L${l.toLevel})${fmt(l.amt)} POL
` :`
✓ Nothing is escaping your org right now — every next-upgrade payment stays with you or your team.
`) ):''; sum.innerHTML=`
Total received (all)${fmt(d.grandEarnedPol)} POL
`+ d.ids.map(id=>{const p=d.perId[id]||{};return `
#${id}${p.registered&&p.levelName?' · '+esc(p.levelName):''}${p.registered?fmt(p.totalEarnedPol)+' POL':'not registered'}${p.registered?`
${p.count} payment${p.count===1?'':'s'}
`:''}
`}).join(''); out.innerHTML=d.rows.length?`
${d.rows.map(r=>``).join('')}
When (UTC)ToFromForAmount
${date(r.ts)}#${r.toId}#${r.fromId}${esc(r.desc||'')}${fmt(r.pol)} POL

${d.rows.length} payment${d.rows.length===1?'':'s'} across your positions, newest first.

`:'
No payments recorded to these positions yet.
'; }catch(x){out.innerHTML=`
${esc(x.message)}
`} } document.getElementById('incomeForm').addEventListener('submit',e=>{e.preventDefault();loadIncome()}); document.getElementById('lookupForm').addEventListener('submit',async e=>{ e.preventDefault(); const id=document.getElementById('lookupId').value.trim(),out=document.getElementById('lookupResult'); if(!/^\d+$/.test(id)){out.innerHTML='
Enter a numeric ID.
';return} out.innerHTML='
Reading the blockchain…
'; try{ const d=await api('/api/admin/member-lookup?id='+id); if(!d.registered){out.innerHTML=`
ID ${esc(id)} is NOT registered on the contract.
`;return} const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:2}); const date=ts=>ts?new Date(ts*1000).toISOString().replace('T',' ').slice(0,16):'—'; const facts=`
`+ ``+ `
Joined (UTC)${date(d.joinedAt)}
`+ `
Tier · Level${esc(d.tierName)} · ${esc(d.levelName)}
`+ `
Referred by${d.referrerId?'#'+d.referrerId:'—'}
`+ `
Matrix upline${d.uplineId?'#'+d.uplineId:'— (root)'}
`+ `
Matrix children${d.matrix?`${d.matrix.left?'#'+d.matrix.left:'open'} · ${d.matrix.right?'#'+d.matrix.right:'open'}`:'—'}
`+ `
Directs (${d.directCount})${d.directs&&d.directs.length?d.directs.map(x=>'#'+x).join(', '):'none yet'}
`+ `
Total earned${fmt(d.totalEarnedPol)} POL
`+ `
Total paid in${fmt(d.totalPaidPol)} POL
`; const chain=d.uplineChain&&d.uplineChain.length?`

Lineage up to root: #${d.id} → ${d.uplineChain.map(u=>`#${u.id} (${esc(u.levelName)}, ${u.directCount} directs)`).join(' → ')}

`:''; const income=d.income&&d.income.length?`
${d.income.map(p=>``).join('')}
When (UTC)From memberForTierAmount
${date(p.ts)}#${p.fromId}${esc(p.desc||p.levelName)}${p.fromTier===2?'Premium':'Standard'}${fmt(p.pol)} POL
`:'
No payments received yet.
'; out.innerHTML=facts+chain+`

Payments received by #${d.id} — ${d.income?d.income.length:0} total, newest first:

`+income; }catch(x){out.innerHTML=`
${esc(x.message)}
`} }); const matrixUI={data:null,byId:{},parent:{},rootId:1,mode:'pyramid'}; function mtIndex(n,par){if(!n)return;matrixUI.byId[n.id]=n;if(par)matrixUI.parent[n.id]=par.id;mtIndex(n.left,n);mtIndex(n.right,n)} function mtBadge(n){return n.tier===2?'P':'S'} const mtFmt=n=>Number(n||0).toLocaleString(undefined,{maximumFractionDigits:0}); function mtCard(n){ if(!n)return '
open
slot
'; const down=n.downCount?`⬇ ${n.downCount} below · ${mtFmt(n.downPol)} POL`:'no downline yet'; const qmark=n.directCount>=2?'✓':''; const par=matrixUI.parent[n.id]; const spill=par&&n.referrerId&&n.referrerId!==par?`↧ spillover · ref #${n.referrerId}`:''; return ``; } function renderPyramid(){ const out=document.getElementById('matrixTree'),nav=document.getElementById('matrixNav'); const root=matrixUI.byId[matrixUI.rootId]; if(!root){out.innerHTML='
Position not found.
';return} const GEN=4; // root + 3 generations = up to 15 cards const rows=[[root]]; for(let g=1;gn?[n.left||null,n.right||null]:[null,null])); } out.innerHTML=`
${rows.map(r=>`
${r.map(mtCard).join('')}
`).join('')}
`; // breadcrumb: path from tree root down to current focus const path=[];let cur=matrixUI.rootId; while(cur){path.unshift(cur);cur=matrixUI.parent[cur]} nav.classList.remove('hidden'); nav.innerHTML=`Viewing: `+path.map((id,i)=>i===path.length-1?`#${id}`:``).join(' › ')+ (matrixUI.parent[matrixUI.rootId]?` `:''); out.querySelectorAll('[data-mid]').forEach(b=>b.addEventListener('click',()=>{matrixUI.rootId=Number(b.dataset.mid);renderPyramid()})); nav.querySelectorAll('[data-mid]').forEach(b=>b.addEventListener('click',()=>{matrixUI.rootId=Number(b.dataset.mid);renderPyramid()})); } function renderList(){ const out=document.getElementById('matrixTree'),d=matrixUI.data; document.getElementById('matrixNav').classList.add('hidden'); const node=(n,depth)=>{ if(!n)return ''; if(n.missing)return `
  • #${n.id} (no data)
  • `; const ch=[n.left,n.right].filter(Boolean); const kids=ch.length?``:''; const open=depth<4?' open':''; const label=`${mtBadge(n)} #${n.id} ${esc(n.levelName)} · ${n.directCount}/2 directs · ${mtFmt(n.earnedPol)} POL earned${n.downCount?` · ⬇ ${n.downCount} below (${mtFmt(n.downPol)} POL)`:''}${n.referrerId?` · ref #${n.referrerId}`:''}`; return ch.length?`
  • ${label}${kids}
  • `:`
  • ${label}
  • `; }; const unplaced=d.unplaced?`

    Not reachable from root: ${d.unplaced.map(i=>'#'+i).join(', ')}

    `:''; out.innerHTML=`${unplaced}`; } function renderMatrix(){ const d=matrixUI.data; const head=`${d.memberCount} positions · snapshot ${new Date(d.snapshotAt).toISOString().replace('T',' ').slice(0,16)} UTC · ✓ qualified (2/2 directs) · P Premium · S Standard · ⬇ full downline below that position`; document.getElementById('treeToggleBtn').classList.remove('hidden'); document.getElementById('treeToggleBtn').textContent=matrixUI.mode==='pyramid'?'List view':'Pyramid view'; if(matrixUI.mode==='pyramid')renderPyramid();else renderList(); const out=document.getElementById('matrixTree'); out.insertAdjacentHTML('afterbegin',`

    ${head}

    `); } document.getElementById('treeLoadBtn').addEventListener('click',async()=>{ const out=document.getElementById('matrixTree'); out.innerHTML='
    Building matrix from the on-chain snapshot…
    '; try{ const d=await api('/api/admin/matrix-tree'); if(!d.ready||!d.root){out.innerHTML='
    Snapshot not ready yet — try again in a minute.
    ';return} matrixUI.data=d;matrixUI.byId={};matrixUI.parent={};matrixUI.rootId=d.root.id; mtIndex(d.root,null); renderMatrix(); }catch(x){out.innerHTML=`
    ${esc(x.message)}
    `} }); document.getElementById('treeToggleBtn').addEventListener('click',()=>{ matrixUI.mode=matrixUI.mode==='pyramid'?'list':'pyramid'; if(matrixUI.data)renderMatrix(); }); loadState();