From 4981b1a923535abe4b442d7476df9749bcecaaf7 Mon Sep 17 00:00:00 2001 From: martbost Date: Sat, 19 Sep 2026 06:50:32 -0500 Subject: [PATCH] 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-' 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 --- chain.js | 22 +++++++++++---- server.js | 80 +++++++++++++++++++++++-------------------------------- 2 files changed, 50 insertions(+), 52 deletions(-) diff --git a/chain.js b/chain.js index 38a5ccb..ac709e2 100644 --- a/chain.js +++ b/chain.js @@ -762,14 +762,26 @@ function memberIdByAccount(address) { function nextOpenPosition(rootId, exclude) { if (!state || !state.snapshotAt) return null; const skip = exclude || new Set(); - const q = [rootId]; const seen = new Set(); + const q = [[rootId, 0]]; const seen = new Set(); 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; // excluded positions are never offered as the target, but their subtree // 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 (m.l) q.push(m.l); if (m.r) q.push(m.r); + 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, 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; } @@ -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 }; diff --git a/server.js b/server.js index 44c7b40..2577174 100644 --- a/server.js +++ b/server.js @@ -362,19 +362,33 @@ async function handleSubmitId(req, res) { new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), 6000)) ]); } catch (e) { onchain = null; } - // the chain decides the path: a rotation join is one whose on-chain referrer is - // a rotation-queue sponsor that IS or HAS BEEN worked (status active or - // qualified) — not just the currently-active one. With auto-advance, the direct - // who completes a sponsor's 2/2 (and thus joined under it) submits their ID - // AFTER the rotation has already moved on, so keying off "active only" wrongly - // labeled them a leg join. A referrer that's a still-waiting queue position, or - // not in the queue at all, is a personal leg join. - const sponsorsNow = getSponsors(); - const active = activeSponsor(sponsorsNow); + // Which link produced this join. In 'chain' mode the company link always offers the + // SHALLOWEST open position under the root, so a join whose on-chain referrer sits deeper + // than some other open position cannot have come from it: it came through somebody's + // personal link. A personal page also tags its submissions 'invite-', which settles it + // outright. The old queue-membership test is kept only for the legacy 'queue' mode. + // (2026-09-19: #861's own join under Terry #840 was called a rotation join because Terry + // had once qualified through the queue, and #861 was swept into a queue that no longer + // places anyone.) + const cfgNow = getConfig(); let joinPath = 'unknown'; if (onchain && onchain.registered) { - const refSp = sponsorsNow.find(s => String(s.id) === String(onchain.referrerId)); - joinPath = (refSp && (refSp.status === 'active' || refSp.status === 'qualified')) ? 'rotation' : 'leg'; + 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'; + } } else if (onchain && !onchain.registered) joinPath = 'notfound'; 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 }); @@ -383,22 +397,8 @@ async function handleSubmitId(req, res) { firePostback(clickid, `purchase-${clickid}`, source); let msg; if (joinPath === 'rotation') { - // rotation joins go straight into the queue as waiting positions — no manual step - let queueNote = ''; - 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.)`; + // company-link join: announce it; the queue is retired and nothing is added to it + 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.)`; } 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.`; } else if (joinPath === 'notfound') { @@ -691,27 +691,16 @@ async function handleApi(req,res,pathname){ try{ 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)}`; - // align next-in-line with the human-curated rotation: prefer the ACTIVE - // rotation sponsor when they sit in this member's leg and need directs; - // else the first chain-order position that's a queue participant; else - // keep the chain's pure structural pick (covers legs outside the queue). + // Where a member's team link sends the next join: the nearest position in THEIR OWN + // leg that still needs its two, in tree order ("your downline, in order", the words + // the training video uses). The walk starts at the member's own children, so it can + // 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){ 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); 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; - 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); + const pick=bfs.find(n=>(n.directCount||0)<2); if(pick)r.nextInLine={id:pick.id,directCount:pick.directCount||0,levelName:pick.levelName}; }catch(e){} } @@ -2032,11 +2021,9 @@ chain.startIndexer(evt=>{ sponsors=normalizeStatuses(sponsors); saveSponsors(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{ sponsors[idx].directs=newDirects; 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){ sponsors[idx].level=evt.levelName; 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)}