Campaign refill notices: tell an advertiser when an ad is nearly out and when it has stopped, one email per owner with what it delivered, their balance and the bonus ladder; every send recorded and watched for the relaunch, top-up or purchase that follows

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-22 05:21:57 -05:00
parent 9df669fa7c
commit 635844a12b
2 changed files with 244 additions and 1 deletions
+19 -1
View File
@@ -39,7 +39,8 @@ const audit = require('./audit'); // counter audit: views vs delivery logs,
const leaderboard = require('./leaderboard');
const toolkit = require('./toolkit');
const badge = require('./badge'); // achievement badge image drawn on the server with ffmpeg (2026-09-16)
const friday = require('./friday'); // Five Dollar Friday: +20% credits on Friday packs, the wave, the badge (Marty, 2026-09-20)
const friday = require('./friday');
const refill = require('./refill'); // campaign running-low / ran-out notices, and what came of them // Five Dollar Friday: +20% credits on Friday packs, the wave, the badge (Marty, 2026-09-20)
const snapshot = require('./snapshot'); // daily growth snapshot -> Telegram payments feed (Marty, 2026-09-15)
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)
@@ -414,6 +415,15 @@ async function boot() {
friday.init({ dataDir: DATA_DIR, siteConfig, chain, ads, accounts, mailer, drip,
telegram: async text => { const sc = siteConfig(); if (!sc.telegramBotToken || !sc.telegramEchoChatId) return false; return telegramSend(sc.telegramEchoChatId, '\u{1F7E0} <b>InstantAdPay</b> \u00b7 ' + text, sc.telegramEchoTopicId); },
notify: async (memberId, subject, text, kind) => { const a = await accounts.byMemberId(memberId); if (!a || !a.email) return; const html = '<p>' + String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/(https:\/\/[^\s]+)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>').split('\n\n').join('</p><p>').replace(/\n/g, '<br>') + '</p>'; await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [a.email], subject, html, kind || 'notice'); } });
// Campaign refill notices: an advertiser is told when an ad is nearly out and when it has stopped,
// with the bonus ladder in the same message, and every send is watched for what they did next.
refill.init({
dataDir: DATA_DIR, siteConfig, ads, accounts, chain, mailer, drip,
creditsFor: async email => { const v = await adminMember.view(email); return (v && v.credits && v.credits.available) || 0; },
message: async (to, subject, html) => { await messages.deliver(1, ADMIN_EMAIL || 'house@instantadpay.com', [to], subject, html, 'notice'); },
});
setTimeout(() => refill.tick().catch(e => console.error('refill', e.message)), 90000);
setInterval(() => refill.tick().catch(e => console.error('refill', e.message)), 20 * 60 * 1000);
setInterval(() => friday.tick().catch(e => console.error('friday', e.message)), 5 * 60 * 1000);
setInterval(() => snapshot.tick().catch(e => console.error('snapshot', e.message)), 10 * 60 * 1000);
tank.init({ dataDir: DATA_DIR, chain, accounts, messages, mailer, site: 'https://instantadpay.com' });
@@ -2898,6 +2908,14 @@ const server = http.createServer(async (req, res) => {
if (!isAdmin(req)) return json(res, 200, { admin: false });
return json(res, 200, { admin: true, email: ADMIN_EMAIL });
}
if (p === '/api/admin/refill' && req.method === 'GET') { // who was told their ads ran out, and what they did next
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, refill.stats());
}
if (p === '/api/admin/refill/run' && req.method === 'POST') { // send the next batch now instead of waiting for the tick
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
return json(res, 200, await refill.tick());
}
if (p === '/api/admin/overview' && req.method === 'GET') {
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
const camps = await ads.adminList();