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 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 12:22:25 -05:00
parent 02f742f90a
commit 560d758ed9
4 changed files with 27 additions and 10 deletions
+24 -7
View File
@@ -9,7 +9,21 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const db = require('./db'); 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; const DAY = 86400000;
// columns, in order. Prospects live in the first and last; directs land by coaching rung. // 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; 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 // who may see the live board: mode on = everyone; preview = the admin account only
function visible(mode, email, adminEmail) { function visible(mode, email, adminEmail) {
@@ -79,13 +93,16 @@ async function board(email) {
for (const m of (levels[0] ? levels[0].members : [])) { for (const m of (levels[0] ? levels[0].members : [])) {
let c = null; try { c = await coach.describe(m, now); } catch (e) {} let c = null; try { c = await coach.describe(m, now); } catch (e) {}
const n = notes[m.email] || {}; 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'; 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'), 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, 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), lastSeen: m.lastSeen || 0, quietDays: c ? c.quietDays : 0, stalled: !!(c && c.stalled && rung < 6),
buyers: c ? c.buyerCount || 0 : 0, bought: !!(c && c.counted), buyers: buyersAll, buyersMain, buyersPositions: pb.count, positions: pb.positions, bought: !!(c && c.counted),
next: c ? c.next : 'Link a wallet', say: c ? c.say.replace('{{name}}', m.username || 'there') : '', next: RR.next, say: RR.say.replace('{{name}}', m.username || 'there'),
note: n.note || '', followUp: n.followUp || null, tag: n.tag || '' }); 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); const due = cards.filter(c => c.followUp && c.followUp <= eod.getTime()).sort((a, b) => a.followUp - b.followUp);
+1 -1
View File
@@ -1383,7 +1383,7 @@
function fillPipeCard(c) { function fillPipeCard(c) {
$('pipeCardName').textContent = c.name; $('pipeCardName').textContent = c.name;
const stage = (PIPE.stages.find(s => s.key === c.stage) || {}).label || ''; 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 = '<b>Next for them:</b> ' + esc(c.next) + (c.stalled ? ' <span class="chip warm">quiet ' + c.quietDays + ' days</span>' : ''); $('pipeCardNext').innerHTML = '<b>Next for them:</b> ' + esc(c.next) + (c.stalled ? ' <span class="chip warm">quiet ' + c.quietDays + ' days</span>' : '');
const say = (c.say || '').replace('{{link}}', pipeInvite()); const say = (c.say || '').replace('{{link}}', pipeInvite());
$('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say; $('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say;
+1 -1
View File
@@ -1021,7 +1021,7 @@
<script src="/assets/common.js?v=20260914a"></script> <script src="/assets/common.js?v=20260914a"></script>
<script src="/assets/wallet.js?v=20260911a"></script> <script src="/assets/wallet.js?v=20260911a"></script>
<script src="/assets/promo.js?v=20260911a"></script> <script src="/assets/promo.js?v=20260911a"></script>
<script src="/assets/my.js?v=20260915c"></script> <script src="/assets/my.js?v=20260915d"></script>
<script src="/assets/chat.js?v=20260907l"></script> <script src="/assets/chat.js?v=20260907l"></script>
</body> </body>
</html> </html>
+1 -1
View File
@@ -370,7 +370,7 @@ async function boot() {
setInterval(() => ads.scheduleSweep().catch(e => console.error('schedule sweep', e.message)), 5 * 60 * 1000); // scheduled starts/ends 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) // 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 }); 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' }); tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
legacy.init({ dataDir: DATA_DIR }); legacy.init({ dataDir: DATA_DIR });
traffic.init({ dataDir: DATA_DIR }); traffic.init({ dataDir: DATA_DIR });