diff --git a/coach.js b/coach.js index 7cf0170..9395dab 100644 --- a/coach.js +++ b/coach.js @@ -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(); } diff --git a/leaderboard.js b/leaderboard.js index 414098a..89a04fe 100644 --- a/leaderboard.js +++ b/leaderboard.js @@ -73,6 +73,8 @@ async function computeRaw(period) { if (s && s.email !== a.email) joins[s.email] = (joins[s.email] || 0) + 1; } for (const [em, n] of Object.entries(joins)) { const r = rows[em] = rows[em] || { email: em, name: names[em] || em, sales: 0, cents: 0, pol: 0n, buyers: new Set() }; r.joins = n; } + // the admin's own account (company placements, tank arrivals) is not a contestant + if (R.adminEmail && rows[R.adminEmail]) delete rows[R.adminEmail]; const out = Object.values(rows).map(r => ({ email: r.email, name: r.name, sales: r.sales, usd: r.cents / 100, pol: Number(r.pol / 10n ** 14n) / 10000, buyers: r.buyers.size, joins: r.joins || 0 })) .sort((a, b) => b.usd - a.usd || b.sales - a.sales || b.joins - a.joins); out.forEach((r, i) => { r.rank = i + 1; }); @@ -131,7 +133,7 @@ async function renderPage() { let h = '
Referral contest
Ranked by ad packages sold to other people (your own positions never count). Read from the chain, updated live. Weeks run Monday to Sunday, Central time.
Referral contest
Ranked by ad packages sold to other people (your own positions never count, and the company account is not a contestant). Read from the chain, updated live. Weeks run Monday to Sunday, Central time.