diff --git a/chain.js b/chain.js index d17cd24..6ba9699 100644 --- a/chain.js +++ b/chain.js @@ -477,6 +477,43 @@ async function memberPublic(id) { return out; } +// For owned positions: detect when a leg member is ONE upgrade away from paying +// the position, but the position isn't eligible yet (not qualified, or below the +// required level) — i.e. "upgrade now or the payment passes you". Computed from +// in-memory state, no RPC. A member M at depth D pays this position on M's +// upgrade OUT of level D, so the trigger is M.level === D with the owner ineligible. +function getOwnerUpgradeNeeds(ids) { + if (!state || !state.snapshotAt || !costs) return { ready: false, needs: [] }; + const needs = []; + for (const id of ids) { + const p = state.members[id]; + const root = getSubtree(id, 99); + if (!p || !root) continue; + const pLevel = p.level || 1, pQual = (p.directCount || 0) >= 2; + const items = []; + (function walk(n, depth) { + if (!n) return; + if (depth >= 1 && (n.level || 1) === depth) { + const eligible = pQual && pLevel >= depth; + if (!eligible) items.push({ memberId: n.id, depth, amount: (costs.up[n.tier === 2 ? 2 : 1] || [])[depth - 1] || 0 }); + } + walk(n.left, depth + 1); walk(n.right, depth + 1); + })(root, 0); + if (items.length) { + const minDepth = Math.min(...items.map(i => i.depth)); + const atMin = items.filter(i => i.depth === minDepth); + needs.push({ + id, level: pLevel, levelName: levelName(pLevel), qualified: pQual, + neededLevel: minDepth, neededLevelName: levelName(minDepth), + members: atMin.map(i => i.memberId), + amountAtRisk: +atMin.reduce((s, i) => s + i.amount, 0).toFixed(2), + reason: pQual ? 'upgrade' : 'qualify' + }); + } + } + return { ready: true, needs }; +} + // focused income read for one position — for the admin "my positions" income view async function getIncome(id) { const m = await fetchMember(id); @@ -491,4 +528,4 @@ async function getIncome(id) { }; } -module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getMatrixTree, isInTeam, CONTRACT }; +module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getIncome, getOwnerUpgradeNeeds, getMatrixTree, isInTeam, CONTRACT }; diff --git a/public/admin.html b/public/admin.html index 855d5b8..97f9193 100644 --- a/public/admin.html +++ b/public/admin.html @@ -4,11 +4,11 @@
Mark a sponsor qualified to automatically activate the next waiting position.
| Order | Sponsor | Parent | Directs | Level | Status | Clicks | Actions |
|---|
First-touch source per visitor session (referring domain or utm_source). Funnel: bridge page → start page → join click.
| Source | Bridge views | Start views | Training views | Invite views | Join clicks | Start → Join |
|---|
New members who confirmed their purchase on the start page. Each one was posted to your Hermes Telegram chat — add them to the rotation.
| When | Name / Handle | New ID | Joined under | Source | On-chain |
|---|
Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.
Every payment received by your own positions, live from the contract. Comma-separated IDs — saved for next time.
Enter an RM Circle ID to read its registration, lineage, and every payment it has received — live from the smart contract.
The entire on-chain matrix — who landed where, with tier, level, directs, and earnings per position. Click a position to drill down.
Paste an OpenRouter API key to switch the help chat from canned answers to AI (). Clear it to switch back.
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).