Pipeline: sponsor follow-up board (stages from the ledger, notes/follow-ups/tags, stage messages into chat), gated by pipelineMode with a coming-soon card until launch; training cards can wait on the same switch

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 11:01:03 -05:00
parent 56aa7418ad
commit 02f742f90a
9 changed files with 282 additions and 6 deletions
+1
View File
@@ -85,6 +85,7 @@ FACTS:
- PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/<username>), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=<username>&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money. - PROMO TOOLKIT BY BADGE + AI COPY ENGINE (2026-09-14): Promo tools > AI Copy Engine shows the ladder: Free (links, posts, swipes, banners, wall, objections, shorts, badge pages), Spark (payouts on: one-tap campaign templates aimed at the member's link, printable handout with their QR at /handout/<username>), Surge (first qualifying buyer; the AI Copy Engine is live: posts, DMs, follow-ups, objection replies, emails, story posts, team broadcasts in the member's name with their link, honesty rules built in; 20 free generations a month), Circuit (60 free; Video Maker renders every promo video and short with the member's own end card and QR, hosted for download; split tester compares join angles by views, joins, buyers), Nexus (150 free; Leader Ops: three-level team triage with stalled flags and one-click nudges, AI-drafted team broadcasts, credit grants from the leader's earned pool to anyone in their line, co-branded join page showing the sponsor's bio, and the member's own partner code: welcome credits funded from the leader's pool at each redemption, plus a partner kit page /partners?ref=<username>&promo=CODE). After the free allowance each generation costs 10 ad credits from the earned pool. Credits are advertising, never money.
- MEMBER UPDATE EMAILS (2026-09-14, admin only): Admin > Releases > Email an update to members: pick release notes, add an intro, choose an audience (newsletter opt-ins, opt-ins active in the last 30 days, or everyone including those who declined), preview, send a test to the admin, then send; plain text from no-reply@instantadpay.com with an opt-out link; the log shows sent/opted out/failed. - MEMBER UPDATE EMAILS (2026-09-14, admin only): Admin > Releases > Email an update to members: pick release notes, add an intro, choose an audience (newsletter opt-ins, opt-ins active in the last 30 days, or everyone including those who declined), preview, send a test to the admin, then send; plain text from no-reply@instantadpay.com with an opt-out link; the log shows sent/opted out/failed.
- LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath. - LINKED POSITIONS AND BADGES (2026-09-14): qualifying buyers on a member's linked positions count toward their achievement badges (Spark/Surge/Circuit/Nexus) and light their chip on their sponsor's line, and a badge once earned never regresses. The contract still pays levels 2 and 3 to each position based on that position's OWN qualifying buyers, so the Qualifying buyers tile shows the main position's count with the linked positions' count underneath.
- PIPELINE (coming soon, built 2026-09-15; opens when the site setting pipelineMode is on): a follow-up board on the dashboard, tab 'Pipeline' between My line and Buy packages. Columns: Talking to, Joined, Wallet linked, Payouts on, Bought, Building, Later. Prospects (from the My line prospect list) and directs are placed automatically from what they have actually done (coaching rung, on-chain buys); nobody drags cards. The sponsor adds a note, a follow-up date and a tag (hot, later, no response, not interested); a 'Follow up today' strip lists due cards; stalled cards (quiet 3+ days) are flagged; each card carries the message for its exact stage with 'Open chat with this message' (member chat) or copy. Until it opens, the tab shows a coming-soon card with the roadmap date.
- PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet. - PAYMENT + MISSED-PAYMENT NOTICES (2026-09-15): every payout that lands (who bought, level, share in POL and dollars, transaction link) and every missed payout is delivered BOTH by email and as an on-site inbox message from the company account, shown in the login pop-up and the Messages card, so it is waiting when the member signs in. MISSED-PAYOUT detail: when a level-2 or level-3 share passes a member by because that level is not open on their account, the member gets an email the same minute: who bought, the POL and dollar amount they missed, how many qualifying buyers they have versus the 2 or 5 needed, and the two ways to close the gap (bring buyers, or Qualified Start). Qualified members whose wallet rejected a transfer get a different email telling them to link a regular wallet.
- LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends. - LAUNCH WEEK SWIPES (2026-09-15): the founding-week checklist page (/launch) ends with four promoter emails members send to their OWN lists, one a day toward Monday's opening, with the member's invite link and the FOUNDER code (500 credits for anyone who joins before Mon 2026-09-21 9 AM Central) filled in, each with a Copy button. These are swipes for members, not emails the site sends.
- LOGIN ADS CHARGE ONLY ON DAYS SHOWN (2026-09-15): the login ad daily fee (100 credits) is charged only for days the ad was actually shown at least once; with many login ads sharing the sign-ins, an ad that was not picked that day pays nothing. Featured links now count views (one per viewer per hour) as well as clicks. - LOGIN ADS CHARGE ONLY ON DAYS SHOWN (2026-09-15): the login ad daily fee (100 credits) is charged only for days the ad was actually shown at least once; with many login ads sharing the sign-ins, an ad that was not picked that day pays nothing. Featured links now count views (one per viewer per hour) as well as clicks.
+9
View File
@@ -185,6 +185,15 @@ async function bootstrap() {
updated BIGINT NOT NULL, updated BIGINT NOT NULL,
INDEX (owner_email) INDEX (owner_email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS pipeline_notes (
owner_email VARCHAR(190) NOT NULL,
person VARCHAR(190) NOT NULL,
note VARCHAR(1000) NULL,
follow_up BIGINT NULL,
tag VARCHAR(20) NULL,
updated BIGINT NOT NULL,
PRIMARY KEY (owner_email, person)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS join_views ( await q(`CREATE TABLE IF NOT EXISTS join_views (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
token VARCHAR(40) NOT NULL, token VARCHAR(40) NOT NULL,
+113
View File
@@ -0,0 +1,113 @@
// Pipeline (Marty, 2026-09-15): a follow-up board for sponsors. One screen where every prospect
// and every direct sits in a column by what they have actually done. Nobody drags a card: the
// site moves it when the event fires (wallet linked, payouts on, first buy, buyers counted).
// The sponsor adds what the site cannot know: a note, a follow-up date, an outcome tag.
// Built before launch, shown as "coming soon" until site setting pipelineMode = on
// (preview = only the admin account sees the live board, for testing and the training video).
// Dual-mode store like the other modules (MySQL when DATABASE_URL is set, JSON on the volume).
const fs = require('fs');
const path = require('path');
const db = require('./db');
let DATA_DIR = null, accounts = null, coach = null;
const DAY = 86400000;
// columns, in order. Prospects live in the first and last; directs land by coaching rung.
const STAGES = [
{ key: 'talking', label: 'Talking to', hint: 'People you have reached out to who have not joined yet.' },
{ key: 'joined', label: 'Joined', hint: 'Joined free. Next: link a wallet.' },
{ key: 'wallet', label: 'Wallet linked', hint: 'Next: switch on payouts, one free transaction.' },
{ key: 'payouts', label: 'Payouts on', hint: 'Next: a first package. $20 or more counts for you.' },
{ key: 'bought', label: 'Bought', hint: 'Your qualifying buyer. Next: their first person.' },
{ key: 'building', label: 'Building', hint: 'They have buyers of their own. Coach them to their two.' },
{ key: 'later', label: 'Later', hint: 'Parked: not now, no response, not interested.' }
];
const TAGS = ['', 'hot', 'later', 'no response', 'not interested'];
const RUNG_STAGE = ['joined', 'wallet', 'payouts', 'bought', 'building', 'building', 'building'];
const PARKED = new Set(['later', 'no response', 'not interested']);
// ---- stores: one row per (owner, person) ----
const J = {
db: null,
FILE: () => path.join(DATA_DIR, 'pipeline.json'),
load() { try { this.db = JSON.parse(fs.readFileSync(this.FILE(), 'utf8')); } catch (e) { this.db = {}; } },
save() { try { fs.writeFileSync(this.FILE(), JSON.stringify(this.db)); } catch (e) {} },
async notes(owner) { if (!this.db) this.load(); return Object.values(this.db).filter(r => r.owner === owner); },
async put(owner, person, f) { if (!this.db) this.load(); const k = owner + '|' + person; this.db[k] = Object.assign(this.db[k] || { owner, person }, f, { updated: Date.now() }); this.save(); return this.db[k]; }
};
const rowR = r => ({ owner: r.owner_email, person: r.person, note: r.note || '', followUp: r.follow_up ? Number(r.follow_up) : null, tag: r.tag || '', updated: Number(r.updated) });
const D = {
async notes(owner) { return (await db.q('SELECT * FROM pipeline_notes WHERE owner_email=?', [owner])).map(rowR); },
async put(owner, person, f) {
await db.q('INSERT INTO pipeline_notes (owner_email,person,note,follow_up,tag,updated) VALUES (?,?,?,?,?,?) ON DUPLICATE KEY UPDATE note=VALUES(note), follow_up=VALUES(follow_up), tag=VALUES(tag), updated=VALUES(updated)',
[owner, person, f.note, f.followUp || null, f.tag || '', Date.now()]);
return rowR((await db.q('SELECT * FROM pipeline_notes WHERE owner_email=? AND person=?', [owner, person]))[0]);
}
};
const impl = () => db.enabled() ? D : J;
function init(opts) { DATA_DIR = opts.dataDir; accounts = opts.accounts; coach = opts.coach; }
// who may see the live board: mode on = everyone; preview = the admin account only
function visible(mode, email, adminEmail) {
if (mode === 'on') return true;
if (mode === 'preview') return !!(email && adminEmail && String(email).toLowerCase() === String(adminEmail).toLowerCase());
return false;
}
async function board(email) {
const owner = String(email || '').toLowerCase();
const now = Date.now(); const eod = new Date(); eod.setHours(23, 59, 59, 999);
const notes = {}; for (const n of await impl().notes(owner)) notes[n.person] = n;
const cards = [];
// prospects: the sponsor's own list of people they have talked to
for (const p of await coach.prospects(owner)) {
const n = notes['p:' + p.id] || {};
let stage = 'talking';
if (p.status === 'not now') stage = 'later';
if (p.status === 'joined') stage = 'joined';
if (p.status === 'bought') stage = 'bought';
if (PARKED.has(n.tag)) stage = 'later';
cards.push({ key: 'p:' + p.id, kind: 'prospect', name: p.name, contact: p.contact || '', status: p.status, stage,
since: p.updated || p.created || now, lastSeen: 0, quietDays: Math.floor((now - (p.updated || p.created || now)) / DAY),
stalled: false, next: p.status === 'contacted' || p.status === 'interested' ? 'Send your link and ask for a yes' : 'Reach out once',
say: 'Hey ' + p.name + ', here is the link I mentioned. Free to join with an email, and I will walk you through the first three steps: {{link}}',
note: n.note || p.note || '', followUp: n.followUp || p.nextTs || null, tag: n.tag || '' });
}
// directs: stage from what they have actually done (the coaching rung)
const levels = await accounts.downline(owner, 1);
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 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') : '',
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 columns = STAGES.map(s => ({ key: s.key, label: s.label, hint: s.hint,
cards: cards.filter(c => c.stage === s.key).sort((a, b) => (b.stalled - a.stalled) || (a.followUp || 9e15) - (b.followUp || 9e15) || b.since - a.since) }));
return { stages: STAGES, tags: TAGS, columns, due, counts: { total: cards.length, stalled: cards.filter(c => c.stalled).length, due: due.length } };
}
async function save(email, body) {
const owner = String(email || '').toLowerCase();
const person = String(body.key || '').trim().toLowerCase().slice(0, 190);
if (!person) return { error: 'Pick who to update.' };
// only people on the sponsor's own board
const ok = person.startsWith('p:') ? (await coach.prospects(owner)).some(p => 'p:' + p.id === person)
: await accounts.isDownlineOf(owner, person, 1);
if (!ok) return { error: 'Not on your board.' };
const tag = TAGS.includes(String(body.tag || '').toLowerCase()) ? String(body.tag || '').toLowerCase() : '';
let followUp = null;
if (body.followUp) { const t = /^\d{4}-\d{2}-\d{2}$/.test(body.followUp) ? Date.parse(body.followUp + 'T12:00:00') : Number(body.followUp); if (t && !isNaN(t)) followUp = t; }
const note = String(body.note || '').trim().slice(0, 1000);
const row = await impl().put(owner, person, { note, followUp, tag });
return { ok: true, note: row };
}
module.exports = { init, STAGES, TAGS, visible, board, save };
+1 -1
View File
@@ -796,7 +796,7 @@
})); }));
// site settings: key / value rows; booleans as checkboxes, numbers stay numbers // site settings: key / value rows; booleans as checkboxes, numbers stay numbers
const SITE_META = { noPayoutIds: 'No-payout positions (member #s, comma): linkage only, no buys from them, no joins routed under them', siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', aiCreditsPerGen: 'AI Copy Engine: credits per generation after the free allowance', aiFreeSurge: 'AI Copy Engine: free generations a month at Surge', aiFreeCircuit: 'AI Copy Engine: free generations a month at Circuit', aiFreeNexus: 'AI Copy Engine: free generations a month at Nexus', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' }; const SITE_META = { noPayoutIds: 'No-payout positions (member #s, comma): linkage only, no buys from them, no joins routed under them', siteName: 'Site name', tagline: 'Tagline', rehearsal: 'Testnet rehearsal banner', defaultSponsorId: 'Fallback sponsor (member #)', walletConnectProjectId: 'WalletConnect project id', moonpayPublicKey: 'MoonPay public key', telegramBotToken: 'Telegram proof feed: bot token', telegramChatId: 'Telegram proof feed: chat id', telegramTopicId: 'Telegram proof feed: topic id (optional)', telegramEvents: 'Telegram proof feed: events (payouts | payouts+purchases | all)', telegramCtaUrl: 'Telegram proof feed: join link under each post', aiCreditsPerGen: 'AI Copy Engine: credits per generation after the free allowance', aiFreeSurge: 'AI Copy Engine: free generations a month at Surge', aiFreeCircuit: 'AI Copy Engine: free generations a month at Circuit', aiFreeNexus: 'AI Copy Engine: free generations a month at Nexus', pipelineMode: 'Pipeline board: off (coming soon card) | preview (admin account only) | on (everyone)', pipelineEta: 'Pipeline: opening date shown on the coming-soon card (e.g. Sep 28)', memberWeeklyEmail: 'Weekly member email to everyone active (1) or only sponsors with a line (0)', leaderboardWeeklyPrize: 'Leaderboard: weekly prize text (optional; blank shows the credit ladder)', leaderboardMonthlyPrize: 'Leaderboard: monthly prize text (optional)', leaderboardWeeklyCredits: 'Leaderboard: weekly credits for 1st,2nd,3rd… (e.g. 1000,500,250; blank = none)', leaderboardMonthlyCredits: 'Leaderboard: monthly credits for 1st,2nd,3rd… (e.g. 5000,2500,1000)', leaderboardAnnounceGeneral: 'Leaderboard: announce winners in the main group too (1/0)', telegramEchoChatId: 'Telegram echo (shared payments topic): chat id', telegramEchoTopicId: 'Telegram echo: topic id', telegramEchoEvents: 'Telegram echo: events (payouts | payouts+purchases | all)', legacyCreditsAdvertiser: 'Legacy welcome credits: former advertisers', legacyCreditsEarner: 'Legacy welcome credits: former earners', pnlFixedMonthlyUsd: 'P&L: fixed monthly cost (USD)' };
function drawSite() { function drawSite() {
const wrap = $('siteForm'); const wrap = $('siteForm');
wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>' wrap.innerHTML = Object.entries(siteObj).map(([k, v]) => '<div class="kv-row"><span class="k" title="' + esc(k) + '">' + esc(SITE_META[k] || humanize(k)) + '</span>'
+76 -2
View File
@@ -632,8 +632,8 @@
} }
// ── back-office menu: hash-routed panes ─────────────── // ── back-office menu: hash-routed panes ───────────────
const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile']; const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
const TITLES = { overview: 'Overview', line: 'My line', buy: 'Buy packages', campaigns: 'Campaigns', const TITLES = { overview: 'Overview', line: 'My line', pipeline: 'Pipeline', buy: 'Buy packages', campaigns: 'Campaigns',
earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', training: 'Training', earn: 'Earn credits', earnings: 'Earnings', promo: 'Promo tools', training: 'Training',
wallet: 'Wallet & account', profile: 'Profile' }; wallet: 'Wallet & account', profile: 'Profile' };
function setPane(name) { function setPane(name) {
@@ -654,6 +654,7 @@
if (name === 'line') { loadLineage(); loadUplineMessages(); loadCoach(); loadLinkStats(); loadProspects(); } if (name === 'line') { loadLineage(); loadUplineMessages(); loadCoach(); loadLinkStats(); loadProspects(); }
if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits
if (name === 'training') loadTraining(); if (name === 'training') loadTraining();
if (name === 'pipeline') loadPipeline();
document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer document.getElementById('memberArea').classList.remove('side-open'); // close mobile drawer
if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name); if (location.hash !== '#' + name) history.replaceState(null, '', '#' + name);
} }
@@ -1336,6 +1337,79 @@
} }
// ── prospects: the member's own follow-up list ── // ── prospects: the member's own follow-up list ──
let PP_STATUSES = ['new', 'contacted', 'interested', 'joined', 'bought', 'not now']; let PP_STATUSES = ['new', 'contacted', 'interested', 'joined', 'bought', 'not now'];
// ── Pipeline: the follow-up board (Marty, 2026-09-15). Stages come from the server; cards never move by hand ──
let PIPE = null, PIPE_KEY = null;
const pipeAgo = ts => { const d = Math.floor((Date.now() - ts) / 86400000); return d <= 0 ? 'today' : d === 1 ? 'yesterday' : d < 30 ? d + 'd ago' : new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); };
const pipeInvite = () => { const t = ($('inviteLine') && $('inviteLine').textContent) || ''; return /^https?:\/\//.test(t) ? t : ''; };
function pipeCardHtml(c, showStage) {
const today = new Date(); today.setHours(23, 59, 59, 999);
const due = c.followUp && c.followUp <= today.getTime();
const chips = [];
if (c.stalled) chips.push('<span class="chip warm">stalled ' + c.quietDays + 'd</span>');
if (due) chips.push('<span class="chip warm">follow up</span>');
else if (c.followUp) chips.push('<span class="chip flat">' + new Date(c.followUp).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + '</span>');
if (c.tag) chips.push('<span class="chip flat">' + esc(c.tag) + '</span>');
if (c.kind === 'member' && c.buyers) chips.push('<span class="chip flat">' + c.buyers + ' buyer' + (c.buyers === 1 ? '' : 's') + '</span>');
const stage = showStage ? (PIPE.stages.find(s => s.key === c.stage) || {}).label || '' : '';
return '<div class="pipe-card' + (c.key === PIPE_KEY ? ' on' : '') + '" tabindex="0" data-pk="' + esc(c.key) + '">'
+ '<div class="nm"><span>' + esc(c.name) + '</span><small>' + (c.kind === 'prospect' ? 'prospect' : (c.lastSeen ? 'seen ' + pipeAgo(c.lastSeen) : 'never signed in')) + '</small></div>'
+ '<div class="sub">' + esc(stage ? stage + ' · ' + c.next : c.next) + '</div>'
+ (c.note ? '<div class="note" title="' + esc(c.note) + '">' + esc(c.note) + '</div>' : '')
+ (chips.length ? '<div>' + chips.join('') + '</div>' : '') + '</div>';
}
async function loadPipeline() {
try {
const r = await (await fetch('/api/my/pipeline')).json();
if (r.error) { IAP.status(r.error, 'bad'); return; }
if (!r.live) {
$('pipeSoon').hidden = false; $('pipeLive').hidden = true;
$('pipeSoonEta').textContent = r.eta ? 'Opens ' + r.eta + '. It is on the roadmap so you can see what is coming and when.' : 'It is on the roadmap so you can see what is coming and when.';
return;
}
PIPE = r; $('pipeSoon').hidden = true; $('pipeLive').hidden = false;
const badge = $('pipeBadge'); if (badge) { const n = r.counts.due + r.counts.stalled; badge.hidden = !n; badge.textContent = n > 9 ? '9+' : n; }
$('pipeDueCount').hidden = !r.due.length; $('pipeDueCount').textContent = r.due.length;
$('pipeDue').innerHTML = r.due.length ? '<div class="pipe-due">' + r.due.map(c => pipeCardHtml(c, true)).join('') + '</div>' : '<p class="muted small">Nothing due. Set a follow-up date on any card and it shows up here.</p>';
$('pipeSummary').textContent = r.counts.total ? r.counts.total + ' people on your board' + (r.counts.stalled ? ', ' + r.counts.stalled + ' stalled' : '') + '. Cards move on their own when something happens on the ledger; open one to add a note, a follow-up date or a tag.' : 'Nobody on your board yet. Add prospects on My line, and everyone who joins through your link appears here on their own.';
$('pipeBoard').innerHTML = r.columns.map(col => '<div class="pipe-col"><h4>' + esc(col.label) + '<span>' + col.cards.length + '</span></h4><p class="hint">' + esc(col.hint) + '</p>' + col.cards.map(c => pipeCardHtml(c, false)).join('') + '</div>').join('');
$('pipeLive').querySelectorAll('[data-pk]').forEach(el => {
el.addEventListener('click', () => openPipeCard(el.dataset.pk));
el.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPipeCard(el.dataset.pk); } });
});
if (PIPE_KEY) { const c = allPipeCards().find(x => x.key === PIPE_KEY); if (c) fillPipeCard(c); else { PIPE_KEY = null; $('pipeCard').hidden = true; } }
} catch (e) { IAP.status('Could not load your pipeline.', 'bad'); }
}
function allPipeCards() { return PIPE ? PIPE.columns.flatMap(c => c.cards) : []; }
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 : ''));
$('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());
$('pipeSayBlock').hidden = !say; $('pipeSay').textContent = say;
$('pipeSend').hidden = c.kind !== 'member' || !c.email;
$('pipeFollow').value = c.followUp ? new Date(c.followUp).toISOString().slice(0, 10) : '';
const sel = $('pipeTag'); sel.innerHTML = PIPE.tags.map(t => '<option value="' + t + '"' + (t === c.tag ? ' selected' : '') + '>' + (t || 'no tag') + '</option>').join('');
$('pipeNote').value = c.note || ''; $('pipeSaved').textContent = '';
$('pipeCard').hidden = false;
}
function openPipeCard(key) {
const c = allPipeCards().find(x => x.key === key); if (!c) return;
PIPE_KEY = key; $('pipeLive').querySelectorAll('[data-pk]').forEach(el => el.classList.toggle('on', el.dataset.pk === key));
fillPipeCard(c); $('pipeCard').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
if ($('pipeCardClose')) $('pipeCardClose').addEventListener('click', () => { PIPE_KEY = null; $('pipeCard').hidden = true; $('pipeLive').querySelectorAll('[data-pk]').forEach(el => el.classList.remove('on')); });
if ($('pipeSave')) $('pipeSave').addEventListener('click', async () => {
if (!PIPE_KEY) return;
try { await api('/api/my/pipeline/note', { key: PIPE_KEY, note: $('pipeNote').value, followUp: $('pipeFollow').value || null, tag: $('pipeTag').value }); $('pipeSaved').textContent = 'Saved.'; loadPipeline(); }
catch (e) { IAP.status(e.message, 'bad'); }
});
if ($('pipeCopy')) $('pipeCopy').addEventListener('click', async () => { try { await navigator.clipboard.writeText($('pipeSay').textContent); $('pipeCopy').textContent = 'Copied'; setTimeout(() => { $('pipeCopy').textContent = 'Copy message'; }, 1500); } catch (e) { IAP.status('Copy failed; select the text by hand.', 'bad'); } });
if ($('pipeSend')) $('pipeSend').addEventListener('click', () => {
const c = allPipeCards().find(x => x.key === PIPE_KEY); if (!c || !c.email) return;
openConvo(c.email, c.name);
const ta = $('chatInput'); if (ta) { ta.value = $('pipeSay').textContent; ta.dispatchEvent(new Event('input')); ta.focus(); }
});
async function loadProspects() { async function loadProspects() {
try { try {
const r = await (await fetch('/api/my/prospects')).json(); const r = await (await fetch('/api/my/prospects')).json();
+18
View File
@@ -730,3 +730,21 @@ img{max-width:100%}
.camp-table td.act{display:flex;flex-wrap:wrap;justify-content:flex-start;gap:8px;margin-top:8px;padding-top:10px;border-top:1px solid var(--line)} .camp-table td.act::before{display:none} .camp-table td.act{display:flex;flex-wrap:wrap;justify-content:flex-start;gap:8px;margin-top:8px;padding-top:10px;border-top:1px solid var(--line)} .camp-table td.act::before{display:none}
.camp-table td.act .btn{flex:1 1 auto;text-align:center} .camp-table td.act .btn{flex:1 1 auto;text-align:center}
} }
/* Pipeline board (Marty, 2026-09-15): columns scroll sideways inside their own box, never the page */
.pipe-board{display:flex;gap:10px;overflow-x:auto;padding:4px 2px 10px;scroll-snap-type:x proximity}
.pipe-col{flex:0 0 220px;scroll-snap-align:start;background:rgba(4,8,7,.45);border:1px solid var(--line);border-radius:12px;padding:10px;min-height:120px}
.pipe-col h4{margin:0 0 2px;font-size:13px;letter-spacing:.06em;text-transform:uppercase;color:var(--mint);display:flex;justify-content:space-between;gap:8px}
.pipe-col h4 span{color:var(--muted);font-variant-numeric:tabular-nums}
.pipe-col .hint{font-size:11.5px;color:var(--muted);margin:0 0 8px;line-height:1.35}
.pipe-card{background:rgba(255,255,255,.04);border:1px solid var(--line);border-radius:10px;padding:8px 10px;margin:0 0 8px;cursor:pointer;transition:border-color .15s}
.pipe-card:hover,.pipe-card:focus-visible{border-color:var(--mint);outline:none}
.pipe-card.on{border-color:var(--gold,#e6c15a)}
.pipe-card .nm{font-weight:700;font-size:14px;display:flex;justify-content:space-between;gap:6px;align-items:baseline}
.pipe-card .nm small{font-weight:400;font-size:11px;color:var(--muted);white-space:nowrap}
.pipe-card .sub{font-size:12px;color:var(--muted);margin-top:3px;line-height:1.35}
.pipe-card .note{font-size:12px;color:var(--ink);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pipe-card .chip{margin-top:5px;margin-right:4px}
.pipe-due{display:grid;gap:6px}
.pipe-due .pipe-card{margin:0}
@media (max-width:640px){.pipe-form{grid-template-columns:1fr !important}.pipe-col{flex-basis:200px}}
+39 -2
View File
@@ -5,7 +5,7 @@
<title>Member area | InstantAdPay</title> <title>Member area | InstantAdPay</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
<link rel="icon" type="image/png" href="/logo-icon.png"> <link rel="icon" type="image/png" href="/logo-icon.png">
<link rel="stylesheet" href="/assets/site.css?v=20260915b"> <link rel="stylesheet" href="/assets/site.css?v=20260915c">
</head> </head>
<body class="bo-body"> <body class="bo-body">
@@ -130,6 +130,7 @@
<nav class="bo-menu" aria-label="Member menu"> <nav class="bo-menu" aria-label="Member menu">
<button data-pane="overview" class="on" type="button"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="2"/><rect x="13" y="3" width="8" height="8" rx="2"/><rect x="3" y="13" width="8" height="8" rx="2"/><rect x="13" y="13" width="8" height="8" rx="2"/></svg>Overview</button> <button data-pane="overview" class="on" type="button"><svg viewBox="0 0 24 24"><rect x="3" y="3" width="8" height="8" rx="2"/><rect x="13" y="3" width="8" height="8" rx="2"/><rect x="3" y="13" width="8" height="8" rx="2"/><rect x="13" y="13" width="8" height="8" rx="2"/></svg>Overview</button>
<button data-pane="line" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg>My line</button> <button data-pane="line" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="7" r="3.2"/><circle cx="5" cy="17" r="2.6"/><circle cx="19" cy="17" r="2.6"/><path d="M12 10v3M12 13l-5 2M12 13l5 2"/></svg>My line</button>
<button data-pane="pipeline" type="button"><svg viewBox="0 0 24 24"><rect x="3" y="4" width="5" height="16" rx="1.2"/><rect x="9.5" y="4" width="5" height="11" rx="1.2"/><rect x="16" y="4" width="5" height="7" rx="1.2"/></svg>Pipeline<span class="pill" id="pipeBadge" hidden></span></button>
<button data-pane="buy" type="button"><svg viewBox="0 0 24 24"><circle cx="9" cy="20" r="1.6"/><circle cx="17" cy="20" r="1.6"/><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.4 17h8.2a2 2 0 0 0 2-1.6L21 8H6"/></svg>Buy packages</button> <button data-pane="buy" type="button"><svg viewBox="0 0 24 24"><circle cx="9" cy="20" r="1.6"/><circle cx="17" cy="20" r="1.6"/><path d="M3 4h2l2.4 11.2A2 2 0 0 0 9.4 17h8.2a2 2 0 0 0 2-1.6L21 8H6"/></svg>Buy packages</button>
<button data-pane="campaigns" type="button"><svg viewBox="0 0 24 24"><path d="M3 11l14-5v12L3 13v-2z"/><path d="M17 8a4 4 0 0 1 0 8M7 13v5a2 2 0 0 0 4 0v-3"/></svg>Campaigns</button> <button data-pane="campaigns" type="button"><svg viewBox="0 0 24 24"><path d="M3 11l14-5v12L3 13v-2z"/><path d="M17 8a4 4 0 0 1 0 8M7 13v5a2 2 0 0 0 4 0v-3"/></svg>Campaigns</button>
<button data-pane="earn" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v9M9 10c0-1.1 1.3-1.8 3-1.8s3 .7 3 1.8-1.3 1.6-3 1.8-3 .7-3 1.8 1.3 1.8 3 1.8 3-.7 3-1.8"/></svg>Earn credits<span class="pill" id="inboxBadge" hidden></span></button> <button data-pane="earn" type="button"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5v9M9 10c0-1.1 1.3-1.8 3-1.8s3 .7 3 1.8-1.3 1.6-3 1.8-3 .7-3 1.8 1.3 1.8 3 1.8 3-.7 3-1.8"/></svg>Earn credits<span class="pill" id="inboxBadge" hidden></span></button>
@@ -374,6 +375,42 @@
<div class="card ad-foot" id="adSlotPane-line" hidden></div> <div class="card ad-foot" id="adSlotPane-line" hidden></div>
</div> </div>
<div class="pane" id="pane-pipeline" hidden>
<div class="card" id="pipeSoon" hidden>
<h3>Pipeline: coming soon</h3>
<p class="muted small" id="pipeSoonText">One board for everyone you are working with. Every prospect and every direct sits in a column by what they have actually done: talking, joined, wallet linked, payouts on, bought, building. The site moves the cards when the event happens on the ledger. You add the part it cannot know: a note, a follow-up date, and the message for that exact stage, sent through the chat you already have.</p>
<p class="muted small" id="pipeSoonEta"></p>
<p><a class="btn small sec" href="/whats-new" target="_blank" rel="noopener">See the roadmap</a></p>
</div>
<div id="pipeLive" hidden>
<div class="card" id="pipeDueCard">
<h3>Follow up today <span class="pill" id="pipeDueCount" hidden></span></h3>
<p class="muted small">Cards with a follow-up date of today or earlier. Cards flagged <b>stalled</b> have been quiet for three days or more.</p>
<div id="pipeDue"><p class="muted small">Nothing due. Set a follow-up date on any card and it shows up here.</p></div>
</div>
<div class="card">
<h3>Your board</h3>
<p class="muted small" id="pipeSummary">Every column is a stage the site can prove. Cards move on their own; open one to add a note, a follow-up date or a tag.</p>
<div class="pipe-board" id="pipeBoard"></div>
</div>
<div class="card" id="pipeCard" hidden>
<div style="display:flex;justify-content:space-between;gap:10px;align-items:flex-start;flex-wrap:wrap">
<div><h3 id="pipeCardName" style="margin:0 0 4px"></h3><p class="muted small" id="pipeCardMeta" style="margin:0"></p></div>
<button class="btn small sec" type="button" id="pipeCardClose">Close</button>
</div>
<p class="small" id="pipeCardNext" style="margin:10px 0 6px"></p>
<div class="promo-block" id="pipeSayBlock" hidden><div class="pb-head"><span class="pb-net">Message for this stage</span></div><div class="pb-text" id="pipeSay"></div>
<p style="margin:8px 0 0"><button class="btn small" type="button" id="pipeSend">Open chat with this message</button> <button class="btn small sec" type="button" id="pipeCopy">Copy message</button></p></div>
<div style="display:grid;gap:10px;grid-template-columns:1fr 1fr;margin-top:12px" class="pipe-form">
<label class="small muted">Follow up on<br><input type="date" id="pipeFollow" class="small" style="width:100%"></label>
<label class="small muted">Tag<br><select id="pipeTag" class="small" style="width:100%"></select></label>
</div>
<label class="small muted" style="display:block;margin-top:10px">Your note<br><textarea id="pipeNote" rows="3" style="width:100%" maxlength="1000" placeholder="What you talked about, what they said, what is next."></textarea></label>
<p style="margin:10px 0 0"><button class="btn small" type="button" id="pipeSave">Save</button> <span class="muted small" id="pipeSaved"></span></p>
</div>
</div>
<div class="card ad-foot" id="adSlotPane-pipeline" hidden></div>
</div>
<div class="pane" id="pane-buy" hidden> <div class="pane" id="pane-buy" hidden>
<div class="card"> <div class="card">
<h3>Buy an ad package</h3> <h3>Buy an ad package</h3>
@@ -984,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=20260915b"></script> <script src="/assets/my.js?v=20260915c"></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
@@ -98,7 +98,7 @@ if (MODE === 'member' || MODE === 'all') {
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000); await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); } if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
await hide(page); flush(bag, L + 'sign-in'); await hide(page); flush(bag, L + 'sign-in');
const PANES = ['overview', 'line', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile']; const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
for (const pn of PANES) { for (const pn of PANES) {
const label = L + 'my#' + pn; const label = L + 'my#' + pn;
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page); await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
+24
View File
@@ -36,6 +36,7 @@ const updates = require('./updates');
const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14) const audit = require('./audit'); // counter audit: views vs delivery logs, charges vs shows (Marty, 2026-09-15) // member update emails from Admin > Releases (Marty, 2026-09-14)
const leaderboard = require('./leaderboard'); const leaderboard = require('./leaderboard');
const toolkit = require('./toolkit'); const toolkit = require('./toolkit');
const pipeline = require('./pipeline'); // sponsor follow-up board (coming soon until site setting pipelineMode = on) (Marty, 2026-09-15)
const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12) const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']); const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']);
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute) let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
@@ -369,6 +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 });
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 });
@@ -421,6 +423,8 @@ function siteConfig() {
telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL telegramAdminChatId: '', // private chat for admin alerts (sign-up guard bursts); falls back to ADMIN_EMAIL
launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark launchAt: '', // public launch moment, ISO 8601 with offset (e.g. 2026-09-18T19:00:00-05:00): countdown on /launch + dashboard mark
aiCreditsPerGen: 10, aiFreeSurge: 20, aiFreeCircuit: 60, aiFreeNexus: 150, // AI Copy Engine: free generations per month by badge, then credits per generation aiCreditsPerGen: 10, aiFreeSurge: 20, aiFreeCircuit: 60, aiFreeNexus: 150, // AI Copy Engine: free generations per month by badge, then credits per generation
pipelineMode: 'off', // Pipeline board: off = 'coming soon' card, preview = only the admin account sees the live board, on = everyone (Marty, 2026-09-15)
pipelineEta: 'Sep 28', // what the coming-soon card and the roadmap promise
memberWeeklyEmail: '0', // 1 = the weekly 'Your week on InstantAdPay' email goes to every active member, not only sponsors with a line memberWeeklyEmail: '0', // 1 = the weekly 'Your week on InstantAdPay' email goes to every active member, not only sponsors with a line
noPayoutIds: '25', // positions kept for linkage only (compromised wallets): no buys signed from them, no new joins routed under them or their upline chain (Marty, 2026-09-14) noPayoutIds: '25', // positions kept for linkage only (compromised wallets): no buys signed from them, no new joins routed under them or their upline chain (Marty, 2026-09-14)
leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover) leaderboardWeeklyPrize: '', leaderboardMonthlyPrize: '', leaderboardWeeklyCredits: '1000,500,250', leaderboardMonthlyCredits: '5000,2500,1000', leaderboardAnnounceGeneral: '1', // referral contest prizes (text shown on /leaderboard; credits granted to the winner automatically at rollover)
@@ -1131,7 +1135,9 @@ const server = http.createServer(async (req, res) => {
const tw = tankWaitCache.list; const tw = tankWaitCache.list;
tankWaiting = { count: tw.length, names: tw.slice(0, 6).map(w => w.name), eligible: !!(await tank.eligibility(s.email)).ok }; tankWaiting = { count: tw.length, names: tw.slice(0, 6).map(w => w.name), eligible: !!(await tank.eligibility(s.email)).ok };
} catch (e) {} } catch (e) {}
const sc0 = siteConfig();
const out = { memberId, tankWaiting, email: s.email || (acct && acct.email) || null, const out = { memberId, tankWaiting, email: s.email || (acct && acct.email) || null,
pipeline: { live: pipeline.visible(sc0.pipelineMode, s.email || (acct && acct.email), ADMIN_EMAIL), mode: sc0.pipelineMode || 'off', eta: sc0.pipelineEta || '' },
address: s.address || (acct && acct.address) || null, address: s.address || (acct && acct.address) || null,
username: (acct && acct.username) || null, username: (acct && acct.username) || null,
refCode: (acct && acct.code) || null, lineBannerUrl: (acct && acct.lineBannerUrl) || null, /* the launch checklist mark reads it (was 7 of 8 with a banner set, 2026-09-14) */ credits: 0, buyerCount: 0, refCode: (acct && acct.code) || null, lineBannerUrl: (acct && acct.lineBannerUrl) || null, /* the launch checklist mark reads it (was 7 of 8 with a banner set, 2026-09-14) */ credits: 0, buyerCount: 0,
@@ -1630,6 +1636,22 @@ const server = http.createServer(async (req, res) => {
return json(res, 200, await coach.linkStats(s.email)); return json(res, 200, await coach.linkStats(s.email));
} }
// -- prospects: the member's own follow-up list // -- prospects: the member's own follow-up list
// -- Pipeline: the follow-up board (stages from the ledger and the site; notes, follow-ups, tags from the sponsor)
if (p === '/api/my/pipeline' && req.method === 'GET') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const sc = siteConfig();
if (!pipeline.visible(sc.pipelineMode, s.email, ADMIN_EMAIL)) return json(res, 200, { live: false, eta: sc.pipelineEta || '' });
const b = await pipeline.board(s.email);
return json(res, 200, Object.assign({ live: true }, b), { 'Cache-Control': 'no-store' });
}
if (p === '/api/my/pipeline/note' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
if (!pipeline.visible(siteConfig().pipelineMode, s.email, ADMIN_EMAIL)) return json(res, 403, { error: 'The Pipeline is not open yet.' });
const r = await pipeline.save(s.email, await readBody(req));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/prospects' && req.method === 'GET') { if (p === '/api/my/prospects' && req.method === 'GET') {
const s = await auth.fromRequest(req); const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' }); if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
@@ -1880,6 +1902,8 @@ const server = http.createServer(async (req, res) => {
if (!s) return json(res, 401, { error: 'Sign in first.' }); if (!s) return json(res, 401, { error: 'Sign in first.' });
let items = []; let items = [];
try { const j = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'training.json'), 'utf8')); if (Array.isArray(j)) items = j; } catch (e) {} try { const j = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'training.json'), 'utf8')); if (Array.isArray(j)) items = j; } catch (e) {}
// a card with feature:'pipeline' waits for the switch, so the video can be ready before the tab opens (Marty, 2026-09-15)
const scT = siteConfig(); items = items.filter(it => !it.feature || (it.feature === 'pipeline' && pipeline.visible(scT.pipelineMode, s.email, ADMIN_EMAIL)));
if (!items.length) items = [{ title: 'Getting started with InstantAdPay', if (!items.length) items = [{ title: 'Getting started with InstantAdPay',
desc: 'How the platform works, how every payout splits on-chain to real wallets, and how to build your line.', desc: 'How the platform works, how every payout splits on-chain to real wallets, and how to build your line.',
docUrl: 'https://instantadpay.com/' }]; docUrl: 'https://instantadpay.com/' }];