Files
rm-circle-team-router/public/admin.html
T
martbost 05afe5c5f6 Admin: award Suite capacity to any member as a team bonus
Marty asked for a way to grant impressions to anybody, labelled as an admin
bonus. Team Grants already did this for leaders, but only within their own
organisation and only out of a level pool.

adminGrant() is deliberately a different thing: it comes from no pool (the
team is not a position and has no allowance), it can reach ANY registered
position rather than only someone downline, and it carries a reason that
survives in the ledger. Recorded with by:0 so nothing downstream mistakes it
for a leader's grant — the member's own page renders those as "the team"
rather than "#0".

The route verifies the position exists on-chain before crediting it; a typo
would otherwise sit in the ledger crediting nobody, and it would be invisible
until someone went looking.

Awards land on top of whatever the level already allows and expire with the
month like every other allowance. The admin page lists what has been awarded
this month, so bonuses stay auditable rather than vanishing once given.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:40:00 -05:00

85 lines
17 KiB
HTML

<!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>RM Circle Team Build Admin</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css"></head><body class="admin-bg">
<section id="loginView" class="login-panel"><div class="brand" style="margin-bottom:22px"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span>RM Circle<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 class="card" style="margin-bottom:18px">
<h2 style="margin:0">Award Suite Capacity</h2>
<p class="muted" style="margin:6px 0 12px;font-size:13.5px">
Give any registered position extra Suite capacity as a team bonus. It sits on top of whatever their level
already allows, expires with the month like every other allowance, and shows on their side as coming from
&ldquo;the team&rdquo;. Nothing is deducted from anyone &mdash; this is issued, not transferred.
</p>
<div style="display:flex;gap:10px;flex-wrap:wrap;align-items:flex-end">
<label style="font-size:12px">Position<br><input id="agTo" type="number" min="1" placeholder="46" style="width:110px"></label>
<label style="font-size:12px">What<br><select id="agTool" style="min-width:180px"></select></label>
<label style="font-size:12px">How much<br><input id="agN" type="number" min="1" step="500" value="5000" style="width:130px"></label>
<label style="font-size:12px;flex:1;min-width:200px">Reason (shown in the log)<br><input id="agNote" type="text" maxlength="80" placeholder="Launch bonus" style="width:100%"></label>
<button id="agGo" class="btn btn-primary">Award it</button>
</div>
<div id="agMsg" style="margin-top:10px;font-size:14px"></div>
<div style="margin-top:14px">
<div class="muted" style="font-size:12px;text-transform:uppercase;letter-spacing:1px">Bonuses awarded this month</div>
<div class="table-wrap"><table style="width:100%;border-collapse:collapse;font-size:13.5px" id="agLogTbl">
<thead><tr><th style="text-align:left">To</th><th style="text-align:left">What</th><th style="text-align:left">Amount</th><th style="text-align:left">Reason</th><th style="text-align:left">When</th></tr></thead>
<tbody id="agLog"></tbody></table></div>
</div>
</section>
<section id="adminView" class="admin hidden"><div class="admin-top"><div><div class="eyebrow">Sponsor router</div><h1>RM Circle 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 &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="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Member Messages</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Wallet-verified member-to-member messages, newest first. Admin can review for abuse — members are told this in the UI.</p></div><button id="msgRefresh" class="btn btn-secondary btn-sm">Refresh</button></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>From</th><th>To</th><th>Message</th><th>Read by</th></tr></thead><tbody id="msgRows"><tr><td colspan="5" class="empty">Loading…</td></tr></tbody></table></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>
<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>Contact email (optional)</label><input name="email" class="input" type="email" placeholder="member@example.com"></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">Payment Emails (SendGrid)</h2><p style="color:var(--muted);font-size:13px;margin:4px 0 12px">When a payout hits a member whose sponsor record has a contact email, they get a "you've been paid" email automatically. Paste your SendGrid API key (Branded Voice Coolify app → Environment Variables → SENDGRID_API_KEY).</p><div id="emailStatus" class="micro" style="margin-bottom:10px"></div><form id="sgKeyForm"><div class="field"><label>SendGrid API key</label><input name="key" class="input" type="password" placeholder="SG.…" autocomplete="off"></div><button class="btn btn-teal" style="width:100%">Save Key</button><button type="button" id="sgKeyClear" class="btn btn-secondary" style="width:100%;margin-top:8px">Clear Key (disable emails)</button></form><div class="field" style="margin-top:12px"><label>From address (marketingwithmarty.com or mybrandedvoice.com — DKIM)</label><input id="emailFromInput" class="input" placeholder="The RM Circle Team &lt;no-reply@marketingwithmarty.com&gt;"><button type="button" id="emailFromSave" class="btn btn-secondary" style="width:100%;margin-top:8px">Save From Address</button></div></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 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 (group or user)</label><input name="telegramChatId" class="input" placeholder="-1001234567890"></div><div class="field"><label>Telegram topic ID (optional, for forum groups)</label><input name="telegramTopicId" class="input" placeholder="55"></div><div class="field"><label>Team root ID (on-chain team-build alerts)</label><input name="teamRootId" class="input" inputmode="numeric" placeholder="21 — alerts fire for any activity at or below this member ID"></div><div class="field"><label>Team alert email (admin copy of every team-build alert)</label><input name="teamAlertEmail" class="input" type="email" placeholder="you@example.com — emailed for any activity below the team root"></div><div class="field"><label>Upgrade alert email (for your own positions)</label><input name="ownerAlertEmail" class="input" type="email" placeholder="you@example.com — emailed when a position in 'My Positions' needs upgrading to catch incoming pay"></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><script>
(function(){
var $=function(i){return document.getElementById(i)};
var TOOLS=[];
function esc(x){return String(x==null?'':x).replace(/[&<>"']/g,function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]})}
function renderLog(rows){
var tb=$('agLog'); if(!tb) return;
tb.innerHTML=(rows||[]).map(function(r){
var t=TOOLS.filter(function(x){return x.key===r.tool})[0]||{};
return '<tr><td><b>#'+esc(r.to)+'</b></td><td>'+esc(t.label||r.tool)+'</td><td>'+Number(r.n).toLocaleString()+' '+esc(t.unit||'')+
'</td><td>'+esc(r.note||'')+'</td><td>'+esc(String(r.at||'').slice(0,10))+'</td></tr>';
}).join('')||'<tr><td colspan="5" class="muted">None yet this month.</td></tr>';
}
async function boot(){
try{
var r=await fetch('/api/admin/grant'); if(!r.ok) return;
var d=await r.json(); TOOLS=d.tools||[];
$('agTool').innerHTML=TOOLS.map(function(t){return '<option value="'+t.key+'">'+esc(t.label)+'</option>'}).join('');
renderLog(d.log);
}catch(e){}
}
async function award(){
var msg=$('agMsg'); msg.textContent='';
var to=Number($('agTo').value)||0, n=Number($('agN').value)||0;
if(!to||!n){ msg.style.color='#f0a05a'; msg.textContent='Need a position and an amount.'; return; }
$('agGo').disabled=true;
try{
var r=await fetch('/api/admin/grant',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({to:to,tool:$('agTool').value,n:n,note:$('agNote').value})});
var d=await r.json();
if(!r.ok){ msg.style.color='#f0a05a'; msg.textContent=d.error||'Failed.'; }
else{
var t=TOOLS.filter(function(x){return x.key===d.granted.tool})[0]||{};
msg.style.color='var(--teal)';
msg.textContent='Awarded '+Number(d.granted.n).toLocaleString()+' '+(t.unit||'')+' to #'+d.granted.to+
' ('+(d.levelName||'')+'). It is in their allowance now.';
$('agNote').value=''; renderLog(d.log);
}
}catch(e){ msg.style.color='#f0a05a'; msg.textContent='Connection hiccup.'; }
$('agGo').disabled=false;
}
document.addEventListener('DOMContentLoaded',function(){ boot(); var b=$('agGo'); if(b) b.addEventListener('click',award); });
})();
</script>
</body></html>