Add public member dashboard at /my/:id

Members enter their on-chain ID (or follow a /my/46-style link) and see
their own position: tier/level/directs/earnings facts, their subtree
pyramid with downline rollups and open slots, classified payment
history, and lineage to root — all chain-derived data only, none of the
admin operational config. Public endpoint /api/public/member is cached
(120s) and rate-limited (20/min/IP). Linked from the public footers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-13 14:35:05 -05:00
parent c69ef6c1f6
commit 9f8898d2bd
6 changed files with 159 additions and 4 deletions
+50 -1
View File
@@ -400,4 +400,53 @@ function getMatrixTree() {
return { ready: true, snapshotAt: state.snapshotAt, memberCount: Object.keys(state.members).length, root, unplaced: unplaced.length ? unplaced : undefined };
}
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, getMatrixTree, isInTeam, CONTRACT };
// subtree rooted at `id` from the snapshot: nodes to `showDepth`, rollups from the FULL subtree
function getSubtree(id, showDepth = 3) {
if (!state || !state.members[id]) return null;
const seen = new Set();
function node(nid, depth) {
if (!nid || seen.has(nid)) return null;
seen.add(nid);
const m = state.members[nid];
if (!m) return null;
const l = node(m.l, depth + 1), r = node(m.r, depth + 1);
const n = {
id: nid, tier: m.tier, level: m.level, levelName: levelName(m.level || 1),
directCount: m.directCount || 0, earnedPol: m.earnedPol || 0,
downCount: 0, downPol: 0
};
for (const c of [l, r]) if (c) { n.downCount += 1 + c.downCount; n.downPol = +(n.downPol + c.earnedPol + c.downPol).toFixed(2); }
if (depth < showDepth) { n.left = l; n.right = r; }
return n;
}
return node(id, 0);
}
// everything a member may see about their own position — chain data only
async function memberPublic(id) {
const m = await fetchMember(id);
if (!m) return { registered: false, id };
await fetchCosts().catch(() => {});
const out = {
registered: true, id, account: m.account, joinedAt: m.joinedAt,
tier: m.tier, tierName: tierName(m.tier), level: m.level, levelName: levelName(m.level),
directCount: m.directCount, totalEarnedPol: m.totalEarnedPol, totalPaidPol: m.totalPaidPol,
referrerId: m.referrerId, uplineId: m.uplineId
};
try {
const inc = await fetchIncome(id);
out.income = inc.map(p => ({ fromId: p.fromId, pol: p.pol, ts: p.ts, desc: describeIncome(p.fromTier, p.level, p.pol) })).reverse().slice(0, 50);
} catch (e) { out.income = []; }
out.subtree = getSubtree(id, 3);
const chain = [];
const seen = new Set([id]);
let cur = m.uplineId;
for (let i = 0; i < 40 && cur && !seen.has(cur); i++) {
seen.add(cur); chain.push(cur);
cur = state && state.members[cur] ? state.members[cur].uplineId : 0;
}
out.uplineChain = chain;
return out;
}
module.exports = { startIndexer, getPayoutsPublic, verifyMember, memberLookup, memberPublic, getMatrixTree, isInTeam, CONTRACT };