';
}
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||'—')}
${levelSelect(s)}
${esc(s.notes||'')}
`).join('')||'
No members in the directory.
';
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();}
if(!grantAutoLoaded){grantAutoLoaded=true;if(window.agBoot)window.agBoot();}
if(!msgsAutoLoaded){msgsAutoLoaded=true;loadAdminMsgs();}
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','directDefaultIds','announceEyebrow','announceImg','announceMeetUrl','announceDateLabel','announceTimes','announceExpiresUTC','announceStartUTC','announceDurationMin','announceId'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;if(f.elements.announceEnabled)f.elements.announceEnabled.checked=c.announceEnabled===undefined?true:!!c.announceEnabled;
}
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('Member 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.announceEnabled=!!(f.elements.announceEnabled&&f.elements.announceEnabled.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==='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='
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.
${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.”
${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.
${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 grantAutoLoaded=false;
async function loadAdminMsgs(){
const tb=document.getElementById('msgRows');if(!tb)return;
try{
const d=await api('/api/admin/messages');
tb.innerHTML=(d.messages||[]).map(m=>`
${esc(new Date(m.ts).toLocaleString())}
#${m.fromId}
${m.org?'📣 whole team':'#'+m.toId}
${esc(m.body)}
${m.readCount}
`).join('')||'
No messages yet.
';
}catch(x){tb.innerHTML=`
${esc(x.message)}
`}
}
const mr=document.getElementById('msgRefresh');if(mr)mr.addEventListener('click',loadAdminMsgs);
let msgsAutoLoaded=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).
⏫ 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.
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
Member's next upgrade
Escapes to (outside)
Amount
`+
rt.leaks.map(l=>`
#${l.from}
#${l.to} (L${l.toLevel})
${fmt(l.amt)} POL
`).join('')+
`
`
:`
✓ Nothing is escaping your org right now — every next-upgrade payment stays with you or your team.
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?`
When (UTC)
From member
For
Tier
Amount
${d.income.map(p=>`
${date(p.ts)}
#${p.fromId}
${esc(p.desc||p.levelName)}
${p.fromTier===2?'Premium':'Standard'}
${fmt(p.pol)} POL
`).join('')}
`:'
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 '