Retire the rotation queue's voice; team links pick the nearest open spot in their own leg
The queue's AUTO-ADVANCE post announced a rotation event this morning when a member had built his own leg (the Triple Play). The queue stopped placing anyone when the public link moved to the chain walk; what was left of it was three Telegram messages, a classifier that called any join under a former queue member a "rotation join" and swept the newcomer into the queue, and a preference inside the team-link picker for queue positions over the nearest open one (15 members affected). Verified offline over the full snapshot first: all 889 personal links already target the member or a position inside their own subtree; the walk starts at the member's own children and cannot leave the leg. Now: the three messages are gone (counters still update silently); a join is a rotation join only if the company link could have produced it (the company link always offers the shallowest open position, so a deeper referrer rules it out; 'invite-<id>' submissions are leg joins outright); nothing is auto-added to the queue; and the team link picks the nearest open position in the member's own leg, in tree order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -762,14 +762,26 @@ function memberIdByAccount(address) {
|
|||||||
function nextOpenPosition(rootId, exclude) {
|
function nextOpenPosition(rootId, exclude) {
|
||||||
if (!state || !state.snapshotAt) return null;
|
if (!state || !state.snapshotAt) return null;
|
||||||
const skip = exclude || new Set();
|
const skip = exclude || new Set();
|
||||||
const q = [rootId]; const seen = new Set();
|
const q = [[rootId, 0]]; const seen = new Set();
|
||||||
while (q.length) {
|
while (q.length) {
|
||||||
const id = q.shift(); if (seen.has(id)) continue; seen.add(id);
|
const [id, depth] = q.shift(); if (seen.has(id)) continue; seen.add(id);
|
||||||
const m = state.members[id]; if (!m) continue;
|
const m = state.members[id]; if (!m) continue;
|
||||||
// excluded positions are never offered as the target, but their subtree
|
// excluded positions are never offered as the target, but their subtree
|
||||||
// is still traversed (founders can reserve a position's open slots)
|
// is still traversed (founders can reserve a position's open slots)
|
||||||
if (!skip.has(id) && (m.directCount || 0) < 2) return { id, directCount: m.directCount || 0, level: levelName(m.level || 1) };
|
if (!skip.has(id) && (m.directCount || 0) < 2) return { id, depth, directCount: m.directCount || 0, level: levelName(m.level || 1) };
|
||||||
if (m.l) q.push(m.l); if (m.r) q.push(m.r);
|
if (m.l) q.push([m.l, depth + 1]); if (m.r) q.push([m.r, depth + 1]);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// How deep a position sits under a root (0 = the root itself); null when it is not under it.
|
||||||
|
function depthFrom(rootId, id) {
|
||||||
|
if (!state || !state.members) return null;
|
||||||
|
const q = [[rootId, 0]]; const seen = new Set();
|
||||||
|
while (q.length) {
|
||||||
|
const [cur, depth] = q.shift(); if (seen.has(cur)) continue; seen.add(cur);
|
||||||
|
if (Number(cur) === Number(id)) return depth;
|
||||||
|
const m = state.members[cur]; if (!m) continue;
|
||||||
|
if (m.l) q.push([m.l, depth + 1]); if (m.r) q.push([m.r, depth + 1]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -829,4 +841,4 @@ async function getIncome(id) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, nextOpenPosition, memberIdByAccount, liveDirects, balanceOf, CONTRACT };
|
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getOrgRouting, getOrgShare, getCoachingScan, getMatrixTree, isInTeam, nextOpenPosition, depthFrom, memberIdByAccount, liveDirects, balanceOf, CONTRACT };
|
||||||
|
|||||||
@@ -362,19 +362,33 @@ async function handleSubmitId(req, res) {
|
|||||||
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
|
new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000))
|
||||||
]);
|
]);
|
||||||
} catch (e) { onchain = null; }
|
} catch (e) { onchain = null; }
|
||||||
// the chain decides the path: a rotation join is one whose on-chain referrer is
|
// Which link produced this join. In 'chain' mode the company link always offers the
|
||||||
// a rotation-queue sponsor that IS or HAS BEEN worked (status active or
|
// SHALLOWEST open position under the root, so a join whose on-chain referrer sits deeper
|
||||||
// qualified) — not just the currently-active one. With auto-advance, the direct
|
// than some other open position cannot have come from it: it came through somebody's
|
||||||
// who completes a sponsor's 2/2 (and thus joined under it) submits their ID
|
// personal link. A personal page also tags its submissions 'invite-<id>', which settles it
|
||||||
// AFTER the rotation has already moved on, so keying off "active only" wrongly
|
// outright. The old queue-membership test is kept only for the legacy 'queue' mode.
|
||||||
// labeled them a leg join. A referrer that's a still-waiting queue position, or
|
// (2026-09-19: #861's own join under Terry #840 was called a rotation join because Terry
|
||||||
// not in the queue at all, is a personal leg join.
|
// had once qualified through the queue, and #861 was swept into a queue that no longer
|
||||||
const sponsorsNow = getSponsors();
|
// places anyone.)
|
||||||
const active = activeSponsor(sponsorsNow);
|
const cfgNow = getConfig();
|
||||||
let joinPath = 'unknown';
|
let joinPath = 'unknown';
|
||||||
if (onchain && onchain.registered) {
|
if (onchain && onchain.registered) {
|
||||||
const refSp = sponsorsNow.find(s => String(s.id) === String(onchain.referrerId));
|
const R = Number(onchain.referrerId);
|
||||||
|
if ((cfgNow.publicRotationMode || 'queue') === 'chain') {
|
||||||
|
if (/^invite-/.test(source)) joinPath = 'leg';
|
||||||
|
else {
|
||||||
|
const root = Number(cfgNow.publicRotationRootId) || 2;
|
||||||
|
const exclude = new Set(String(cfgNow.rotationExcludeIds || '').split(',').map(n => Number(n.trim())).filter(n => n > 0));
|
||||||
|
exclude.add(R);
|
||||||
|
const dR = chain.depthFrom(root, R);
|
||||||
|
const other = chain.nextOpenPosition(root, exclude);
|
||||||
|
joinPath = (dR != null && (!other || other.depth >= dR)) ? 'rotation' : 'leg';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const sponsorsNow = getSponsors();
|
||||||
|
const refSp = sponsorsNow.find(s => String(s.id) === String(R));
|
||||||
joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? 'rotation' : 'leg';
|
joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? 'rotation' : 'leg';
|
||||||
|
}
|
||||||
} else if (onchain && !onchain.registered) joinPath = 'notfound';
|
} else if (onchain && !onchain.registered) joinPath = 'notfound';
|
||||||
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath,
|
subs.push({ newId, memberName, sponsorId, source: source||'(direct)', clickid, ts: new Date().toISOString(), path: joinPath,
|
||||||
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
|
onchain: onchain ? { registered: onchain.registered, tier: onchain.tierName, level: onchain.levelName, referrerId: onchain.referrerId, uplineId: onchain.uplineId } : undefined });
|
||||||
@@ -383,22 +397,8 @@ async function handleSubmitId(req, res) {
|
|||||||
firePostback(clickid, `purchase-${clickid}`, source);
|
firePostback(clickid, `purchase-${clickid}`, source);
|
||||||
let msg;
|
let msg;
|
||||||
if (joinPath === 'rotation') {
|
if (joinPath === 'rotation') {
|
||||||
// rotation joins go straight into the queue as waiting positions — no manual step
|
// company-link join: announce it; the queue is retired and nothing is added to it
|
||||||
let queueNote = '';
|
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n(Sponsor #${onchain.referrerId}'s direct count syncs from the chain automatically.)`;
|
||||||
try {
|
|
||||||
let sponsors = getSponsors();
|
|
||||||
if (sponsors.some(s => String(s.id) === String(newId))) {
|
|
||||||
queueNote = 'Already in the rotation queue.';
|
|
||||||
} else {
|
|
||||||
const maxOrder = sponsors.reduce((m, s) => Math.max(m, s.sortOrder || 0), 0);
|
|
||||||
sponsors.push({ id: String(newId), name: memberName, parentId: String(onchain.referrerId), directs: 0, level: onchain.levelName || 'Scintilla', status: sponsors.some(s => s.status === 'active') ? 'waiting' : 'active', sortOrder: maxOrder + 10, clicks: 0, notes: `auto-added: rotation join under #${onchain.referrerId} ${new Date().toISOString().slice(0, 10)}` });
|
|
||||||
sponsors = normalizeStatuses(sponsors);
|
|
||||||
saveSponsors(sponsors);
|
|
||||||
const waitingAhead = sponsors.filter(s => s.status === 'waiting' && (s.sortOrder || 0) < maxOrder + 10).length;
|
|
||||||
queueNote = `Auto-added to the rotation queue (${waitingAhead} waiting ahead of them).`;
|
|
||||||
}
|
|
||||||
} catch (e) { queueNote = `⚠ Auto-add to queue failed (${e.message}) — add manually.`; console.error('queue auto-add', e.message); }
|
|
||||||
msg = `🔔 RM Circle: ROTATION JOIN CONFIRMED ✅\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nJoined under rotation sponsor: #${onchain.referrerId}\nSource: ${source||'(direct)'}\n✅ ${queueNote}\n(Sponsor #${onchain.referrerId}'s direct count syncs from the chain automatically.)`;
|
|
||||||
} else if (joinPath === 'leg') {
|
} else if (joinPath === 'leg') {
|
||||||
msg = `🌱 RM Circle: TEAM-BUILD JOIN (not rotation)\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nActual sponsor on-chain: #${onchain.referrerId}${sponsorId!=='?'&&String(onchain.referrerId)!==sponsorId?` (form said ${sponsorId})`:''}\nSource: ${source||'(direct)'}\n→ Leg growth under #${onchain.referrerId} — no rotation action needed. Add them to the rotation queue only if they want the team effort.`;
|
msg = `🌱 RM Circle: TEAM-BUILD JOIN (not rotation)\nName: ${memberName}\nNew ID: ${newId} (${onchain.tierName}, verified on-chain)\nActual sponsor on-chain: #${onchain.referrerId}${sponsorId!=='?'&&String(onchain.referrerId)!==sponsorId?` (form said ${sponsorId})`:''}\nSource: ${source||'(direct)'}\n→ Leg growth under #${onchain.referrerId} — no rotation action needed. Add them to the rotation queue only if they want the team effort.`;
|
||||||
} else if (joinPath === 'notfound') {
|
} else if (joinPath === 'notfound') {
|
||||||
@@ -691,27 +691,16 @@ async function handleApi(req,res,pathname){
|
|||||||
try{
|
try{
|
||||||
const r=await Promise.race([chain.memberPublic(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),20000))]);
|
const r=await Promise.race([chain.memberPublic(id),new Promise((_,rej)=>setTimeout(()=>rej(new Error('Blockchain lookup timed out — try again.')),20000))]);
|
||||||
if(r.registered)r.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(id)}`;
|
if(r.registered)r.referralUrl=`${getConfig().dappReferralBaseUrl}${encodeURIComponent(id)}`;
|
||||||
// align next-in-line with the human-curated rotation: prefer the ACTIVE
|
// Where a member's team link sends the next join: the nearest position in THEIR OWN
|
||||||
// rotation sponsor when they sit in this member's leg and need directs;
|
// leg that still needs its two, in tree order ("your downline, in order", the words
|
||||||
// else the first chain-order position that's a queue participant; else
|
// the training video uses). The walk starts at the member's own children, so it can
|
||||||
// keep the chain's pure structural pick (covers legs outside the queue).
|
// never leave the leg. The old rotation queue used to be consulted first here; it no
|
||||||
|
// longer places anyone, so it no longer steers this either (2026-09-19).
|
||||||
if(r.registered&&r.subtree){
|
if(r.registered&&r.subtree){
|
||||||
try{
|
try{
|
||||||
const sponsors=getSponsors();
|
|
||||||
const act=activeSponsor(sponsors);
|
|
||||||
const participants=new Set(sponsors.filter(s=>s.status!=='qualified').map(s=>String(s.id)));
|
|
||||||
const bfs=[];const q=[r.subtree.left,r.subtree.right].filter(Boolean);
|
const bfs=[];const q=[r.subtree.left,r.subtree.right].filter(Boolean);
|
||||||
while(q.length){const n=q.shift();bfs.push(n);if(n.left)q.push(n.left);if(n.right)q.push(n.right);}
|
while(q.length){const n=q.shift();bfs.push(n);if(n.left)q.push(n.left);if(n.right)q.push(n.right);}
|
||||||
let pick=null;
|
const pick=bfs.find(n=>(n.directCount||0)<2);
|
||||||
if(act)pick=bfs.find(n=>String(n.id)===String(act.id)&&(n.directCount||0)<2);
|
|
||||||
if(!pick)pick=bfs.find(n=>participants.has(String(n.id))&&(n.directCount||0)<2);
|
|
||||||
// Last resort WITHIN the leg: the nearest position that still needs its 2,
|
|
||||||
// curated list or not. Without this, a member whose directs are too new to be
|
|
||||||
// in the rotation list had no leg pick at all and fell through to the GLOBAL
|
|
||||||
// rotation — sending their people to a stranger's position while their own
|
|
||||||
// brand-new directs sat on 0/2. That is the opposite of team-first. (Terry #840,
|
|
||||||
// qualified the same day he joined, was routing to #148; 2026-09-18.)
|
|
||||||
if(!pick)pick=bfs.find(n=>(n.directCount||0)<2);
|
|
||||||
if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName};
|
if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName};
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
}
|
}
|
||||||
@@ -2032,11 +2021,9 @@ chain.startIndexer(evt=>{
|
|||||||
sponsors=normalizeStatuses(sponsors);
|
sponsors=normalizeStatuses(sponsors);
|
||||||
saveSponsors(sponsors);
|
saveSponsors(sponsors);
|
||||||
const next=activeSponsor(sponsors);
|
const next=activeSponsor(sponsors);
|
||||||
sendTelegram(`✅ AUTO-ADVANCE: queue sponsor #${s.id}${s.name?` (${s.name})`:''} reached 2/2 and is now QUALIFIED.\nRotation moved to ${next?`#${next.id}${next.name?` (${next.name})`:''}`:'— nobody waiting (add the next position to the queue)'}.`);
|
|
||||||
}else{
|
}else{
|
||||||
sponsors[idx].directs=newDirects;
|
sponsors[idx].directs=newDirects;
|
||||||
saveSponsors(sponsors);
|
saveSponsors(sponsors);
|
||||||
sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${newDirects}/2.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2050,7 +2037,6 @@ chain.startIndexer(evt=>{
|
|||||||
if(idx>=0&&LEVELS.includes(evt.levelName)&&sponsors[idx].level!==evt.levelName){
|
if(idx>=0&&LEVELS.includes(evt.levelName)&&sponsors[idx].level!==evt.levelName){
|
||||||
sponsors[idx].level=evt.levelName;
|
sponsors[idx].level=evt.levelName;
|
||||||
saveSponsors(sponsors);
|
saveSponsors(sponsors);
|
||||||
sendTelegram(`🤖 AUTO-LEVEL: queue sponsor #${sponsors[idx].id}${sponsors[idx].name?` (${sponsors[idx].name})`:''} upgraded on-chain — queue level updated to ${evt.levelName}.`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}catch(e){console.error('auto-level error',e.message)}
|
}catch(e){console.error('auto-level error',e.message)}
|
||||||
|
|||||||
Reference in New Issue
Block a user