Make event pop-up admin-toggleable; drop it from join pages

The Weekly Huddle overlay now reads its content from /api/announce (config-
driven) instead of a baked-in block, and Admin -> Settings -> Event pop-up
exposes a toggle plus editable fields (eyebrow, flyer, join URL, date, times,
auto-hide, event id). Defaults are seeded into config on boot so the panel
reflects real state. Removed the include from join.html so it never interrupts
the join flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-08 05:28:03 -05:00
parent be7cb93bc5
commit e3f6b00f95
5 changed files with 151 additions and 114 deletions
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -46,12 +46,12 @@ function render(){
if(!grantAutoLoaded){grantAutoLoaded=true;if(window.agBoot)window.agBoot();}
if(!msgsAutoLoaded){msgsAutoLoaded=true;loadAdminMsgs();}
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail','directDefaultIds'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;
const f=document.getElementById('configForm'),c=state.config;for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','bemobPostbackUrl','telegramBotToken','telegramChatId','telegramTopicId','teamRootId','teamAlertEmail','ownerAlertEmail','directDefaultIds','announceEyebrow','announceImg','announceMeetUrl','announceDateLabel','announceTimes','announceExpiresUTC','announceId'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;if(f.elements.announceEnabled)f.elements.announceEnabled.checked=c.announceEnabled===undefined?true:!!c.announceEnabled;
}
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)}});
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.announceEnabled=!!(f.elements.announceEnabled&&f.elements.announceEnabled.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==='rename'){const cur=(state.sponsors.find(s=>s.id===id)||{}).name||'';const nm=prompt(`Name for sponsor ID ${id}:`,cur);if(nm===null||!nm.trim())return;d=await api(`/api/admin/sponsors/${id}`,{method:'PATCH',body:JSON.stringify({name:nm.trim()})});}else if(a==='email'){const cur=(state.sponsors.find(s=>s.id===id)||{}).email||'';const em=prompt(`Contact email for sponsor ID ${id} (leave empty to clear):`,cur);if(em===null)return;d=await api(`/api/admin/sponsors/${id}`,{method:'PATCH',body:JSON.stringify({email:em.trim()})});}else 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)}});
document.getElementById('aiKeyForm').addEventListener('submit',async e=>{e.preventDefault();const inp=e.currentTarget.elements.key,key=inp.value.trim();if(!key){showToast('Paste a key first');return}try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key})});state.aiChat={...(state.aiChat||{}),configured:d.configured};inp.value='';render();showToast('AI chat enabled')}catch(x){showToast(x.message)}});
document.getElementById('aiKeyClear').addEventListener('click',async()=>{if(!confirm('Turn off AI chat and go back to built-in answers?'))return;try{const d=await api('/api/admin/openrouter-key',{method:'POST',body:JSON.stringify({key:''})});state.aiChat={...(state.aiChat||{}),configured:d.configured};render();showToast('AI chat disabled')}catch(x){showToast(x.message)}});
+118 -109
View File
@@ -1,114 +1,123 @@
/* announce.js — dismissible event pop-up overlay (RM Circle).
Weekly one-off: edit the EV block below each week (or set enabled:false to hide).
Self-gating: shows once per browser until dismissed, and never after `expiresUTC`. */
Content is admin-controlled: fetched from /api/announce (edit it under
Admin → Settings → Event pop-up; no deploy needed). Self-gating: shows once
per browser until dismissed, and never after `expiresUTC`. */
(function () {
var EV = {
id: 'huddle-2026-09-08', // bump this when the event changes (resets dismissals)
enabled: true,
img: '/huddle-flyer.jpg',
meet: 'https://meet.google.com/gsw-yhqn-zrc',
eyebrow: 'Team RM Circle · Weekly Huddle',
dateLabel: 'Tuesday, September 8',
times: [
['🇺🇸', '7:00 PM CST', 'USA / Canada'],
['🇺🇸', '8:00 PM EST', ''],
['🇹🇹', '8:00 PM AST', 'Caribbean'],
['🇬🇧', '1:00 AM', 'UK']
],
expiresUTC: '2026-09-09T02:30:00Z' // ~9:30 PM CT Sept 8 (after the huddle ends)
};
if (!EV.enabled) return;
if (Date.now() > Date.parse(EV.expiresUTC)) return;
var KEY = 'rmc-announce-' + EV.id;
try { if (localStorage.getItem(KEY) === 'done') return; } catch (e) {}
function dismiss() { try { localStorage.setItem(KEY, 'done'); } catch (e) {} close(); }
var back, card;
function close() {
if (!back) return;
back.style.opacity = '0';
setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260);
document.removeEventListener('keydown', onKey);
}
function onKey(e) { if (e.key === 'Escape') dismiss(); }
function build() {
back = document.createElement('div');
back.setAttribute('role', 'dialog');
back.setAttribute('aria-label', 'Team RM Circle Weekly Huddle invitation');
back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;' +
'padding:20px;background:rgba(3,7,14,.82);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' +
'opacity:0;transition:opacity .28s ease;overflow:auto;';
back.addEventListener('click', function (e) { if (e.target === back) dismiss(); });
card = document.createElement('div');
card.style.cssText = 'position:relative;width:100%;max-width:428px;max-height:94vh;overflow:auto;border-radius:20px;' +
'background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7),0 0 0 1px rgba(212,175,55,.15);' +
'font-family:system-ui,Segoe UI,Arial,sans-serif;';
var x = document.createElement('button');
x.setAttribute('aria-label', 'Close');
x.innerHTML = '&times;';
x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;' +
'background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;' +
'box-shadow:0 2px 10px rgba(0,0,0,.5);';
x.addEventListener('click', dismiss);
var eyebrow = document.createElement('div');
eyebrow.textContent = EV.eyebrow;
eyebrow.style.cssText = 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;' +
'color:#d4af37;padding:14px 44px 10px;';
var img = document.createElement('img');
img.src = EV.img;
img.alt = 'You are invited — RM Circle online presentation, Tuesday 7:00 PM Central, via Google Meet';
img.style.cssText = 'display:block;width:100%;height:auto;';
var foot = document.createElement('div');
foot.style.cssText = 'padding:16px 20px 20px;';
var dateRow = document.createElement('div');
dateRow.innerHTML = '🗓 <b>' + EV.dateLabel + '</b>';
dateRow.style.cssText = 'text-align:center;color:#eef2f7;font-size:15px;margin-bottom:12px;';
var grid = document.createElement('div');
grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;';
EV.times.forEach(function (t) {
var cell = document.createElement('div');
cell.style.cssText = 'background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.22);border-radius:10px;padding:8px 10px;';
cell.innerHTML = '<div style="font-size:15px;font-weight:800;color:#f3c34a">' + t[1] + '</div>' +
(t[2] ? '<div style="font-size:11px;color:#9fb0c2;margin-top:1px">' + t[2] + '</div>' : '<div style="font-size:11px;color:#9fb0c2;margin-top:1px">&nbsp;</div>');
grid.appendChild(cell);
});
var join = document.createElement('a');
join.href = EV.meet;
join.target = '_blank';
join.rel = 'noopener';
join.textContent = 'Join Google Meet →';
join.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);' +
'color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);';
join.addEventListener('click', function () { try { localStorage.setItem(KEY, 'done'); } catch (e) {} });
var link = document.createElement('div');
link.textContent = 'meet.google.com/gsw-yhqn-zrc';
link.style.cssText = 'text-align:center;color:#9fb0c2;font-size:13px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;';
var later = document.createElement('button');
later.textContent = 'Maybe later';
later.style.cssText = 'display:block;margin:12px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;';
later.addEventListener('click', dismiss);
foot.appendChild(dateRow); foot.appendChild(grid); foot.appendChild(join); foot.appendChild(link); foot.appendChild(later);
card.appendChild(x); card.appendChild(eyebrow); card.appendChild(img); card.appendChild(foot);
back.appendChild(card);
document.body.appendChild(back);
document.addEventListener('keydown', onKey);
requestAnimationFrame(function () { back.style.opacity = '1'; });
function parseTimes(s) {
return String(s || '').split(/\r?\n+/).map(function (l) { return l.trim(); }).filter(Boolean)
.map(function (l) { var i = l.indexOf('|'); return i < 0 ? [l, ''] : [l.slice(0, i).trim(), l.slice(i + 1).trim()]; });
}
function go() { setTimeout(build, 1100); }
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go);
else go();
function start(EV) {
if (!EV || !EV.enabled || !EV.id || !EV.img) return;
if (EV.expiresUTC) { var exp = Date.parse(EV.expiresUTC); if (exp && Date.now() > exp) return; }
var KEY = 'rmc-announce-' + EV.id;
try { if (localStorage.getItem(KEY) === 'done') return; } catch (e) {}
EV.timesArr = parseTimes(EV.times);
var back, card;
function done() { try { localStorage.setItem(KEY, 'done'); } catch (e) {} }
function close() {
if (!back) return;
back.style.opacity = '0';
setTimeout(function () { if (back && back.parentNode) back.parentNode.removeChild(back); back = null; }, 260);
document.removeEventListener('keydown', onKey);
}
function dismiss() { done(); close(); }
function onKey(e) { if (e.key === 'Escape') dismiss(); }
function build() {
back = document.createElement('div');
back.setAttribute('role', 'dialog');
back.setAttribute('aria-label', (EV.eyebrow || 'Event') + ' invitation');
back.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;justify-content:center;' +
'padding:20px;background:rgba(3,7,14,.82);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);' +
'opacity:0;transition:opacity .28s ease;overflow:auto;';
back.addEventListener('click', function (e) { if (e.target === back) dismiss(); });
card = document.createElement('div');
card.style.cssText = 'position:relative;width:100%;max-width:428px;max-height:94vh;overflow:auto;border-radius:20px;' +
'background:#0a1119;border:1px solid rgba(212,175,55,.55);box-shadow:0 24px 70px rgba(0,0,0,.7),0 0 0 1px rgba(212,175,55,.15);' +
'font-family:system-ui,Segoe UI,Arial,sans-serif;';
var x = document.createElement('button');
x.setAttribute('aria-label', 'Close');
x.innerHTML = '&times;';
x.style.cssText = 'position:absolute;top:10px;right:12px;z-index:2;width:38px;height:38px;border-radius:50%;border:none;cursor:pointer;' +
'background:rgba(6,10,16,.72);color:#fff;font-size:24px;line-height:1;font-weight:700;display:grid;place-items:center;box-shadow:0 2px 10px rgba(0,0,0,.5);';
x.addEventListener('click', dismiss);
var eyebrow = document.createElement('div');
eyebrow.textContent = EV.eyebrow || '';
eyebrow.style.cssText = 'text-align:center;letter-spacing:2px;text-transform:uppercase;font-size:11px;font-weight:700;color:#d4af37;padding:14px 44px 10px;';
var img = document.createElement('img');
img.src = EV.img;
img.alt = EV.eyebrow || 'Event flyer';
img.style.cssText = 'display:block;width:100%;height:auto;';
var foot = document.createElement('div');
foot.style.cssText = 'padding:16px 20px 20px;';
if (EV.dateLabel) {
var dateRow = document.createElement('div');
dateRow.innerHTML = '🗓 <b></b>';
dateRow.querySelector('b').textContent = EV.dateLabel;
dateRow.style.cssText = 'text-align:center;color:#eef2f7;font-size:15px;margin-bottom:12px;';
foot.appendChild(dateRow);
}
if (EV.timesArr.length) {
var grid = document.createElement('div');
grid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:16px;';
EV.timesArr.forEach(function (t) {
var cell = document.createElement('div');
cell.style.cssText = 'background:rgba(212,175,55,.08);border:1px solid rgba(212,175,55,.22);border-radius:10px;padding:8px 10px;';
var time = document.createElement('div');
time.style.cssText = 'font-size:15px;font-weight:800;color:#f3c34a;'; time.textContent = t[0];
var region = document.createElement('div');
region.style.cssText = 'font-size:11px;color:#9fb0c2;margin-top:1px;'; region.textContent = t[1] || ' ';
cell.appendChild(time); cell.appendChild(region); grid.appendChild(cell);
});
foot.appendChild(grid);
}
var join = document.createElement('a');
join.href = EV.meetUrl || '#'; join.target = '_blank'; join.rel = 'noopener';
join.textContent = 'Join Google Meet →';
join.style.cssText = 'display:block;text-align:center;text-decoration:none;background:linear-gradient(180deg,#e9c249,#d4af37);' +
'color:#0a1119;font-size:18px;font-weight:800;padding:15px;border-radius:12px;box-shadow:0 8px 22px rgba(212,175,55,.35);';
join.addEventListener('click', done);
foot.appendChild(join);
if (EV.meetUrl) {
var link = document.createElement('div');
link.textContent = EV.meetUrl.replace(/^https?:\/\//, '');
link.style.cssText = 'text-align:center;color:#9fb0c2;font-size:13px;margin-top:10px;font-family:ui-monospace,Menlo,Consolas,monospace;word-break:break-all;';
foot.appendChild(link);
}
var later = document.createElement('button');
later.textContent = 'Maybe later';
later.style.cssText = 'display:block;margin:12px auto 0;background:none;border:none;color:#7d8ea0;font-size:13px;cursor:pointer;text-decoration:underline;';
later.addEventListener('click', dismiss);
foot.appendChild(later);
card.appendChild(x); card.appendChild(eyebrow); card.appendChild(img); card.appendChild(foot);
back.appendChild(card); document.body.appendChild(back);
document.addEventListener('keydown', onKey);
requestAnimationFrame(function () { back.style.opacity = '1'; });
}
setTimeout(build, 1100);
}
function fetchAndGo() {
fetch('/api/announce', { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (EV) { if (EV) start(EV); })
.catch(function () {});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fetchAndGo);
else fetchAndGo();
})();
+1 -1
View File
@@ -29,5 +29,5 @@
<div class="callout warning" style="margin-top:12px;max-width:880px;margin-left:auto;margin-right:auto"><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></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="/training">Training</a><a href="/my">Member Dashboard</a><a href="/contract">Contract Security</a><a href="/disclaimer">Disclaimers</a><a href="/tools">Promo Tools</a><a href="/privacy">Privacy</a><a href="/refunds">Refunds</a></div></footer>
<script src="/track.js"></script><script src="/join.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script> <script src="/announce.js"></script>
<script src="/track.js"></script><script src="/join.js"></script><script src="/payouts.js" defer></script><script src="/chat.js" defer></script><script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script>
</body></html>
+29 -1
View File
@@ -381,6 +381,25 @@ function writeJson(file, data) {
function getSponsors() { return readJson(SPONSORS_FILE).sort((a,b)=>(a.sortOrder||0)-(b.sortOrder||0)); }
function saveSponsors(s) { writeJson(SPONSORS_FILE, s); }
function getConfig() { return readJson(CONFIG_FILE); }
// Event pop-up (announce.js) defaults — seeded into config once so the admin
// panel shows the real state and admins can toggle/edit it without a deploy.
const ANNOUNCE_DEFAULTS = {
announceEnabled: true,
announceId: 'huddle-2026-09-08',
announceImg: '/huddle-flyer.jpg',
announceMeetUrl: 'https://meet.google.com/gsw-yhqn-zrc',
announceEyebrow: 'Team RM Circle · Weekly Huddle',
announceDateLabel: 'Tuesday, September 8',
announceTimes: '7:00 PM CST | USA / Canada\n8:00 PM EST |\n8:00 PM AST | Caribbean\n1:00 AM | UK',
announceExpiresUTC: '2026-09-09T02:30:00Z'
};
function seedAnnounceDefaults() {
try { const c = readJson(CONFIG_FILE) || {}; let ch = false;
for (const k in ANNOUNCE_DEFAULTS) if (!(k in c)) { c[k] = ANNOUNCE_DEFAULTS[k]; ch = true; }
if (ch) writeJson(CONFIG_FILE, c);
} catch (e) {}
}
seedAnnounceDefaults();
function activeSponsor(sponsors) { return sponsors.find(s=>s.status==='active') || sponsors.find(s=>s.status==='waiting') || null; }
function getAnalytics() { try { return readJson(ANALYTICS_FILE); } catch (e) { return { sources: {} }; } }
function recordEvent(event, source) {
@@ -528,6 +547,15 @@ async function handleApi(req,res,pathname){
if(req.method==='GET'&&pathname==='/api/public/config'){
const c=getConfig();await getPolUsd().catch(()=>{});return json(res,200,{polUsd:polPrice.usd||0,siteName:c.siteName,programName:c.programName,bridgeHeadline:c.bridgeHeadline,bridgeSubheadline:c.bridgeSubheadline,premiumEntryPol:c.premiumEntryPol,telegramUrl:c.telegramUrl,supportLabel:c.supportLabel,showQueueProgress:c.showQueueProgress,walletNotice:c.walletNotice||''});
}
if(req.method==='GET'&&pathname==='/api/announce'){
const c=getConfig();
return json(res,200,{
enabled: c.announceEnabled===undefined ? true : !!c.announceEnabled,
id: c.announceId||'', img: c.announceImg||'', meetUrl: c.announceMeetUrl||'',
eyebrow: c.announceEyebrow||'', dateLabel: c.announceDateLabel||'',
times: c.announceTimes||'', expiresUTC: c.announceExpiresUTC||''
});
}
if(req.method==='GET'&&pathname==='/api/public/member'){
const ip=String(req.headers['x-forwarded-for']||req.socket.remoteAddress||'').split(',')[0].trim();
if(memberLookupLimited(ip))return json(res,429,{error:'Too many lookups — give it a minute.'});
@@ -1523,7 +1551,7 @@ async function handleApi(req,res,pathname){
const maxOrder=sponsors.reduce((m,s)=>Math.max(m,s.sortOrder||0),0);sponsors.push({id:String(id).trim(),name:String(name).trim(),parentId:String(parentId||'').trim(),directs:0,level,status:sponsors.some(s=>s.status==='active')?'waiting':'active',sortOrder:maxOrder+10,clicks:0,notes:String(notes||'').trim(),email:String(email||'').trim().slice(0,120)});sponsors=normalizeStatuses(sponsors);saveSponsors(sponsors);return json(res,201,{sponsors});
}
if(req.method==='PATCH'&&pathname==='/api/admin/config'){
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
const b=await bodyJson(req),cur=getConfig(),next={...cur};for(const k of ['siteName','programName','bridgeHeadline','bridgeSubheadline','premiumEntryPol','dappReferralBaseUrl','telegramUrl','supportLabel','showSponsorName','showQueueProgress','bemobPostbackUrl','telegramBotToken','companionBotToken','miniAppShortName','telegramChatId','telegramTopicId','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','ctbOfferPostbackUrl','ctbOfferSecret','recruitCtaUrl','walletNotice','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId','rotationExcludeIds','suiteAllowlist','suiteToolsInAlerts','suiteLevelOverride','directDefaultIds','announceEnabled','announceId','announceImg','announceMeetUrl','announceEyebrow','announceDateLabel','announceTimes','announceExpiresUTC'])if(Object.prototype.hasOwnProperty.call(b,k))next[k]=b[k];next.premiumEntryPol=Number(next.premiumEntryPol)||362;if('announceEnabled' in next)next.announceEnabled=!!next.announceEnabled;next.updatedAt=new Date().toISOString();writeJson(CONFIG_FILE,next);return json(res,200,{config:next});
}
const m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
if(m){const id=decodeURIComponent(m[1]),action=m[2]||null;let sponsors=getSponsors(),idx=sponsors.findIndex(s=>s.id===id);if(idx<0)return json(res,404,{error:'Sponsor not found.'});