Close the conversion loop: member ID submission with Hermes Telegram notify
- Start page step 6 is now a bold gold-highlighted "Submit your NEW RM Circle ID" form with larger step numbers throughout; captures the sponsor the visitor was shown at join time. - POST /api/public/submit-id: validates numeric ID, dedupes, stores on the volume, records a per-source "purchase" analytics event, fires a BeMob postback (txid=purchase-<clickid>) to close paid campaigns, and posts the new ID + sponsor + source to the configured Telegram chat (MB Hermes pattern). Rate-limited. - Admin: Member ID Submissions table, "Confirmed joins" tile, and Telegram bot token / chat ID settings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+3
-2
@@ -2,8 +2,9 @@
|
||||
<section id="loginView" class="login-panel"><div class="brand" style="margin-bottom:22px"><div class="brand-mark">RM</div><span>Crypto Team Build<small>Sponsor Router Admin</small></span></div><h1>Team Admin</h1><p>Manage the current sponsor, qualification queue, and onboarding settings.</p><form id="loginForm"><div class="field"><label for="password">Admin password</label><input id="password" class="input" type="password" autocomplete="current-password" required></div><button class="btn btn-primary" style="width:100%">Sign in</button><div id="loginError" class="micro" style="color:var(--danger)"></div></form></section>
|
||||
<section id="adminView" class="admin hidden"><div class="admin-top"><div><div class="eyebrow">Sponsor router</div><h1>Crypto Team Build Admin</h1></div><div class="nav-actions"><a class="btn btn-secondary" href="/start" target="_blank">View Live Page ↗</a><button id="logoutBtn" class="btn btn-secondary">Log out</button></div></div>
|
||||
<div class="admin-grid"><div class="stack"><div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px"><div><h2 style="margin:0">Sponsor Queue</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Mark a sponsor qualified to automatically activate the next waiting position.</p></div></div><div class="table-wrap"><table class="table"><thead><tr><th>Order</th><th>Sponsor</th><th>Parent</th><th>Directs</th><th>Level</th><th>Status</th><th>Clicks</th><th>Actions</th></tr></thead><tbody id="sponsorRows"></tbody></table></div></div>
|
||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & 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>Join clicks</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div></div>
|
||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & 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>Join clicks</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>New ID</th><th>Joined under</th><th>Source</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div></div>
|
||||
<div class="stack"><div class="table-card"><h2 style="margin-top:0">Add Sponsor</h2><form id="addSponsorForm"><div class="form-grid"><div class="field"><label>ID</label><input name="id" class="input" required></div><div class="field"><label>Name</label><input name="name" class="input" required></div><div class="field"><label>Parent ID</label><input name="parentId" class="input"></div><div class="field"><label>Level</label><select name="level" class="select"><option>Scintilla</option><option>Ascensus</option><option>Fabrica</option><option>Culmen</option><option>Apex</option><option>Fastigium</option><option>Vertex</option><option>Corona</option></select></div></div><div class="field"><label>Notes</label><input name="notes" class="input"></div><button class="btn btn-teal" style="width:100%">Add to Queue</button></form></div>
|
||||
<div class="table-card"><h2 style="margin-top:0">AI Chat</h2><p style="color:var(--muted);font-size:13px;margin:4px 0 12px">Paste an OpenRouter API key to switch the help chat from canned answers to AI (<span id="aiModel"></span>). Clear it to switch back.</p><div id="aiStatus" class="micro" style="margin-bottom:10px"></div><form id="aiKeyForm"><div class="field"><label>OpenRouter API key</label><input name="key" class="input" type="password" placeholder="sk-or-v1-…" autocomplete="off"></div><button class="btn btn-teal" style="width:100%">Save Key</button><button type="button" id="aiKeyClear" class="btn btn-secondary" style="width:100%;margin-top:8px">Clear Key (use canned answers)</button></form></div>
|
||||
<div class="table-card"><h2 style="margin-top:0">Public Page Settings</h2><form id="configForm"><div class="field"><label>Site name</label><input name="siteName" class="input"></div><div class="field"><label>Program name</label><input name="programName" class="input"></div><div class="field"><label>Bridge headline</label><input name="bridgeHeadline" class="input"></div><div class="field"><label>Bridge subheadline</label><textarea name="bridgeSubheadline" class="input" rows="3"></textarea></div><div class="field"><label>Premium entry (POL)</label><input name="premiumEntryPol" class="input" type="number"></div><div class="field"><label>RM dApp referral base URL</label><input name="dappReferralBaseUrl" class="input" placeholder="https://app.thermcircle.com?ref="></div><div class="field"><label>BeMob postback URL (paid traffic)</label><input name="bemobPostbackUrl" class="input" placeholder="https://xxxxx.bemobtrcks.com/postback"></div><div class="field"><label>Telegram/support URL (optional)</label><input name="telegramUrl" class="input"></div><div class="field"><label>Support message</label><textarea name="supportLabel" class="input" rows="3"></textarea></div><label style="text-transform:none;letter-spacing:0;margin:10px 0"><input type="checkbox" name="showSponsorName"> Show sponsor name publicly</label><label style="text-transform:none;letter-spacing:0;margin:10px 0"><input type="checkbox" name="showQueueProgress"> Show number waiting in queue</label><button class="btn btn-primary" style="width:100%;margin-top:8px">Save Settings</button></form></div></div></div></section>
|
||||
<div class="table-card"><h2 style="margin-top:0">Public Page Settings</h2><form id="configForm"><div class="field"><label>Site name</label><input name="siteName" class="input"></div><div class="field"><label>Program name</label><input name="programName" class="input"></div><div class="field"><label>Bridge headline</label><input name="bridgeHeadline" class="input"></div><div class="field"><label>Bridge subheadline</label><textarea name="bridgeSubheadline" class="input" rows="3"></textarea></div><div class="field"><label>Premium entry (POL)</label><input name="premiumEntryPol" class="input" type="number"></div><div class="field"><label>RM dApp referral base URL</label><input name="dappReferralBaseUrl" class="input" placeholder="https://app.thermcircle.com?ref="></div><div class="field"><label>BeMob postback URL (paid traffic)</label><input name="bemobPostbackUrl" class="input" placeholder="https://xxxxx.bemobtrcks.com/postback"></div><div class="field"><label>Telegram bot token (Hermes notifications)</label><input name="telegramBotToken" class="input" type="password" autocomplete="off" placeholder="123456:ABC…"></div><div class="field"><label>Telegram chat ID (Hermes notifications)</label><input name="telegramChatId" class="input" placeholder="1289244227"></div><div class="field"><label>Telegram/support URL (optional)</label><input name="telegramUrl" class="input"></div><div class="field"><label>Support message</label><textarea name="supportLabel" class="input" rows="3"></textarea></div><label style="text-transform:none;letter-spacing:0;margin:10px 0"><input type="checkbox" name="showSponsorName"> Show sponsor name publicly</label><label style="text-transform:none;letter-spacing:0;margin:10px 0"><input type="checkbox" name="showQueueProgress"> Show number waiting in queue</label><button class="btn btn-primary" style="width:100%;margin-top:8px">Save Settings</button></form></div></div></div></section>
|
||||
<div id="toast" class="toast"></div><script src="/admin.js"></script></body></html>
|
||||
|
||||
+6
-3
@@ -8,8 +8,8 @@ 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};
|
||||
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;return{name,bridge:r.bridge||0,start:r.start||0,click:r.click||0,training:r.training||0}})
|
||||
const tot={bridge:0,start:0,click:0,training:0,postback:0,purchase: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;return{name,bridge:r.bridge||0,start:r.start||0,click:r.click||0,training:r.training||0}})
|
||||
.sort((a,b)=>b.click-a.click||b.start-a.start||b.bridge-a.bridge);
|
||||
document.getElementById('funnelStats').innerHTML=
|
||||
`<div class="fact"><small>Bridge views</small><strong>${tot.bridge}</strong></div>`+
|
||||
@@ -17,7 +17,10 @@ function renderAnalytics(){
|
||||
`<div class="fact"><small>Join clicks</small><strong>${tot.click}</strong></div>`+
|
||||
`<div class="fact"><small>Bridge → Start</small><strong>${pct(tot.start,tot.bridge)}</strong></div>`+
|
||||
`<div class="fact"><small>Start → Join</small><strong>${pct(tot.click,tot.start)}</strong></div>`+
|
||||
(tot.purchase?`<div class="fact"><small>Confirmed joins</small><strong>${tot.purchase}</strong></div>`:'')+
|
||||
(tot.postback?`<div class="fact"><small>BeMob postbacks</small><strong>${tot.postback}</strong></div>`:'');
|
||||
const subs=state.submissions||[];
|
||||
document.getElementById('submissionRows').innerHTML=subs.map(s=>`<tr><td>${esc((s.ts||'').replace('T',' ').slice(0,16))}</td><td><strong>${esc(s.newId)}</strong></td><td>ID ${esc(s.sponsorId)}</td><td>${esc(s.source)}</td></tr>`).join('')||'<tr><td colspan="4" class="empty">No submissions yet.</td></tr>';
|
||||
document.getElementById('trafficRows').innerHTML=list.map(r=>`<tr><td><strong>${esc(r.name)}</strong></td><td>${r.bridge}</td><td>${r.start}</td><td>${r.training}</td><td>${r.click}</td><td>${pct(r.click,r.start)}</td></tr>`).join('')||'<tr><td colspan="6" class="empty">No traffic recorded yet.</td></tr>';
|
||||
}
|
||||
function esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))}
|
||||
@@ -25,7 +28,7 @@ function render(){
|
||||
const sponsors=[...state.sponsors].sort((a,b)=>a.sortOrder-b.sortOrder);rows.innerHTML=sponsors.map((s,i)=>`<tr><td>${i+1}</td><td><strong>${esc(s.name)}</strong><div class="micro" style="margin:2px 0 0">ID ${esc(s.id)}</div></td><td>${esc(s.parentId||'—')}</td><td><strong>${s.directs}/2</strong></td><td>${levelSelect(s)}</td><td><span class="${statusClass(s.status)}">${esc(s.status)}</span></td><td>${s.clicks||0}</td><td><div class="actions">${s.status!=='qualified'?`<button class="btn btn-secondary btn-sm" data-action="inc" data-id="${esc(s.id)}">+1 Direct</button><button class="btn btn-teal btn-sm" data-action="qualify" data-id="${esc(s.id)}">Qualified</button>`:`<button class="btn btn-secondary btn-sm" data-action="reset" data-id="${esc(s.id)}">Reset</button>`}${s.status==='waiting'?`<button class="btn btn-secondary btn-sm" data-action="activate" data-id="${esc(s.id)}">Make Active</button>`:''}<button class="btn btn-secondary btn-sm" data-action="up" data-id="${esc(s.id)}">↑</button><button class="btn btn-secondary btn-sm" data-action="down" data-id="${esc(s.id)}">↓</button><button class="btn btn-danger btn-sm" data-action="delete" data-id="${esc(s.id)}">×</button></div></td></tr>`).join('')||'<tr><td colspan="8" class="empty">No sponsors in the queue.</td></tr>';
|
||||
renderAnalytics();
|
||||
const ai=state.aiChat||{};document.getElementById('aiModel').textContent=ai.model||'OpenRouter';document.getElementById('aiStatus').innerHTML=ai.configured?'<span style="color:var(--ok)">● AI chat is ON</span> — key is set':'<span style="color:var(--muted)">○ AI chat is OFF</span> — using built-in answers';
|
||||
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;
|
||||
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId'])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()});
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
<article class="instruction"><div class="num">3</div><div><h3>Fund your wallet with POL</h3><p>Premium entry is <strong><span id="entryPol2">362</span> POL</strong>. Keep a small additional POL balance available for network gas. POL's USD value changes with the market.</p></div></article>
|
||||
<article class="instruction"><div class="num">4</div><div><h3>Use the current team sponsor</h3><p>Confirm that the sponsor ID shown by the RM Circle dApp matches <strong>ID <span id="sponsorIdInline">—</span></strong> before completing the transaction.</p></div></article>
|
||||
<article class="instruction"><div class="num">5</div><div><h3>Select Premium and review the transaction</h3><p>Read the wallet transaction before signing. Never share your Secret Recovery Phrase or seed words with this site, a sponsor, or a dApp.</p></div></article>
|
||||
<article class="instruction"><div class="num">6</div><div><h3>Tell the team when you're in</h3><p>Once your position is confirmed, let the team know your new RM Circle ID and sponsor ID so your placement can be mapped correctly.</p></div></article></div>
|
||||
<article class="instruction" style="border-color:var(--gold);background:rgba(243,190,67,.05)"><div class="num" style="background:var(--gold);color:#071421;border-color:var(--gold)">6</div><div><h3>Submit your NEW RM Circle ID here</h3><p>After your purchase confirms, the RM Circle dApp shows your <strong>new member ID</strong>. Type it below and hit submit — this tells the team you're in, maps your placement, and puts you in line for the rotation that gets <strong>your</strong> 2.</p><form id="idSubmitForm" style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap"><input name="newId" class="input" inputmode="numeric" pattern="[0-9]{1,10}" maxlength="10" placeholder="Your NEW RM Circle ID (numbers only)" required style="flex:1;min-width:200px"><button class="btn btn-primary">Submit My ID →</button></form><div id="idSubmitMsg" style="margin-top:10px;font-size:14px"></div></div></article></div>
|
||||
<div class="callout" style="margin-top:16px"><strong>What happens next:</strong> get exactly 2 directs, retire your qualified referral link, help your 2 get their 2, and upgrade with earned POL when practical. The goal is depth and duplication—not endless directs on one link.</div>
|
||||
<div class="callout warning" style="margin-top:12px"><strong>Risk reminder:</strong> participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.</div><div id="supportBox" class="notice" style="margin-top:12px"></div><div id="supportLinkWrap" class="hidden" style="margin-top:10px"><a id="supportLink" class="btn btn-secondary" target="_blank" rel="noopener noreferrer">Open Team Support ↗</a></div></section></div>
|
||||
<figure class="roadmap-figure"><a href="/roadmap.webp" target="_blank" rel="noopener"><img src="/roadmap.webp" alt="RM Circle Premium Team Build Roadmap — core strategy, step-by-step guide, premium levels, and duplication formula" width="1149" height="1369" loading="lazy"></a><figcaption>The RM Circle is a team build project of the <strong>Crypto Team Build Network</strong>. This roadmap is the plan every member follows — tap to view full size.</figcaption></figure></div></main>
|
||||
|
||||
+18
-1
@@ -23,7 +23,24 @@ async function load(){
|
||||
document.getElementById('copyButton').classList.add('hidden');
|
||||
}
|
||||
}
|
||||
document.getElementById('joinButton').addEventListener('click',()=>{fetch('/api/public/join-click',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:window.ctbGetSource?window.ctbGetSource():'(direct)',clickid:window.ctbGetClickId?window.ctbGetClickId():''})}).catch(()=>{})});
|
||||
document.getElementById('joinButton').addEventListener('click',()=>{try{if(currentSponsor)sessionStorage.setItem('ctb.joinedSponsor',currentSponsor.id)}catch(e){};fetch('/api/public/join-click',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({source:window.ctbGetSource?window.ctbGetSource():'(direct)',clickid:window.ctbGetClickId?window.ctbGetClickId():''})}).catch(()=>{})});
|
||||
document.getElementById('idSubmitForm').addEventListener('submit',async e=>{
|
||||
e.preventDefault();
|
||||
const form=e.currentTarget,input=form.elements.newId,msg=document.getElementById('idSubmitMsg');
|
||||
const newId=input.value.trim();
|
||||
if(!/^[0-9]{1,10}$/.test(newId)){msg.style.color='var(--danger)';msg.textContent='Numbers only — the ID shown by the RM Circle dApp.';return}
|
||||
let joinedSponsor='';try{joinedSponsor=sessionStorage.getItem('ctb.joinedSponsor')||''}catch(e){}
|
||||
const sponsorId=joinedSponsor||(currentSponsor?currentSponsor.id:'?');
|
||||
msg.style.color='var(--muted)';msg.textContent='Submitting…';
|
||||
try{
|
||||
const r=await fetch('/api/public/submit-id',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({newId,sponsorId,source:window.ctbGetSource?window.ctbGetSource():'(direct)',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)';
|
||||
msg.innerHTML=d.duplicate?`✓ ID <strong>${newId}</strong> was already submitted — you're on the list.`:`✓ Got it! ID <strong>${newId}</strong> is submitted under sponsor ID <strong>${sponsorId}</strong>. The team has been notified and you'll be added to the rotation.`;
|
||||
}catch(x){msg.style.color='var(--danger)';msg.textContent=x.message}
|
||||
});
|
||||
document.getElementById('copyButton').addEventListener('click',async()=>{
|
||||
if(!currentSponsor)return; await navigator.clipboard.writeText(currentSponsor.id); const b=document.getElementById('copyButton'); const old=b.textContent;b.textContent='Copied ✓';setTimeout(()=>b.textContent=old,1400)
|
||||
});
|
||||
|
||||
@@ -46,3 +46,6 @@ a{color:inherit}.wrap{width:min(1160px,calc(100% - 32px));margin:auto}.nav{heigh
|
||||
.chat-hidden{display:none}
|
||||
.chat-typing{letter-spacing:3px;animation:chatpulse 1s infinite}
|
||||
@keyframes chatpulse{50%{opacity:.35}}
|
||||
|
||||
.instruction{grid-template-columns:56px 1fr}
|
||||
.num{width:56px;height:56px;font-size:28px;border-radius:14px}
|
||||
|
||||
Reference in New Issue
Block a user