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:
@@ -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())); }
|
||||
|
||||
Reference in New Issue
Block a user