Admin: move the bonus panel script out of the page (CSP)

The site sends script-src 'self'. The panel's logic was an inline <script>
block, so the browser refused to run it: window.agBoot was never defined,
admin.js's guarded call did nothing, and the "What" select stayed empty with
no console-visible cause beyond the CSP report.

admin.html had no inline scripts before this panel — the extraction restores
that, so the next person adding a panel copies a pattern that works.

Fourth thing this CSP has silently broken. Inline scripts are not an option
on this site; new panels ship as files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-30 06:00:37 -05:00
parent e4b7d64605
commit 77dcda6a1a
2 changed files with 50 additions and 50 deletions
+49
View File
@@ -0,0 +1,49 @@
// Award Suite Capacity panel. Lives in its own file because the site's CSP
// is script-src 'self' — an inline <script> in admin.html is silently blocked.
(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;
}
window.agBoot=boot;
document.addEventListener('DOMContentLoaded',function(){
var b=$('agGo'); if(b) b.addEventListener('click',award);
// Belt and braces: if the select is still empty when it's opened, try again.
var sel=$('agTool'); if(sel) sel.addEventListener('focus',function(){ if(!sel.options.length) boot(); });
});
})();
+1 -50
View File
@@ -37,53 +37,4 @@
<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">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">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 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> <div id="toast" class="toast"></div><script src="/admin.js"></script><script src="/admin-grant.js"></script></body></html>
(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;
}
window.agBoot=boot;
document.addEventListener('DOMContentLoaded',function(){
var b=$('agGo'); if(b) b.addEventListener('click',award);
// Belt and braces: if the select is still empty when it's opened, try again.
var sel=$('agTool'); if(sel) sel.addEventListener('focus',function(){ if(!sel.options.length) boot(); });
});
})();
</script>
</body></html>