From 560d758ed9dab6671a7e5cd3df14515c1656d94c Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 15 Sep 2026 12:22:25 -0500 Subject: [PATCH] Pipeline: count linked positions' buyers on member cards, the way My line and badges do; show the main/positions split on the card Co-Authored-By: Claude Fable 5.1 --- pipeline.js | 31 ++++++++++++++++++++++++------- public/assets/my.js | 2 +- public/my.html | 2 +- server.js | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/pipeline.js b/pipeline.js index d913152..b2ca7ab 100644 --- a/pipeline.js +++ b/pipeline.js @@ -9,7 +9,21 @@ const fs = require('fs'); const path = require('path'); const db = require('./db'); -let DATA_DIR = null, accounts = null, coach = null; +let DATA_DIR = null, accounts = null, coach = null, chain = null; +// linked positions (Qualified Start) belong to the member: their buyers count toward the account, the same way +// My line chips and the badges count them (Jim's card showed 1 buyer while his badge said Circuit, 2026-09-15) +const posCache = new Map(); +async function positionBuyers(email) { + let ids = []; try { ids = (await accounts.positions(email)).map(p => p.memberId).filter(Boolean); } catch (e) {} + let n = 0; + for (const id of ids) { + const c = posCache.get(id); let v = c && Date.now() - c.t < 60000 ? c.v : null; + if (v == null) { try { v = (await chain.member(id)).buyerCount || 0; } catch (e) { v = 0; } posCache.set(id, { t: Date.now(), v }); } + n += v; + } + return { count: n, positions: ids.length }; +} +function rungForTotal(rung, total) { if (rung < 3) return rung; return total === 0 ? 3 : total === 1 ? 4 : total < 5 ? 5 : 6; } const DAY = 86400000; // columns, in order. Prospects live in the first and last; directs land by coaching rung. @@ -46,7 +60,7 @@ const D = { }; const impl = () => db.enabled() ? D : J; -function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; coach = opts.coach; } +function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; coach = opts.coach; chain = opts.chain; } // who may see the live board: mode on = everyone; preview = the admin account only function visible(mode, email, adminEmail) { @@ -79,13 +93,16 @@ async function board(email) { for (const m of (levels[0] ? levels[0].members : [])) { let c = null; try { c = await coach.describe(m, now); } catch (e) {} const n = notes[m.email] || {}; - const rung = c ? c.rung : 0; + const pb = c && c.rung >= 3 ? await positionBuyers(m.email) : { count: 0, positions: 0 }; + const buyersMain = c ? c.buyerCount || 0 : 0, buyersAll = buyersMain + pb.count; + const rung = c ? rungForTotal(c.rung, buyersAll) : 0; + const RR = coach.RUNGS[rung]; const stage = PARKED.has(n.tag) && rung < 3 ? 'later' : RUNG_STAGE[rung] || 'joined'; cards.push({ key: m.email, kind: 'member', email: m.email, name: m.username ? '@' + m.username : (m.memberId ? 'member #' + m.memberId : 'member'), - memberId: m.memberId || 0, stage, rung, rungLabel: c ? c.label : 'Joined', since: m.created || now, - lastSeen: m.lastSeen || 0, quietDays: c ? c.quietDays : 0, stalled: !!(c && c.stalled), - buyers: c ? c.buyerCount || 0 : 0, bought: !!(c && c.counted), - next: c ? c.next : 'Link a wallet', say: c ? c.say.replace('{{name}}', m.username || 'there') : '', + memberId: m.memberId || 0, stage, rung, rungLabel: RR.label, since: m.created || now, + lastSeen: m.lastSeen || 0, quietDays: c ? c.quietDays : 0, stalled: !!(c && c.stalled && rung < 6), + buyers: buyersAll, buyersMain, buyersPositions: pb.count, positions: pb.positions, bought: !!(c && c.counted), + next: RR.next, say: RR.say.replace('{{name}}', m.username || 'there'), note: n.note || '', followUp: n.followUp || null, tag: n.tag || '' }); } const due = cards.filter(c => c.followUp && c.followUp <= eod.getTime()).sort((a, b) => a.followUp - b.followUp); diff --git a/public/assets/my.js b/public/assets/my.js index fc3c084..98f088e 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -1383,7 +1383,7 @@ function fillPipeCard(c) { $('pipeCardName').textContent = c.name; const stage = (PIPE.stages.find(s => s.key === c.stage) || {}).label || ''; - $('pipeCardMeta').textContent = stage + (c.kind === 'member' ? ' · joined ' + new Date(c.since).toLocaleDateString() + (c.lastSeen ? ' · last seen ' + pipeAgo(c.lastSeen) : ' · never signed in') + (c.bought ? ' · your qualifying buyer' : '') : ' · prospect' + (c.contact ? ' · ' + c.contact : '')); + $('pipeCardMeta').textContent = stage + (c.kind === 'member' ? ' · joined ' + new Date(c.since).toLocaleDateString() + (c.lastSeen ? ' · last seen ' + pipeAgo(c.lastSeen) : ' · never signed in') + (c.bought ? ' · your qualifying buyer' : '') + (c.buyersPositions ? ' · ' + c.buyers + ' buyers: ' + c.buyersMain + ' on the main wallet, ' + c.buyersPositions + ' on linked positions' : '') : ' · prospect' + (c.contact ? ' · ' + c.contact : '')); $('pipeCardNext').innerHTML = 'Next for them: ' + esc(c.next) + (c.stalled ? ' quiet ' + c.quietDays + ' days' : ''); const say = (c.say || '').replace('{{link}}', pipeInvite()); $('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say; diff --git a/public/my.html b/public/my.html index 855c705..6fd67e6 100644 --- a/public/my.html +++ b/public/my.html @@ -1021,7 +1021,7 @@ - + diff --git a/server.js b/server.js index acc871c..7128bc4 100644 --- a/server.js +++ b/server.js @@ -370,7 +370,7 @@ async function boot() { setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends // follow-up email sequence: send whatever came due (every 10 min, first pass shortly after boot) coach.init({ dataDir: DATA_DIR, chain, accounts, mailer, ads, tank, lb: () => leaderboard, siteConfig }); - pipeline.init({ dataDir: DATA_DIR, accounts, coach }); + pipeline.init({ dataDir: DATA_DIR, accounts, coach, chain }); tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' }); legacy.init({ dataDir: DATA_DIR }); traffic.init({ dataDir: DATA_DIR });