Coaching Radar: live downline triage in admin + member dashboards

- chain.js getCoachingScan(rootId): classifies everyone below a root into
  atRisk (POL forming they can't catch - corrected bought-level rule),
  rollForward (qualified-at-Scintilla with entry rewards covering Ascensus),
  and oneAway (1/2 directs)
- Admin: GET /api/admin/coaching?root= (name-decorated) + "Coaching Radar"
  panel with tiered who/what-to-say/POL-at-stake rows, auto-loaded
- Member dashboards: memberPublic now returns .coach scoped to the member's
  own leg; new "Coach your team" card shows the same triage so every member
  coaches their own team - computed live, no snapshots or cron needed
- Chatbot canned answer + AI system prompt updated to describe the panel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-19 05:15:45 -05:00
parent 9966dc1728
commit a835291eab
7 changed files with 99 additions and 3 deletions
+1
View File
@@ -5,6 +5,7 @@
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic &amp; Conversions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.</p></div><div id="funnelStats" class="funnel"></div><div class="table-wrap"><table class="table"><thead><tr><th>Source</th><th>Bridge views</th><th>Start views</th><th>Training views</th><th>Invite views</th><th>Join-now views</th><th>Join clicks</th><th>Confirmed</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div>
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Member ID Submissions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.</p></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>Name / Handle</th><th>New ID</th><th>Joined under</th><th>Source</th><th>On-chain</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div>
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Your Organization vs. the Network</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">How your team — rooted at your top ID — stacks up against the entire RM Circle smart contract. Live on-chain.</p></div><form id="orgShareForm" style="display:flex;gap:8px"><input id="orgRoot" class="input" style="max-width:110px" placeholder="21" inputmode="numeric"><button class="btn btn-secondary btn-sm">Refresh</button></form></div><div id="orgShare"><div class="empty">Reading the blockchain…</div></div></div>
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Coaching Radar</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Live triage of your whole org: who to nudge, what to tell them, and how much POL is on the line. Computed fresh from the chain index on every refresh.</p></div><button id="coachRefresh" class="btn btn-secondary btn-sm">Refresh</button></div><div id="coachOut"><div class="empty">Loading…</div></div></div>
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">My Positions — Income</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.</p></div><form id="incomeForm" style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"><input id="incomeIds" class="input" style="max-width:260px" placeholder="21,24,25" inputmode="numeric"><button class="btn btn-teal">Load</button></form><div id="incomeAlert"></div><div id="incomeRouting" style="margin-bottom:14px"></div><div id="incomeSummary" class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px"></div><div id="incomeResult"></div></div>
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">On-Chain Member Lookup</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.</p></div><form id="lookupForm" style="display:flex;gap:10px;margin-bottom:14px"><input id="lookupId" class="input" style="max-width:220px" placeholder="Member ID e.g. 46" inputmode="numeric"><button class="btn btn-teal">Look Up</button></form><div id="lookupResult"></div></div>
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Matrix View</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.</p></div><div style="display:flex;gap:8px"><button id="treeLoadBtn" class="btn btn-secondary btn-sm">Load Matrix</button><button id="treeToggleBtn" class="btn btn-secondary btn-sm hidden">List view</button></div></div><div id="matrixNav" class="hidden" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="matrixTree"></div></div></div>
+24
View File
@@ -40,6 +40,7 @@ function render(){
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;
}
@@ -87,6 +88,29 @@ function genBreakdownHtml(gc,rootId){
return `<div style="margin-top:16px"><small style="text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-size:11px">Depth — members per generation below #${rootId}</small><div style="margin-top:8px">${rows}</div></div>`;
}
document.getElementById('orgShareForm').addEventListener('submit',e=>{e.preventDefault();loadOrgShare()});
async function loadCoaching(){
const out=document.getElementById('coachOut');if(!out)return;
out.innerHTML='<div class="empty">Scanning the org…</div>';
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='<div class="empty">Chain snapshot not ready — try again in a minute.</div>';return}
const nm=r=>r.name?`${esc(r.name)} (#${r.id})`:`#${r.id}`;
const link=r=>`<a href="/my/${r.id}" target="_blank" rel="noopener" style="color:var(--teal)">${nm(r)}</a>`;
const t1=d.rollForward.map(r=>`<div class="pp-row" style="padding:8px 12px"><div class="pp-icon">💬</div><div class="pp-body">${link(r)} — qualified, still Scintilla, sitting on <strong>${fmt(r.earnedPol)} POL</strong> (Ascensus costs ${fmt(r.ascensusCost)}). One message: “your entry rewards already cover the upgrade that catches your team's first payments.”</div></div>`).join('')||'<div class="empty">Nobody stuck at qualified-Scintilla. 🎉</div>';
const t2=d.atRisk.filter(r=>r.qualified&&r.level>1).map(r=>`<div class="pp-row" style="padding:8px 12px"><div class="pp-icon">⚠️</div><div class="pp-body">${link(r)} — ${esc(r.levelName)}, needs <strong>${esc(r.neededLevelName)}</strong> to catch <strong style="color:var(--gold)">${fmt(r.atRiskPol)} POL</strong> forming under them (from ${r.fromIds.map(f=>'#'+f).join(', ')}).</div></div>`).join('')||'<div class="empty">Everyone leveled is ahead of their wave. 🎉</div>';
const t3=d.oneAway.map(r=>`<div class="pp-row" style="padding:8px 12px"><div class="pp-icon">🎯</div><div class="pp-body">${link(r)} — 1/2 directs, one placement from qualifying.</div></div>`).join('')||'<div class="empty">Nobody stranded at 1/2.</div>';
const unq=d.atRisk.filter(r=>!r.qualified).map(r=>`<div class="pp-row" style="padding:8px 12px"><div class="pp-icon">⏰</div><div class="pp-body">${link(r)} — NOT qualified (${r.directCount}/2) with <strong style="color:var(--gold)">${fmt(r.atRiskPol)} POL</strong> already forming below. Qualification is urgent for them.</div></div>`).join('');
out.innerHTML=
`<div class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:14px"><div class="fact"><small>Members scanned</small><strong>${d.scanned}</strong></div><div class="fact"><small>POL at risk</small><strong style="color:var(--gold)">${fmt(d.totals.atRiskPol)}</strong></div><div class="fact"><small>Easy wins</small><strong>${d.totals.rollForwardCount}</strong></div><div class="fact"><small>One direct away</small><strong>${d.totals.oneAwayCount}</strong></div></div>`+
`<h3 style="margin:12px 0 6px;font-size:14px">Tier 1 · Easy wins — one message each</h3>${t1}`+
`<h3 style="margin:16px 0 6px;font-size:14px">Tier 2 · Money at risk — coach the upgrade</h3>${t2}`+
(unq?`<h3 style="margin:16px 0 6px;font-size:14px">Tier 2b · Money forming, not yet qualified</h3>${unq}`:'')+
`<h3 style="margin:16px 0 6px;font-size:14px">Tier 3 · One placement from qualified</h3>${t3}`;
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
}
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();
+1 -1
View File
@@ -49,7 +49,7 @@
{k:['tier','standard','premium tier','standard tier','premium vs standard','which tier','half','smaller payment','less than expected','why is my payment'],
a:()=>`There are two tiers. <strong>Premium</strong> is what our whole team builds at (${pol()} POL entry) — full payments. <strong>Standard</strong> costs about half and pays/earns half at every level. So if a payment ever comes in smaller than expected, it usually came from a Standard-tier position below you. Your tier is <strong>set when you join and can't be changed later</strong> (upgrading advances your level, not your tier), so always join <strong>Premium</strong> and make sure the people you bring on do too. Full breakdown: <a href="/how-pay-works">how-pay-works</a>.`},
{k:['dashboard','my dashboard','my page','my position','my team','check my','see my','pipeline','my stats','alerts','notify me','email me','get notified'],
a:()=>`Your <strong>Member Dashboard</strong> is at <a href="/my">rmcircle.team/my</a> — enter your ID to see your position, your team, your payments, your pipeline (money forming below you), and any spillover under you. You can also turn on <strong>email alerts</strong> there to be notified the moment you're paid or need to upgrade. To share, use your personal invite page: <strong>rmcircle.team/join/&lt;your ID&gt;</strong>.`},
a:()=>`Your <strong>Member Dashboard</strong> is at <a href="/my">rmcircle.team/my</a> — enter your ID to see your position, your team (with a depth summary showing members per generation), your payments, your pipeline (money forming below you), any spillover under you, and a <strong>Coach Your Team</strong> panel that tells you exactly who in your leg needs a nudge and what to say. You can also turn on <strong>email alerts</strong> there to be notified the moment you're paid or need to upgrade. To share, use your personal invite page: <strong>rmcircle.team/join/&lt;your ID&gt;</strong>.`},
{k:['level','levels','upgrade','scintilla','ascensus','fabrica','culmen','apex','fastigium','vertex','corona','8 levels'],
a:()=>`There are 8 Premium levels: <strong>Scintilla, Ascensus, Fabrica, Culmen, Apex, Fastigium, Vertex, Corona</strong>. Everyone starts at Scintilla. Upgrade as quickly as practical — ideally using earned POL — because the first two payments at each level are designed to help fund your next upgrade. Stay aware of your active downline's levels so you don't fall behind.`},
{k:['30 positions','goal','milestone','matrix','how many people','team size'],
+1
View File
@@ -19,6 +19,7 @@
<div id="dNextStep"></div>
<div class="table-card" style="margin-bottom:18px"><div style="display:flex;justify-content:space-between;gap:12px;align-items:flex-start;flex-wrap:wrap"><div><h2 style="margin:0 0 4px">Your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 14px">Your position's matrix — the rollup line on each card counts everyone underneath, all the way down. Click a position to drill into that leg. Open slots are where the next placements land.</p></div><button id="dTreeToggle" class="btn btn-secondary btn-sm">List view</button></div><div id="dTreeNav" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="dTree"></div><div id="dGens" style="margin-top:14px"></div><p class="micro" style="margin:14px 0 0"><span class="mtp-qmark" style="position:static;display:inline-grid;vertical-align:middle">✓</span> qualified (2/2 directs) · <span class="mt-badge mt-prem">P</span> Premium · <span class="mt-badge">S</span> Standard · ⬇ everyone below that position (all generations) and the POL they've earned · <span style="color:var(--teal)">↧ spillover</span> = placed there by upline activity; only members who join with a position's own ID count toward its 2/2</p><div id="dSpillNote" class="hidden"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(123,224,161,.4)"><h2 style="margin:0 0 4px">Your pipeline</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Money forming below you. Each generation in your leg pays your position at exactly one level — when a member's level catches up to their depth, their <em>next</em> upgrade comes to you.</p><div id="dPipeline"></div></div>
<div id="dCoachCard" class="table-card" style="margin-bottom:18px;display:none;border-color:rgba(240,197,109,.35)"><h2 style="margin:0 0 4px">Coach your team</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">The fastest way to grow your own income is helping the people below you take their next step. Here's who in <em>your</em> team could use a nudge today — updated live from the blockchain.</p><div id="dCoach"></div></div>
<div class="table-card" style="margin-bottom:18px;border-color:rgba(78,214,203,.35)"><h2 style="margin:0 0 4px">Email me my alerts</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Get an email the moment this position is <strong>paid</strong>, and when it <strong>needs an upgrade</strong> to catch incoming pay — so you never miss one. Opt in with your email; unsubscribe anytime.</p><div id="dAlerts"></div></div>
<div class="table-card" style="margin-bottom:18px"><h2 style="margin:0 0 4px">Share this position</h2><div id="dShare"></div><a class="btn btn-teal btn-sm" href="/tools" style="margin-top:14px">🎬 Promo Tools — posts, swipes, video clips &amp; banners →</a></div>
<div class="table-card" style="margin-bottom:18px;border-color:var(--gold)"><h2 style="margin:0 0 4px">Just joined under this position?</h2><p style="color:var(--muted);font-size:13px;margin:0 0 12px">Welcome to the team! Enter the <strong>new member ID</strong> the RM Circle dApp gave you — we'll verify it on the blockchain and let the team know you're in.</p><form id="dJoinForm" style="display:grid;gap:8px;max-width:480px"><input name="newId" class="input" inputmode="numeric" pattern="[0-9]{1,10}" maxlength="10" placeholder="Your NEW RM Circle ID (numbers only)" required><input name="memberName" class="input" maxlength="60" placeholder="Your name or Telegram @handle" required><button class="btn btn-primary">Submit My ID →</button></form><div id="dJoinMsg" style="margin-top:10px;font-size:14px"></div></div>
+18
View File
@@ -124,6 +124,7 @@
renderNextStep(d);
renderGens(d);
renderPipeline(d);
renderCoach(d);
renderAlerts(d);
renderShare(d);
document.getElementById('dLineage').innerHTML=d.uplineChain&&d.uplineChain.length
@@ -157,6 +158,23 @@
}).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>`;
}
// "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.
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!</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.</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'}.</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.</div></div>`));
if(!rows.length){card.style.display='none';return;}
el.innerHTML=rows.slice(0,8).join('')+`<p class="micro" style="margin:10px 0 0">Why this matters to YOU: every upgrade below you either pays your position directly or builds the depth that will. Helping them is not charity — it's your pipeline.</p>`;
card.style.display='';
}
// "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){