Toolkit: ship the rest of the ladder (Spark templates + handout, Circuit Video Maker + split tester, Nexus Leader Ops, grants, co-brand, partner code)

- Spark: one-tap banner/text campaign templates aimed at the member's link (POST /api/my/toolkit/template); printable QR handout at /handout/<username>
- Circuit: videomaker.js renders every promo video/short with the member's end card + QR via ffmpeg (Dockerfile adds ffmpeg + ttf-dejavu), hosted in Spaces; split tester compares join angles
- Nexus: Leader Ops (three-level triage, stalled flags, one-click nudges), AI team broadcast kind + Send to my team, credit grants from the leader's earned pool, co-branded join page (sponsor bio for level-3 sponsors), member-funded partner code (promo_codes.funder; redemption charges the funder) + /partners?ref=<username>
- toolkit ladder all live; chatbot updated; my.js v20260914e, site.css v20260914c, join.js/partners.js v20260914a

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-14 07:15:43 -05:00
parent 0f030a4943
commit 9316dfc0db
14 changed files with 436 additions and 27 deletions
+60 -5
View File
@@ -33,7 +33,8 @@ const adminMember = require('./adminmember');
const syndicate = require('./syndicate');
const releases = require('./releases');
const leaderboard = require('./leaderboard');
const toolkit = require('./toolkit'); // 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 toolkit = require('./toolkit');
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']);
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
@@ -369,7 +370,8 @@ async function boot() {
loadOpenTokens();
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
releases.init({ dataDir: DATA_DIR });
toolkit.init({ dataDir: DATA_DIR, ads, accounts, siteConfig });
toolkit.init({ dataDir: DATA_DIR, ads, accounts, siteConfig, coach, messages, promos, videomaker, chain });
videomaker.init({ dataDir: DATA_DIR, spaces, accounts });
leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,
notify: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return; await telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId); if (String(sc.leaderboardAnnounceGeneral || '1') !== '0') await telegramSend(sc.telegramEchoChatId, text, null); } });
setTimeout(() => leaderboard.rolloverTick().catch(e => console.error('leaderboard rollover', e.message)), 90 * 1000);
@@ -839,9 +841,9 @@ const server = http.createServer(async (req, res) => {
const t = nameTok.toLowerCase();
let a = await accounts.byCode(t); if (!a) a = await accounts.byUsername(t);
if (!a && /^\d+$/.test(nameTok)) a = await accounts.byMemberId(Number(nameTok));
if (a) { name = a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : null); avatarUrl = a.avatarUrl || null; own = !!(acct && a.email === acct.email); }
if (a) { name = a.username ? '@' + a.username : (a.memberId ? 'member #' + a.memberId : null); avatarUrl = a.avatarUrl || null; own = !!(acct && a.email === acct.email); var bio = null, cobrand = false; try { cobrand = (await ads.milestonesOf(a.email)).includes('level3'); if (cobrand) bio = a.bio ? String(a.bio).slice(0, 220) : null; } catch (e) {} }
}
return json(res, 200, { ref: tok, sponsorId, invited: !!(tok || showTok), name, avatarUrl, own });
return json(res, 200, { ref: tok, sponsorId, invited: !!(tok || showTok), name, avatarUrl, own, bio: typeof bio === 'undefined' ? null : bio, cobrand: typeof cobrand === 'undefined' ? false : cobrand });
}
if (p === '/api/stats' && req.method === 'GET') {
let members = 0; try { members = await chain.memberCount(); } catch (e) {}
@@ -931,7 +933,7 @@ const server = http.createServer(async (req, res) => {
// partner promo code carried on the join link: redeem once per account (ignored if invalid/used)
try {
const pc = parseCookies(req)['iap.promo'];
if (pc) { const g = await promos.redeem(pc, e, 'link'); if (g.ok) { await ads.addEarned(e, g.credits); console.log('promo redeemed', g.code, g.credits, e); } }
if (pc) { const g = await promos.redeem(pc, e, 'link'); if (g.ok) { if (g.funder && g.funder !== e) { if (await ads.spendEarned(g.funder, g.credits)) { await ads.addEarned(e, g.credits); console.log('promo redeemed (member-funded)', g.code, g.credits, e, 'by', g.funder); } else console.log('promo funder short', g.code, g.funder); } else { await ads.addEarned(e, g.credits); console.log('promo redeemed', g.code, g.credits, e); } } }
} catch (err) { console.error('promo redeem', err.message); }
// legacy bridge: a listed former Faucet Wave / Tier One Ads member gets welcome-back credits once
if (r.created && /^(fw|t1)-(adv|earn)$/.test(via)) {
@@ -1238,6 +1240,57 @@ const server = http.createServer(async (req, res) => {
const r = await toolkit.generate(s.email, String(b.kind || ''), String(b.brief || ''), String(b.angle || 'plain'));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/toolkit/template' && req.method === 'POST') { // Spark: one-tap campaign aimed at the member's link
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req); const memberId = await auth.refreshMemberId(s);
const r = await toolkit.template(s.email, memberId, (await myMemberIds(s)).ids, String(b.kind || ''), Number(b.budget) || 0);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/toolkit/split' && req.method === 'GET') { // Circuit: per-angle views/joins/buyers for the member's link
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await toolkit.split(s.email));
}
if (p === '/api/my/toolkit/videos' && req.method === 'GET') { // Circuit: Video Maker list
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await toolkit.videos(s.email));
}
if (p === '/api/my/toolkit/video' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await toolkit.makeVideo(s.email, String(b.slug || ''));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/toolkit/team' && req.method === 'GET') { // Nexus: Leader Ops
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await toolkit.team(s.email));
}
if (p === '/api/my/toolkit/nudge' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req); const memberId = await auth.refreshMemberId(s);
const r = await toolkit.nudge(s.email, memberId, String(b.email || ''), String(b.text || ''));
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/toolkit/grant' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await toolkit.grant(s.email, String(b.email || ''), Number(b.credits) || 0);
if (r.ok) pushFeed({ type: 'Grant', from: r.fromName, to: r.toName, credits: r.credits, ts: Date.now() });
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/my/toolkit/promo' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const b = await readBody(req);
const r = await toolkit.partnerCode(s.email, String(b.code || ''), Number(b.credits) || 0);
return json(res, r.error ? 400 : 200, r);
}
if (p === '/api/admin/toolkit' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await toolkit.adminUsage());
@@ -2446,6 +2499,8 @@ const server = http.createServer(async (req, res) => {
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=300' }));
return res.end(blog.renderIndex(posts, pg, tag));
}
m = /^\/handout\/([a-z0-9_]{3,20})$/i.exec(p);
if (m) { const h = await toolkit.handout(m[1].toLowerCase()); if (!h) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/plain' })); return res.end('Not found'); } res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-cache' })); return res.end(h); }
if (p === '/leaderboard') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=60' })); return res.end(await leaderboard.renderPage()); }
if (p === '/whats-new') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=120' })); return res.end(releases.renderPage()); }
if (p === '/blog/feed.xml') { res.writeHead(200, baseHeaders({ 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=900' })); return res.end(blog.rss(await blog.listPublished())); }