Dormant company-rotation mode (publicRotationMode=chain, root #2) + multi-root teamRootId alert gating - founders' deal 2026-08-19, flip deferred until team link migration + Marty's right-leg position secured

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-19 12:39:01 -05:00
parent c64a716fcd
commit e31b01e338
2 changed files with 32 additions and 5 deletions
+16 -1
View File
@@ -667,6 +667,21 @@ function getOrgShare(rootId) {
}; };
} }
// Company-rotation pick: breadth-first (matrix order, left→right) first
// position under `rootId` that still needs directs — the "next open team
// position" for the public rotation when publicRotationMode==='chain'.
function nextOpenPosition(rootId) {
if (!state || !state.snapshotAt) return null;
const q = [rootId]; const seen = new Set();
while (q.length) {
const id = q.shift(); if (seen.has(id)) continue; seen.add(id);
const m = state.members[id]; if (!m) continue;
if ((m.directCount || 0) < 2) return { id, directCount: m.directCount || 0, level: levelName(m.level || 1) };
if (m.l) q.push(m.l); if (m.r) q.push(m.r);
}
return null;
}
// Coaching radar: triage every member below `rootId` into actionable tiers. // Coaching radar: triage every member below `rootId` into actionable tiers.
// - atRisk: money forming in their leg that they can't catch yet (corrected // - atRisk: money forming in their leg that they can't catch yet (corrected
// rule: catcher must be qualified AND at the level being bought) // rule: catcher must be qualified AND at the level being bought)
@@ -720,4 +735,4 @@ async function getIncome(id) {
}; };
} }
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, balanceOf, CONTRACT }; module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, nextOpenPosition, balanceOf, CONTRACT };
+16 -4
View File
@@ -487,7 +487,18 @@ async function handleApi(req,res,pathname){
return json(res,200,{url:'https://www.moonpay.com/buy/pol',signed:false,pol}); return json(res,200,{url:'https://www.moonpay.com/buy/pol',signed:false,pol});
} }
if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){ if(req.method==='GET'&&pathname==='/api/public/current-sponsor'){
const sponsors=getSponsors(),c=getConfig(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'}); const c=getConfig();
// DORMANT until config.publicRotationMode='chain': company-wide rotation —
// the next open position under publicRotationRootId (default #2), read
// straight from the chain. Negotiated with the RM Circle founders
// 2026-08-19; do NOT enable until Marty says flip (his team is migrating
// links first, and his new right-leg position must be secured first).
if((c.publicRotationMode||'queue')==='chain'){
const root=Number(c.publicRotationRootId)||2;
const pick=chain.nextOpenPosition(root);
if(pick)return json(res,200,{sponsor:{id:String(pick.id),name:null,directs:pick.directCount,goal:2,level:pick.level,referralUrl:`${c.dappReferralBaseUrl}${encodeURIComponent(pick.id)}`},waitingCount:0,mode:'chain',message:'Company rotation — you join under the next open team position. It advances automatically as positions qualify.'});
}
const sponsors=getSponsors(),a=activeSponsor(sponsors);if(!a)return json(res,404,{error:'No active sponsor is currently assigned.'});
return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'}); return json(res,200,{sponsor:publicSponsorPayload(a,c),waitingCount:sponsors.filter(s=>s.status==='waiting').length,message:'Always use the current sponsor shown on this page. Team placement rotates as members qualify.'});
} }
if(req.method==='POST'&&pathname==='/api/public/join-click'){ if(req.method==='POST'&&pathname==='/api/public/join-click'){
@@ -591,7 +602,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','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey'])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','telegramRecruitTopicId','teamRootId','emailFrom','teamAlertEmail','ownerIds','ownerAlertEmail','orgRootId','tweetEnabled','tweetCtaUrl','tweetHashtags','blotatoTwitterId','dappFallbackPublic','moonpayPublicKey','moonpaySecretKey','publicRotationMode','publicRotationRootId'])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.'});
@@ -712,9 +723,10 @@ setTimeout(checkUpgradeAlerts, 30000).unref();
chain.startIndexer(evt=>{ chain.startIndexer(evt=>{
try{ try{
const c=getConfig(); const c=getConfig();
const rootId=Number(c.teamRootId)||0; // teamRootId accepts a comma list ("21,136") — alerts fire for ANY listed org
const roots=String(c.teamRootId||'').split(',').map(n=>Number(n.trim())).filter(n=>n>0);
const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id]; const ids=evt.type==='payout'?[evt.toId,evt.fromId]:[evt.id];
if(rootId&&ids.some(i=>chain.isInTeam(i,rootId))){ if(roots.length&&ids.some(i=>roots.some(r=>chain.isInTeam(i,r)))){
const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''}; const contact=id=>{const s=getSponsors().find(x=>String(x.id)===String(id));return s&&s.email?`\nContact: ${s.name?s.name+' — ':''}${s.email}`:''};
let text; let text;
if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`; if(evt.type==='registered')text=`📈 TEAM BUILD: new position!\n#${evt.id} registered under #${evt.referrerId} (${evt.tierName}).`;