Leaderboard: company account excluded from rankings. Weekly member email: credits unspent, streak, contest rank, tank, line (sponsors by default; memberWeeklyEmail=1 for everyone)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 05:26:23 -05:00
parent b0ef3b3a58
commit 6233d112e0
4 changed files with 51 additions and 14 deletions
+44 -10
View File
@@ -229,20 +229,53 @@ async function nudgeTick() {
catch (e) { console.error('nudge', acct.email, e.message); }
if (nudges >= 40) break; // spread the load across ticks
}
// weekly digest: every sponsor with at least one direct
// weekly member email (Marty, 2026-09-14, modelled on the mailer.gold weekly): credits sitting unspent,
// the streak, the contest standings with the member's own rank, the tank, and the line for sponsors
let lbWeek = null; try { lbWeek = X.lb ? await X.lb().view('week') : null; } catch (e) {}
let tankN = 0; try { tankN = X.tank ? (await X.tank.waiting()).length : 0; } catch (e) {}
for (const acct of all) {
if (!acct.email) continue;
const lastD = await impl().lastDigest(acct.email);
if (now - lastD < 7 * DAY) continue;
if (now - (acct.lastSeen || acct.created || 0) > 45 * DAY) { await impl().setDigest(acct.email, now); continue; } // gone quiet for six weeks: leave them to the nudges
const view = await coachView(acct.email);
if (!view.directs.length) { await impl().setDigest(acct.email, now); continue; }
const week = view.directs.filter(d => now - d.joined < 7 * DAY).length;
const lines = view.directs.slice(0, 25).map(d => '- ' + d.name + ': ' + d.label + (d.stalled ? ' (quiet ' + d.quietDays + ' days)' : '') + '. Next: ' + d.next);
const actions = view.directs.filter(d => d.stalled).slice(0, 3).map(d => '- Message ' + d.name + ': "' + d.say.replace('{{name}}', d.name.replace(/^@/, '')) + '"');
const body = 'Your line this week:\n' + week + ' joined in the last 7 days. ' + view.stalled + ' of your ' + view.directs.length + ' directs have gone quiet.\n\n'
+ lines.join('\n') + '\n\n' + (actions.length ? 'Three things to do today:\n' + actions.join('\n') + '\n\n' : '')
+ 'Open My line to message anyone in one click: https://instantadpay.com/my#line\n\nInstantAdPay';
try { await mailer.send(acct.email, 'Your line this week: ' + view.stalled + ' to nudge', body); digests += 1; } catch (e) { console.error('digest', acct.email, e.message); }
// audience: sponsors with directs (as before) unless the admin switched the weekly on for every member
const everyone = X.siteConfig && String(X.siteConfig().memberWeeklyEmail || '0') === '1';
if (!everyone && !view.directs.length) { await impl().setDigest(acct.email, now); continue; }
const name = acct.username ? '@' + acct.username : 'there';
const parts = ['Hi ' + name + ', your week on InstantAdPay:'];
// credits
try {
const ids = [acct.memberId, ...(await accounts.positions(acct.email)).map(p => p.memberId)].filter(Boolean);
const bal = await X.ads.balances(ids, acct.email);
if (bal.available > 0) parts.push('- You have ' + bal.available.toLocaleString() + ' ad credits sitting unspent. That is ' + bal.available.toLocaleString() + ' cents of delivery doing nothing. Campaigns > New campaign, point it at your invite link: https://instantadpay.com/my#campaigns');
else if (bal.inCampaigns > 0) parts.push('- All ' + bal.inCampaigns.toLocaleString() + ' of your credits are working in live campaigns. Good.');
} catch (e) {}
// streak
try {
const st = await X.ads.viewStatus(acct.email);
if (st.streakDay > 1) parts.push('- Your claim streak is on day ' + st.streakDay + '. Today\'s claim pays ' + (st.claimCredits || 0) + ' credits; miss a day and it restarts at 5.');
else parts.push('- Five ads and a claim a day is the free way in: 5, 7, 10, then 25 credits every seventh day in a row: https://instantadpay.com/my#earn');
} catch (e) {}
// contest
if (lbWeek && lbWeek.top) {
const me = lbWeek.top.find(r => r.name === '@' + acct.username) || null;
let mine = me; if (!mine) { try { mine = (await X.lb().view('week', acct.email)).me; } catch (e) {} }
parts.push('- Referral contest this week (' + (lbWeek.prize || 'credits to the top 3') + '): ' + (lbWeek.top.length ? lbWeek.top.slice(0, 3).map(r => r.rank + '. ' + r.name + ' (' + r.sales + ' sold)').join(', ') : 'no sales yet, first sale takes the top spot') + '.'
+ (mine ? ' You are #' + mine.rank + ' with ' + mine.sales + ' sold.' : ' You are not on the board yet; one $20 package bought by someone you sponsor puts you there.') + ' https://instantadpay.com/leaderboard');
}
// tank
if (tankN > 0 && acct.memberId) parts.push('- ' + tankN + ' member' + (tankN === 1 ? ' is' : 's are') + ' waiting for a sponsor in the holding tank. Adopt one from My line: https://instantadpay.com/my#line');
// line (sponsors only)
if (view.directs.length) {
const week = view.directs.filter(d => now - d.joined < 7 * DAY).length;
parts.push('- Your line: ' + view.directs.length + ' direct' + (view.directs.length === 1 ? '' : 's') + ', ' + week + ' joined this week, ' + view.stalled + ' gone quiet.');
const actions = view.directs.filter(d => d.stalled).slice(0, 3).map(d => ' Message ' + d.name + ': "' + d.say.replace('{{name}}', d.name.replace(/^@/, '')) + '"');
if (actions.length) parts.push(actions.join('\n'));
}
parts.push('\nTwenty minutes, in order: the set, one message to your line, the tank, one conversation outward. https://instantadpay.com/blog/the-twenty-minute-day\n\nInstantAdPay · https://instantadpay.com/my\nNo income is guaranteed. Credits are advertising, not money.');
const subject = lbWeek && lbWeek.top && lbWeek.top.length && lbWeek.top[0].name === '@' + acct.username ? 'You are #1 this week on InstantAdPay' : 'Your week on InstantAdPay: credits, streak, contest';
try { await mailer.send(acct.email, subject, parts.join('\n')); digests += 1; } catch (e) { console.error('digest', acct.email, e.message); }
await impl().setDigest(acct.email, now);
if (digests >= 30) break;
}
@@ -250,7 +283,8 @@ async function nudgeTick() {
return { nudges, digests };
}
function init(opts) {
let X = {}; // extra refs for the weekly member email (ads, tank, lb getter), 2026-09-14
function init(opts) { X = arguments[0] || {};
DATA_DIR = opts.dataDir; chain = opts.chain; accounts = opts.accounts; mailer = opts.mailer;
J.load();
}