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:
@@ -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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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
@@ -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 <no-reply@marketingwithmarty.com>"><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 <no-reply@marketingwithmarty.com>"><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 {'&':'&','<':'<','>':'>','"':'"',"'":'''}[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>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user