Initial commit: RM Circle team sponsor router (bridge page, /start, /admin, Docker)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-11 07:03:35 -05:00
commit 76bd288bf6
20 changed files with 512 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Not Found</title><link rel="stylesheet" href="/styles.css"></head><body><main class="wrap hero"><div class="eyebrow">404</div><h1>That page isn't here.</h1><p>Return to the Crypto Team Build strategy page.</p><a class="btn btn-primary" href="/">Go Home</a></main></body></html>
+7
View File
@@ -0,0 +1,7 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex,nofollow"><title>Crypto Team Build Admin</title><link rel="stylesheet" href="/styles.css"></head><body class="admin-bg">
<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="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="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">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>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>
+16
View File
@@ -0,0 +1,16 @@
const loginView=document.getElementById('loginView'),adminView=document.getElementById('adminView'),rows=document.getElementById('sponsorRows'),toast=document.getElementById('toast');let state=null;
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 esc(s){return String(s??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]))}
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>${esc(s.level)}</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>';
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel'])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==='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)}});
loadState();
+17
View File
@@ -0,0 +1,17 @@
(async function(){
try{
const r=await fetch('/api/public/config');
if(!r.ok)return;
const c=await r.json();
document.title=`${c.siteName} | ${c.programName}`;
const sub=document.getElementById('bridgeSubheadline');
if(sub&&c.bridgeSubheadline)sub.textContent=c.bridgeSubheadline;
const h=document.getElementById('bridgeHeadline');
if(h&&c.bridgeHeadline)h.textContent=c.bridgeHeadline;
const qs=window.location.search; if(qs){document.querySelectorAll('a[href="/start"]').forEach(a=>a.href='/start'+qs)}
}catch(e){}
for(const id of ['p8','p16']){
const node=document.getElementById(id); if(!node)continue;
const count=id==='p8'?8:16; for(let i=0;i<count;i++){const s=document.createElement('span');s.className='person';node.appendChild(s)}
}
})();
+11
View File
@@ -0,0 +1,11 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Crypto Team Build evergreen RM Circle Premium strategy bridge page."><title>Crypto Team Build | RM Circle Premium</title><link rel="stylesheet" href="/styles.css"></head>
<body>
<header class="wrap nav"><div class="brand"><div class="brand-mark">RM</div><span>Crypto Team Build<small>RM Circle Premium</small></span></div><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="#strategy">How it works</a><a class="btn btn-primary" href="/start">Get Started</a></div></header>
<main>
<section class="hero wrap"><div class="eyebrow">Premium • Polygon • Team Duplication</div><h1 id="bridgeHeadline"><span class="gold">Get Your 2.</span><br>Help Your 2 Get Their 2.</h1><p id="bridgeSubheadline">A team-first strategy built around qualification, depth, and duplication.</p><div class="hero-actions"><a class="btn btn-primary" href="/start">Show Me the Current Team Placement →</a><a class="btn btn-secondary" href="#strategy">See the Strategy</a></div><div class="micro">Independent team training resource • Participation involves risk • No income is guaranteed</div></section>
<section id="strategy" class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">The strategy</div><h2>Simple enough to duplicate.</h2><p>The goal is not endless personal recruiting. Each position gets two directs, retires that referral link, then helps the next two positions repeat the process.</p></div><div class="grid-3"><article class="card"><div class="card-icon">2</div><h3>Get exactly two</h3><p>Use your position's link until two direct positions are placed beneath you and your position is qualified.</p></article><article class="card"><div class="card-icon">↘</div><h3>Move the effort down</h3><p>Retire the qualified link. Help each of your two directs use their links until they each have two.</p></article><article class="card"><div class="card-icon">↑</div><h3>Upgrade responsibly</h3><p>Use earned POL to advance when practical. Active builders may also choose to self-fund, but only within their own risk tolerance.</p></article></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Moving-link workflow</div><h2>Links retire. Recruiting keeps moving.</h2></div><div class="flow"><div class="flow-step"><b>Step 1</b><strong>Use link</strong><p>Share the current position's referral link.</p></div><div class="flow-step"><b>Step 2</b><strong>Get 2</strong><p>Place exactly two direct positions.</p></div><div class="flow-step"><b>Step 3</b><strong>Retire link</strong><p>Stop creating extra shallow legs.</p></div><div class="flow-step"><b>Step 4</b><strong>Help your 2</strong><p>Shift the team effort to their links.</p></div><div class="flow-step"><b>Step 5</b><strong>Repeat</strong><p>Keep the qualification wave moving down.</p></div></div></div></section>
<section class="section"><div class="wrap"><div class="section-head"><div class="eyebrow">Depth over width</div><h2>2 → 4 → 8 → 16 → 32</h2><p>The first major team milestone is 30 correctly placed positions across the first four generations: 2 + 4 + 8 + 16.</p></div><div class="matrix" aria-label="Matrix growth illustration"><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span></div><b>2</b></div><div class="matrix-group"><div class="people"><span class="person"></span><span class="person"></span><span class="person"></span><span class="person"></span></div><b>4</b></div><div class="matrix-group"><div class="people" id="p8"></div><b>8</b></div><div class="matrix-group"><div class="people" id="p16"></div><b>16</b></div></div><div class="notice"><strong>Team principle:</strong> once your two are in place, do not keep adding more directs to the same qualified link. Help the next positions become qualified so the matrix develops depth instead of extra shallow legs.</div></div></section>
<section class="section"><div class="wrap"><div class="card" style="text-align:center;padding:34px"><div class="eyebrow">Ready to start?</div><h2 style="font-size:38px;margin:10px 0">See the current team placement.</h2><p style="max-width:680px;margin:0 auto 20px;color:var(--muted)">The onboarding page automatically shows the sponsor position the team is currently helping. Always use the sponsor shown there instead of an old screenshot or saved link.</p><a class="btn btn-primary" href="/start">Open Getting Started Instructions →</a></div></div></section>
</main><footer class="wrap disclaimer">This independent team page is educational and is not an earnings guarantee or investment advice. Cryptocurrency and smart-contract participation involve risk, including possible loss of funds. Never use funds you cannot afford to lose. Results depend on actual participation, qualification, upgrades, smart-contract behavior, and the market value of POL.<div class="footer-links"><a href="/start">Getting Started</a><a href="/admin">Team Admin</a></div></footer>
<script src="/bridge.js"></script></body></html>
+15
View File
@@ -0,0 +1,15 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="description" content="Current Crypto Team Build onboarding and sponsor placement."><title>Get Started | Crypto Team Build</title><link rel="stylesheet" href="/styles.css"></head>
<body>
<header class="wrap nav"><div class="brand"><div class="brand-mark">RM</div><span>Crypto Team Build<small>Getting Started</small></span></div><div class="nav-actions"><a class="btn btn-secondary" href="/">← Strategy</a></div></header>
<main class="sponsor-shell"><div class="wrap"><div class="section-head"><div class="eyebrow">Current placement</div><h1 style="font-size:clamp(34px,5vw,54px);margin:8px 0 10px">Use the sponsor shown on <span class="gold">this page.</span></h1><p>Team placements rotate as positions become qualified. Do not use an old screenshot or saved referral link.</p></div>
<div class="sponsor-grid"><aside class="sponsor-card"><span class="live-badge"><span class="dot"></span> Current team sponsor</span><div class="sponsor-id" id="sponsorId">—</div><div class="sponsor-name" id="sponsorName">Loading current placement…</div><div id="progressArea"><div style="display:flex;justify-content:space-between;color:var(--muted);font-size:13px"><span>Qualification progress</span><strong id="progressText">0 / 2</strong></div><div class="progress-line"><span id="progressBar" style="width:0%"></span></div></div><div class="facts"><div class="fact"><small>Program</small><strong>Premium</strong></div><div class="fact"><small>Entry</small><strong><span id="entryPol">362</span> POL</strong></div><div class="fact"><small>Network</small><strong>Polygon</strong></div><div class="fact"><small>Gas token</small><strong>POL</strong></div></div><a id="joinButton" class="btn btn-primary" style="width:100%" href="#">Join With Current Sponsor →</a><button id="copyButton" class="btn btn-secondary" style="width:100%;margin-top:9px">Copy Sponsor ID</button><p class="micro" id="queueText" style="text-align:center"></p></aside>
<section><div class="instruction-list"><article class="instruction"><div class="num">1</div><div><h3>Install or open MetaMask</h3><p>Use the official MetaMask website or your device's official app store. Never install a wallet from a link sent by a stranger.</p><div style="margin-top:10px"><a class="btn btn-secondary btn-sm" href="https://metamask.io/" target="_blank" rel="noopener noreferrer">Official MetaMask Site ↗</a></div></div></article>
<article class="instruction"><div class="num">2</div><div><h3>Use Polygon Mainnet</h3><p>In MetaMask, select Polygon. Polygon Mainnet uses chain ID <strong>137</strong>, and POL is the native gas token.</p></div></article>
<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>
<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></div></main>
<footer class="wrap disclaimer">This is an independent Crypto Team Build onboarding resource, not an owner/principal page. Always confirm transaction details in your wallet before signing. Never disclose your Secret Recovery Phrase.</footer>
<script src="/start.js"></script></body></html>
+29
View File
@@ -0,0 +1,29 @@
let currentSponsor=null;
async function load(){
try{
const [cr,sr]=await Promise.all([fetch('/api/public/config'),fetch('/api/public/current-sponsor')]);
const c=await cr.json(); const s=await sr.json();
if(!sr.ok)throw new Error(s.error||'No sponsor assigned');
currentSponsor=s.sponsor;
document.getElementById('sponsorId').textContent=`ID ${currentSponsor.id}`;
document.getElementById('sponsorIdInline').textContent=currentSponsor.id;
document.getElementById('sponsorName').textContent=currentSponsor.name||'Current Crypto Team Build placement';
document.getElementById('progressText').textContent=`${currentSponsor.directs} / 2`;
document.getElementById('progressBar').style.width=`${Math.min(100,(currentSponsor.directs/2)*100)}%`;
document.getElementById('entryPol').textContent=c.premiumEntryPol;
document.getElementById('entryPol2').textContent=c.premiumEntryPol;
document.getElementById('joinButton').href=currentSponsor.referralUrl;
document.getElementById('queueText').textContent=c.showQueueProgress?`${s.waitingCount} team placement${s.waitingCount===1?'':'s'} waiting behind the current sponsor.`:'';
document.getElementById('supportBox').textContent=c.supportLabel||'Contact your team sponsor if you need help before joining.';
if(c.telegramUrl){const w=document.getElementById('supportLinkWrap'),a=document.getElementById('supportLink');a.href=c.telegramUrl;w.classList.remove('hidden')}
}catch(e){
document.getElementById('sponsorName').textContent=e.message;
document.getElementById('joinButton').classList.add('hidden');
document.getElementById('copyButton').classList.add('hidden');
}
}
document.getElementById('joinButton').addEventListener('click',()=>{fetch('/api/public/join-click',{method:'POST'}).catch(()=>{})});
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)
});
load();
+10
View File
File diff suppressed because one or more lines are too long