Files
rm-circle-team-router/public/admin-grant.js
T
martbost 77dcda6a1a 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>
2026-08-30 06:00:37 -05:00

50 lines
2.6 KiB
JavaScript

// 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(); });
});
})();