From e3f6b00f9585495238b551664617d7d50cc99f24 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 8 Sep 2026 05:28:03 -0500 Subject: [PATCH] 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 --- public/admin.html | 2 +- public/admin.js | 4 +- public/announce.js | 227 +++++++++++++++++++++++---------------------- public/join.html | 2 +- server.js | 30 +++++- 5 files changed, 151 insertions(+), 114 deletions(-) diff --git a/public/admin.html b/public/admin.html index 268a812..fbdaeee 100644 --- a/public/admin.html +++ b/public/admin.html @@ -1,5 +1,5 @@ RM Circle Team Build Admin +

Sponsor Queue

Mark a sponsor qualified to automatically activate the next waiting position.

OrderSponsorParentDirectsLevelStatusClicksActions

Add Sponsor

Put a new position into the qualification queue.
Toggle

Coaching Radar

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.

Loading…

Award Suite Capacity

Give any position extra Suite allowance as a team bonus — issued by the team, not taken from anyone.
Open
Bonuses awarded this month
ToWhatAmountReasonWhen

Member Messages

Wallet-verified member-to-member messages, newest first. Admin can review for abuse — members are told this in the UI.

WhenFromToMessageRead by
Loading…

Traffic & Conversions

First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.

SourceBridge viewsStart viewsTraining viewsInvite viewsJoin-now viewsJoin clicksConfirmedStart → Join

Your Organization vs. the Network

How your team — rooted at your top ID — stacks up against the entire RM Circle smart contract. Live on-chain.

Reading the blockchain…

Member ID Submissions

New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.

WhenName / HandleNew IDJoined underSourceOn-chain

On-Chain Member Lookup

Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.

Matrix View

The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.

My Positions — Income

Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.

AI Chat

Paste an OpenRouter API key to switch the help chat from canned answers to AI (). Clear it to switch back.

Payment Emails (SendGrid)

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).

Public Page Settings

Their /join/<id> links place new joins directly under them instead of rotating to the next member needing directs — so they keep the entry reward. Add ?direct=0 to a link to force rotation for that one share.

Event pop-up

A dismissible overlay shown site-wide (never on join pages). Turn it off after the event, or edit it for the next huddle. Bump the Event ID to re-show it to everyone who already dismissed it.

diff --git a/public/admin.js b/public/admin.js index 76eca1e..91f2fca 100644 --- a/public/admin.js +++ b/public/admin.js @@ -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)}}); diff --git a/public/announce.js b/public/announce.js index a6bee58..665192e 100644 --- a/public/announce.js +++ b/public/announce.js @@ -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 = '×'; - 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 = '🗓 ' + EV.dateLabel + ''; - 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 = '
' + t[1] + '
' + - (t[2] ? '
' + t[2] + '
' : '
 
'); - 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 = '×'; + 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 = '🗓 '; + 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(); })(); diff --git a/public/join.html b/public/join.html index 3790da1..ecd4344 100644 --- a/public/join.html +++ b/public/join.html @@ -29,5 +29,5 @@
Risk reminder: participation involves cryptocurrency and smart-contract risk. No income is guaranteed. Use only funds you can afford to lose.
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.
- + diff --git a/server.js b/server.js index 7366568..20006b5 100644 --- a/server.js +++ b/server.js @@ -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.'});