Add "Your Organization vs. the Network" live stat to admin
New chain.getOrgShare(rootId) + /api/admin/org-share endpoint compute an org's share of the whole contract by both member count and total POL paid, from in-memory state. Admin card (default root 21, saved to config.orgRootId, auto-loads) shows the two headline percentages, a bar, and the member/POL breakdown. Verified: #21 org = 51.1% of members, 49.5% of POL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -514,6 +514,28 @@ function getOwnerUpgradeNeeds(ids) {
|
|||||||
return { ready: true, needs };
|
return { ready: true, needs };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How an organization rooted at `rootId` stacks up against the whole contract —
|
||||||
|
// share of members and share of all POL paid. Computed from in-memory state.
|
||||||
|
function getOrgShare(rootId) {
|
||||||
|
if (!state || !state.snapshotAt) return { ready: false };
|
||||||
|
const totalMembers = Object.keys(state.members).length;
|
||||||
|
let totalPol = 0; for (const m of Object.values(state.members)) totalPol += m.earnedPol || 0;
|
||||||
|
const root = getSubtree(rootId, 99);
|
||||||
|
const rm = state.members[rootId];
|
||||||
|
const base = { ready: true, rootId, totalMembers, totalPol: +totalPol.toFixed(2) };
|
||||||
|
if (!root || !rm) return { ...base, found: false };
|
||||||
|
const orgBelow = root.downCount || 0;
|
||||||
|
const orgMembers = orgBelow + 1;
|
||||||
|
const orgPol = (rm.earnedPol || 0) + (root.downPol || 0);
|
||||||
|
return {
|
||||||
|
...base, found: true, orgBelow, orgMembers,
|
||||||
|
memberPct: +(100 * orgMembers / totalMembers).toFixed(1),
|
||||||
|
belowPct: +(100 * orgBelow / totalMembers).toFixed(1),
|
||||||
|
orgPol: +orgPol.toFixed(2),
|
||||||
|
polPct: totalPol ? +(100 * orgPol / totalPol).toFixed(1) : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// focused income read for one position — for the admin "my positions" income view
|
// focused income read for one position — for the admin "my positions" income view
|
||||||
async function getIncome(id) {
|
async function getIncome(id) {
|
||||||
const m = await fetchMember(id);
|
const m = await fetchMember(id);
|
||||||
@@ -528,4 +550,4 @@ async function getIncome(id) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getMatrixTree, isInTeam, CONTRACT };
|
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgShare, getMatrixTree, isInTeam, CONTRACT };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<div class="admin-grid"><div class="stack"><div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px"><div><h2 style="margin:0">Sponsor Queue</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Mark a sponsor qualified to automatically activate the next waiting position.</p></div></div><div class="table-wrap"><table class="table"><thead><tr><th>Order</th><th>Sponsor</th><th>Parent</th><th>Directs</th><th>Level</th><th>Status</th><th>Clicks</th><th>Actions</th></tr></thead><tbody id="sponsorRows"></tbody></table></div></div>
|
<div class="admin-grid"><div class="stack"><div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px"><div><h2 style="margin:0">Sponsor Queue</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Mark a sponsor qualified to automatically activate the next waiting position.</p></div></div><div class="table-wrap"><table class="table"><thead><tr><th>Order</th><th>Sponsor</th><th>Parent</th><th>Directs</th><th>Level</th><th>Status</th><th>Clicks</th><th>Actions</th></tr></thead><tbody id="sponsorRows"></tbody></table></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & Conversions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.</p></div><div id="funnelStats" class="funnel"></div><div class="table-wrap"><table class="table"><thead><tr><th>Source</th><th>Bridge views</th><th>Start views</th><th>Training views</th><th>Invite views</th><th>Join clicks</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Traffic & Conversions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.</p></div><div id="funnelStats" class="funnel"></div><div class="table-wrap"><table class="table"><thead><tr><th>Source</th><th>Bridge views</th><th>Start views</th><th>Training views</th><th>Invite views</th><th>Join clicks</th><th>Start → Join</th></tr></thead><tbody id="trafficRows"></tbody></table></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Member ID Submissions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.</p></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>Name / Handle</th><th>New ID</th><th>Joined under</th><th>Source</th><th>On-chain</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">Member ID Submissions</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.</p></div><div class="table-wrap"><table class="table"><thead><tr><th>When</th><th>Name / Handle</th><th>New ID</th><th>Joined under</th><th>Source</th><th>On-chain</th></tr></thead><tbody id="submissionRows"></tbody></table></div></div>
|
||||||
|
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Your Organization vs. the Network</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">How your team — rooted at your top ID — stacks up against the entire Crypto Team Build Network smart contract. Live on-chain.</p></div><form id="orgShareForm" style="display:flex;gap:8px"><input id="orgRoot" class="input" style="max-width:110px" placeholder="21" inputmode="numeric"><button class="btn btn-secondary btn-sm">Refresh</button></form></div><div id="orgShare"><div class="empty">Reading the blockchain…</div></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">My Positions — Income</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.</p></div><form id="incomeForm" style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"><input id="incomeIds" class="input" style="max-width:260px" placeholder="21,24,25" inputmode="numeric"><button class="btn btn-teal">Load</button></form><div id="incomeAlert"></div><div id="incomeSummary" class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px"></div><div id="incomeResult"></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">My Positions — Income</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.</p></div><form id="incomeForm" style="display:flex;gap:8px;margin-bottom:14px;flex-wrap:wrap"><input id="incomeIds" class="input" style="max-width:260px" placeholder="21,24,25" inputmode="numeric"><button class="btn btn-teal">Load</button></form><div id="incomeAlert"></div><div id="incomeSummary" class="facts" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px"></div><div id="incomeResult"></div></div>
|
||||||
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">On-Chain Member Lookup</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.</p></div><form id="lookupForm" style="display:flex;gap:10px;margin-bottom:14px"><input id="lookupId" class="input" style="max-width:220px" placeholder="Member ID e.g. 46" inputmode="numeric"><button class="btn btn-teal">Look Up</button></form><div id="lookupResult"></div></div>
|
<div class="table-card"><div style="margin-bottom:12px"><h2 style="margin:0">On-Chain Member Lookup</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.</p></div><form id="lookupForm" style="display:flex;gap:10px;margin-bottom:14px"><input id="lookupId" class="input" style="max-width:220px" placeholder="Member ID e.g. 46" inputmode="numeric"><button class="btn btn-teal">Look Up</button></form><div id="lookupResult"></div></div>
|
||||||
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Matrix View</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.</p></div><div style="display:flex;gap:8px"><button id="treeLoadBtn" class="btn btn-secondary btn-sm">Load Matrix</button><button id="treeToggleBtn" class="btn btn-secondary btn-sm hidden">List view</button></div></div><div id="matrixNav" class="hidden" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="matrixTree"></div></div></div>
|
<div class="table-card"><div style="display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:12px;flex-wrap:wrap"><div><h2 style="margin:0">Matrix View</h2><p style="color:var(--muted);margin:4px 0 0;font-size:13px">The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.</p></div><div style="display:flex;gap:8px"><button id="treeLoadBtn" class="btn btn-secondary btn-sm">Load Matrix</button><button id="treeToggleBtn" class="btn btn-secondary btn-sm hidden">List view</button></div></div><div id="matrixNav" class="hidden" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-bottom:12px"></div><div id="matrixTree"></div></div></div>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ function render(){
|
|||||||
const em=state.email||{};document.getElementById('emailStatus').innerHTML=em.configured?'<span style="color:var(--ok)">● Payment emails are ON</span> — key is set':'<span style="color:var(--muted)">○ Payment emails are OFF</span> — no SendGrid key';
|
const em=state.email||{};document.getElementById('emailStatus').innerHTML=em.configured?'<span style="color:var(--ok)">● Payment emails are ON</span> — key is set':'<span style="color:var(--muted)">○ Payment emails are OFF</span> — no SendGrid key';
|
||||||
const incEl=document.getElementById('incomeIds');if(incEl&&!incEl.value)incEl.value=state.config.ownerIds||'21,24,25';
|
const incEl=document.getElementById('incomeIds');if(incEl&&!incEl.value)incEl.value=state.config.ownerIds||'21,24,25';
|
||||||
if(!incomeAutoLoaded&&incEl&&incEl.value){incomeAutoLoaded=true;loadIncome();}
|
if(!incomeAutoLoaded&&incEl&&incEl.value){incomeAutoLoaded=true;loadIncome();}
|
||||||
|
const orgEl=document.getElementById('orgRoot');if(orgEl&&!orgEl.value)orgEl.value=state.config.orgRootId||'21';
|
||||||
|
if(!orgShareAutoLoaded&&orgEl&&orgEl.value){orgShareAutoLoaded=true;loadOrgShare();}
|
||||||
const efi=document.getElementById('emailFromInput');if(efi&&!efi.value)efi.value=state.config.emailFrom||em.from||'';
|
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'])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'])if(f.elements[k])f.elements[k].value=c[k]??'';f.elements.showSponsorName.checked=!!c.showSponsorName;f.elements.showQueueProgress.checked=!!c.showQueueProgress;
|
||||||
}
|
}
|
||||||
@@ -50,6 +52,29 @@ rows.addEventListener('change',async e=>{const sel=e.target.closest('select[data
|
|||||||
document.getElementById('sgKeyForm').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/sendgrid-key',{method:'POST',body:JSON.stringify({key})});state.email={...(state.email||{}),configured:d.configured};inp.value='';render();showToast('Payment emails enabled')}catch(x){showToast(x.message)}});
|
document.getElementById('sgKeyForm').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/sendgrid-key',{method:'POST',body:JSON.stringify({key})});state.email={...(state.email||{}),configured:d.configured};inp.value='';render();showToast('Payment emails enabled')}catch(x){showToast(x.message)}});
|
||||||
document.getElementById('sgKeyClear').addEventListener('click',async()=>{if(!confirm('Disable "you\'ve been paid" emails?'))return;try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key:''})});state.email={...(state.email||{}),configured:d.configured};render();showToast('Payment emails disabled')}catch(x){showToast(x.message)}});
|
document.getElementById('sgKeyClear').addEventListener('click',async()=>{if(!confirm('Disable "you\'ve been paid" emails?'))return;try{const d=await api('/api/admin/sendgrid-key',{method:'POST',body:JSON.stringify({key:''})});state.email={...(state.email||{}),configured:d.configured};render();showToast('Payment emails disabled')}catch(x){showToast(x.message)}});
|
||||||
document.getElementById('emailFromSave').addEventListener('click',async()=>{const v=document.getElementById('emailFromInput').value.trim();try{const d=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({emailFrom:v})});state.config=d.config;showToast('From address saved')}catch(x){showToast(x.message)}});
|
document.getElementById('emailFromSave').addEventListener('click',async()=>{const v=document.getElementById('emailFromInput').value.trim();try{const d=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({emailFrom:v})});state.config=d.config;showToast('From address saved')}catch(x){showToast(x.message)}});
|
||||||
|
let orgShareAutoLoaded=false;
|
||||||
|
async function loadOrgShare(){
|
||||||
|
const root=(document.getElementById('orgRoot').value||'').trim();
|
||||||
|
const out=document.getElementById('orgShare');
|
||||||
|
if(!root||!/^\d+$/.test(root)){out.innerHTML='<div class="empty">Enter a numeric top ID (e.g. 21).</div>';return}
|
||||||
|
out.innerHTML='<div class="empty">Reading the blockchain…</div>';
|
||||||
|
try{
|
||||||
|
try{const c=await api('/api/admin/config',{method:'PATCH',body:JSON.stringify({orgRootId:root})});state.config=c.config;}catch(_){}
|
||||||
|
const d=await api('/api/admin/org-share?root='+encodeURIComponent(root));
|
||||||
|
if(!d.ready){out.innerHTML='<div class="empty">Snapshot not ready yet — try again in a minute.</div>';return}
|
||||||
|
if(!d.found){out.innerHTML=`<div class="empty" style="color:var(--danger)">ID ${esc(root)} isn't in the indexed matrix yet.</div>`;return}
|
||||||
|
const fmt=n=>Number(n).toLocaleString(undefined,{maximumFractionDigits:0});
|
||||||
|
out.innerHTML=
|
||||||
|
`<div style="display:flex;gap:20px;flex-wrap:wrap;align-items:center;margin-bottom:14px">`+
|
||||||
|
`<div style="text-align:center"><div style="font-size:44px;font-weight:900;color:var(--gold);line-height:1">${d.memberPct}%</div><div class="micro" style="margin-top:2px">of all members</div></div>`+
|
||||||
|
`<div style="text-align:center"><div style="font-size:44px;font-weight:900;color:var(--ok);line-height:1">${d.polPct}%</div><div class="micro" style="margin-top:2px">of all POL paid</div></div>`+
|
||||||
|
`</div>`+
|
||||||
|
`<div class="progress-line" style="height:16px;margin:0 0 6px"><span style="width:${Math.min(100,d.memberPct)}%"></span></div>`+
|
||||||
|
`<p style="color:var(--muted);margin:0 0 14px;font-size:14px">Your organization rooted at <strong class="mt-id">#${d.rootId}</strong> is <strong style="color:var(--text)">${d.orgMembers} of ${d.totalMembers}</strong> members in the whole contract (${d.orgBelow} below you + your position), and has earned <strong style="color:var(--text)">${fmt(d.orgPol)} of ${fmt(d.totalPol)} POL</strong> ever paid.</p>`+
|
||||||
|
`<div class="facts" style="grid-template-columns:repeat(4,1fr)"><div class="fact"><small>Your org members</small><strong>${d.orgMembers}</strong></div><div class="fact"><small>Whole network</small><strong>${d.totalMembers}</strong></div><div class="fact"><small>Your org earned</small><strong>${fmt(d.orgPol)} POL</strong></div><div class="fact"><small>Network paid</small><strong>${fmt(d.totalPol)} POL</strong></div></div>`;
|
||||||
|
}catch(x){out.innerHTML=`<div class="empty" style="color:var(--danger)">${esc(x.message)}</div>`}
|
||||||
|
}
|
||||||
|
document.getElementById('orgShareForm').addEventListener('submit',e=>{e.preventDefault();loadOrgShare()});
|
||||||
let incomeAutoLoaded=false;
|
let incomeAutoLoaded=false;
|
||||||
async function loadIncome(){
|
async function loadIncome(){
|
||||||
const ids=(document.getElementById('incomeIds').value||'').trim();
|
const ids=(document.getElementById('incomeIds').value||'').trim();
|
||||||
|
|||||||
@@ -419,6 +419,12 @@ async function handleApi(req,res,pathname){
|
|||||||
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
|
if(req.method==='GET'&&pathname==='/api/admin/matrix-tree'){
|
||||||
return json(res,200,chain.getMatrixTree());
|
return json(res,200,chain.getMatrixTree());
|
||||||
}
|
}
|
||||||
|
if(req.method==='GET'&&pathname==='/api/admin/org-share'){
|
||||||
|
const raw=new URL(req.url,'http://x').searchParams.get('root');
|
||||||
|
const root=Number(raw||parseOwnerIds()[0]||21);
|
||||||
|
if(!Number.isInteger(root)||root<1||root>281474976710655)return json(res,400,{error:'Enter a numeric root ID.'});
|
||||||
|
return json(res,200,chain.getOrgShare(root));
|
||||||
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/admin/income'){
|
if(req.method==='GET'&&pathname==='/api/admin/income'){
|
||||||
const raw=new URL(req.url,'http://x').searchParams.get('ids')||'';
|
const raw=new URL(req.url,'http://x').searchParams.get('ids')||'';
|
||||||
const ids=[...new Set(raw.split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0&&n<=281474976710655))].slice(0,12);
|
const ids=[...new Set(raw.split(',').map(s=>parseInt(String(s).trim(),10)).filter(n=>Number.isInteger(n)&&n>0&&n<=281474976710655))].slice(0,12);
|
||||||
@@ -470,7 +476,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});
|
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'){
|
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','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail'])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','telegramChatId','telegramTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId'])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 m=pathname.match(/^\/api\/admin\/sponsors\/([^/]+)(?:\/(increment|activate|qualify|reset|move))?$/);
|
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.'});
|
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.'});
|
||||||
|
|||||||
Reference in New Issue
Block a user