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:
@@ -0,0 +1,225 @@
|
|||||||
|
// Campaign refill notices (Marty, 2026-09-22). Two moments in an advertiser's life were silent:
|
||||||
|
// the ad running low, and the ad stopping dead. 70 member campaigns had already delivered their whole
|
||||||
|
// budget and stopped with nobody told, and half of those owners were still holding credits they could
|
||||||
|
// have spent the same minute. This watches for both moments, writes one email and one on-site notice
|
||||||
|
// per owner (never one per campaign), leads with what the ad actually delivered, and puts the bonus
|
||||||
|
// ladder in front of them at the only moment it is persuasive.
|
||||||
|
//
|
||||||
|
// Everything sent is recorded, and every send is then watched for what the owner did next: relaunched
|
||||||
|
// a campaign, topped one up, or bought credits. That is the conversion history, in data/refill.json.
|
||||||
|
//
|
||||||
|
// Settings (siteConfig): refillNotices '1'|'0' (default on), refillLowPct (80) the share of budget
|
||||||
|
// delivered that counts as "running low".
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
let X = {}, FILE = null, S = null;
|
||||||
|
const DAY = 86400000;
|
||||||
|
const LOOKBACK = 45 * DAY; // a send older than this stops being watched for a conversion
|
||||||
|
const MAX_PER_TICK = 25; // owners emailed in one pass, so a backfill goes out in batches
|
||||||
|
|
||||||
|
function init(opts) { X = opts; FILE = path.join(opts.dataDir, 'refill.json'); S = load(); }
|
||||||
|
function load() { try { return JSON.parse(fs.readFileSync(FILE, 'utf8')); } catch (e) { return { sent: {}, seq: 0 }; } }
|
||||||
|
function save() { try { fs.writeFileSync(FILE, JSON.stringify(S)); } catch (e) {} }
|
||||||
|
|
||||||
|
function cfg() {
|
||||||
|
const c = (X.siteConfig && X.siteConfig()) || {};
|
||||||
|
return {
|
||||||
|
on: String(c.refillNotices == null ? '1' : c.refillNotices) === '1',
|
||||||
|
lowPct: Math.min(99, Math.max(50, Number(c.refillLowPct) || 80)),
|
||||||
|
fridayOn: String(c.fridayPromo == null ? '1' : c.fridayPromo) === '1',
|
||||||
|
fridayPct: Number(c.fridayBonusPct) || 20,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SITE = 'https://instantadpay.com';
|
||||||
|
const n0 = v => (typeof v === 'number' && isFinite(v) ? v : 0);
|
||||||
|
const money = n => Number(n || 0).toLocaleString('en-US');
|
||||||
|
const delivered = c => n0(c.spent) + n0(c.accrued);
|
||||||
|
const key = (id, state) => id + ':' + state;
|
||||||
|
|
||||||
|
// the five packages and what each dollar buys, so the ladder is never hand-typed
|
||||||
|
const PACKS = [
|
||||||
|
{ price: 5, credits: 500, bonus: 0 },
|
||||||
|
{ price: 20, credits: 2000, bonus: 0 },
|
||||||
|
{ price: 50, credits: 5500, bonus: 10 },
|
||||||
|
{ price: 100, credits: 12000, bonus: 20 },
|
||||||
|
{ price: 250, credits: 32500, bonus: 30 },
|
||||||
|
];
|
||||||
|
|
||||||
|
function ladderText() {
|
||||||
|
const c = cfg();
|
||||||
|
const rows = PACKS.map(p => ' $' + p.price + ' -> ' + money(p.credits) + ' credits' + (p.bonus ? ' (' + p.bonus + '% more per dollar)' : ''));
|
||||||
|
let s = 'Ad packages, and what each one adds to your balance:\n\n' + rows.join('\n')
|
||||||
|
+ '\n\nA credit is one cent of delivery, so the bigger packages are simply more delivery for the same dollar.';
|
||||||
|
if (c.fridayOn) s += '\n\nFive Dollar Friday: any package from $5 up, bought on a Friday, earns an extra ' + c.fridayPct + '% in credits on top of the above.';
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
function ladderHtml() {
|
||||||
|
const c = cfg();
|
||||||
|
const rows = PACKS.map(p => '<tr><td style="padding:6px 14px 6px 0"><b>$' + p.price + '</b></td><td style="padding:6px 14px 6px 0">' + money(p.credits)
|
||||||
|
+ ' credits</td><td style="padding:6px 0;color:#0E8F72">' + (p.bonus ? p.bonus + '% more per dollar' : '') + '</td></tr>').join('');
|
||||||
|
return '<p><b>Ad packages, and what each one adds to your balance:</b></p><table style="border-collapse:collapse">' + rows + '</table>'
|
||||||
|
+ '<p style="color:#556">A credit is one cent of delivery, so the bigger packages are simply more delivery for the same dollar.</p>'
|
||||||
|
+ (c.fridayOn ? '<p><b>Five Dollar Friday:</b> any package from $5 up, bought on a Friday, earns an extra ' + c.fridayPct + '% in credits on top of the above.</p>' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// what one owner's batch of campaigns says in the body
|
||||||
|
function describe(list) {
|
||||||
|
return list.map(c => {
|
||||||
|
const bits = [money(Math.round(delivered(c))) + ' of ' + money(Math.round(n0(c.budget))) + ' credits delivered'];
|
||||||
|
if (n0(c.imps)) bits.push(money(n0(c.imps)) + ' impressions');
|
||||||
|
if (n0(c.clicks)) bits.push(money(n0(c.clicks)) + ' clicks');
|
||||||
|
return ' "' + (c.name || c.title || ('campaign #' + c.id)) + '" (' + (c.type || 'ad') + '): ' + bits.join(', ');
|
||||||
|
}).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function compose(owner, list, state, credits) {
|
||||||
|
const many = list.length > 1;
|
||||||
|
const totalImps = list.reduce((n, c) => n + n0(c.imps), 0);
|
||||||
|
const totalClicks = list.reduce((n, c) => n + n0(c.clicks), 0);
|
||||||
|
const canRelaunch = credits >= 100;
|
||||||
|
const subject = state === 'out'
|
||||||
|
? (many ? 'Your InstantAdPay ads have finished delivering' : 'Your ad "' + (list[0].name || 'campaign') + '" has finished delivering')
|
||||||
|
: (many ? 'Your InstantAdPay ads are nearly out of credits' : 'Your ad "' + (list[0].name || 'campaign') + '" is nearly out of credits');
|
||||||
|
const opener = state === 'out'
|
||||||
|
? (many ? 'Your ' + list.length + ' campaigns have spent their full budget and stopped. They delivered everything you paid for.'
|
||||||
|
: 'That campaign spent its full budget and stopped. It delivered everything you paid for.')
|
||||||
|
: (many ? 'Your ' + list.length + ' campaigns are close to spending their full budget. When they do, they stop.'
|
||||||
|
: 'That campaign is close to spending its full budget. When it does, it stops.');
|
||||||
|
const totals = (totalImps || totalClicks)
|
||||||
|
? 'Between them so far: ' + money(totalImps) + ' impressions and ' + money(totalClicks) + ' clicks.'
|
||||||
|
: '';
|
||||||
|
const next = canRelaunch
|
||||||
|
? 'You still have ' + money(Math.round(credits)) + ' credits in your balance, which is enough to put an ad back up right now without buying anything. Open Campaigns and start one: ' + SITE + '/my#campaigns'
|
||||||
|
: 'Your balance is down to ' + money(Math.round(credits)) + ' credits, so the next campaign needs a top-up first: ' + SITE + '/my#buy';
|
||||||
|
const lines = [opener, totals, next, ladderText(),
|
||||||
|
'Every package pays your sponsor line in the same on-chain transaction, and every payout is on the public ledger at ' + SITE + '/ledger.'];
|
||||||
|
const text = lines.filter(Boolean).join('\n\n') + '\n\nWhat each one did:\n' + describe(list) + '\n\nInstantAdPay';
|
||||||
|
const html = '<p>' + opener + '</p>' + (totals ? '<p>' + totals + '</p>' : '')
|
||||||
|
+ '<p>' + next.replace(/(https:\/\/[^\s]+)/g, '<a href="$1">$1</a>') + '</p>'
|
||||||
|
+ '<p><b>What each one did:</b></p><ul>' + list.map(c => '<li>' + esc(c.name || ('campaign #' + c.id)) + ' (' + esc(c.type || 'ad') + '): '
|
||||||
|
+ money(Math.round(delivered(c))) + ' of ' + money(Math.round(n0(c.budget))) + ' credits delivered'
|
||||||
|
+ (n0(c.imps) ? ', ' + money(n0(c.imps)) + ' impressions' : '') + (n0(c.clicks) ? ', ' + money(n0(c.clicks)) + ' clicks' : '') + '</li>').join('') + '</ul>'
|
||||||
|
+ ladderHtml()
|
||||||
|
+ '<p style="color:#556">Every package pays your sponsor line in the same on-chain transaction, and every payout is on the public ledger.</p>';
|
||||||
|
return { subject, text, html };
|
||||||
|
}
|
||||||
|
const esc = s => String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
|
// ---- the pass -------------------------------------------------------------------------------
|
||||||
|
let running = false;
|
||||||
|
async function tick() {
|
||||||
|
if (running || !cfg().on) return { sent: 0 };
|
||||||
|
running = true;
|
||||||
|
try {
|
||||||
|
const c = cfg();
|
||||||
|
const all = await X.ads.adminList(1000);
|
||||||
|
const mine = all.filter(x => !x.house && x.owner);
|
||||||
|
// group the campaigns that need a notice by owner, so one person gets one email
|
||||||
|
const due = new Map(); // owner -> { state -> [campaigns] }
|
||||||
|
for (const cm of mine) {
|
||||||
|
const b = n0(cm.budget); if (b <= 0) continue;
|
||||||
|
let state = null;
|
||||||
|
if (cm.status === 'out') state = 'out';
|
||||||
|
else if (cm.status === 'active' && delivered(cm) >= b * (c.lowPct / 100)) state = 'low';
|
||||||
|
if (!state) continue;
|
||||||
|
if (S.sent[key(cm.id, state)]) continue;
|
||||||
|
if (state === 'low' && S.sent[key(cm.id, 'out')]) continue; // already past it
|
||||||
|
const g = due.get(cm.owner) || {}; (g[state] = g[state] || []).push(cm); due.set(cm.owner, g);
|
||||||
|
}
|
||||||
|
let sent = 0;
|
||||||
|
for (const [owner, byState] of due) {
|
||||||
|
if (sent >= MAX_PER_TICK) break;
|
||||||
|
for (const state of ['out', 'low']) {
|
||||||
|
const list = byState[state]; if (!list || !list.length) continue;
|
||||||
|
const ok = await notify(owner, list, state).catch(e => { console.error('refill notify', owner, e.message); return false; });
|
||||||
|
if (ok) sent++;
|
||||||
|
break; // one state per owner per pass: "out" is the louder message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await checkConversions();
|
||||||
|
if (sent) { save(); console.log('refill notices sent:', sent); }
|
||||||
|
return { sent };
|
||||||
|
} finally { running = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function notify(owner, list, state) {
|
||||||
|
const acct = await X.accounts.byEmail(owner); if (!acct) return false;
|
||||||
|
const stopped = X.drip && X.drip.isUnsubscribed ? await X.drip.isUnsubscribed(owner).catch(() => false) : false;
|
||||||
|
let credits = 0; try { credits = await X.creditsFor(owner); } catch (e) {}
|
||||||
|
const m = await compose(owner, list, state, credits);
|
||||||
|
// the on-site notice always goes; the email is skipped for anyone who opted out
|
||||||
|
try { await X.message(owner, m.subject, m.html); } catch (e) { console.error('refill inbox', e.message); }
|
||||||
|
if (!stopped && X.mailer.hasKey()) { try { await X.mailer.send(owner, m.subject, m.text); } catch (e) { console.error('refill mail', e.message); } }
|
||||||
|
const rec = {
|
||||||
|
id: ++S.seq, owner, state, ts: Date.now(), emailed: !stopped && X.mailer.hasKey(),
|
||||||
|
campaigns: list.map(c => ({ id: c.id, name: c.name || null, type: c.type || null, budget: Math.round(n0(c.budget)), delivered: Math.round(delivered(c)), imps: n0(c.imps), clicks: n0(c.clicks) })),
|
||||||
|
creditsAtSend: Math.round(credits), memberId: acct.memberId || 0, converted: null,
|
||||||
|
};
|
||||||
|
for (const cm of list) S.sent[key(cm.id, state)] = rec.id;
|
||||||
|
S.records = S.records || {}; S.records[rec.id] = rec;
|
||||||
|
save();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- did anything come of it? ---------------------------------------------------------------
|
||||||
|
// Polled rather than hooked: a relaunch, a top-up and a purchase all show up in state, and polling
|
||||||
|
// cannot miss one because a call site was forgotten.
|
||||||
|
async function checkConversions() {
|
||||||
|
S.records = S.records || {};
|
||||||
|
const open = Object.values(S.records).filter(r => !r.converted && Date.now() - r.ts < LOOKBACK);
|
||||||
|
if (!open.length) return;
|
||||||
|
const all = await X.ads.adminList(1000);
|
||||||
|
const evs = (X.chain.recentEvents(4000) || []).filter(e => e.type === 'Purchase');
|
||||||
|
for (const r of open) {
|
||||||
|
const since = r.ts;
|
||||||
|
// 1. bought credits (any position on the account)
|
||||||
|
let ids = [r.memberId].filter(Boolean);
|
||||||
|
try { const ps = await X.accounts.positions(r.owner); ids = [...new Set([...ids, ...ps.map(p => p.memberId).filter(Boolean)])]; } catch (e) {}
|
||||||
|
const buy = evs.find(e => ids.includes(Number(e.id || e.buyerId)) && norm(e) > since);
|
||||||
|
if (buy) { mark(r, 'purchase', { cents: n0(buy.priceCents), tx: buy.tx || null }); continue; }
|
||||||
|
// 2. put an ad back up, or topped one up
|
||||||
|
const fresh = all.find(c => c.owner === r.owner && !c.house && (n0(c.created) > since || (c.status === 'active' && !r.campaigns.some(x => x.id === c.id))));
|
||||||
|
if (fresh) { mark(r, 'relaunch', { campaignId: fresh.id, name: fresh.name || null }); continue; }
|
||||||
|
const topped = all.find(c => { const was = r.campaigns.find(x => x.id === c.id); return was && n0(c.budget) > was.budget; });
|
||||||
|
if (topped) { mark(r, 'topup', { campaignId: topped.id, added: Math.round(n0(topped.budget) - r.campaigns.find(x => x.id === topped.id).budget) }); }
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
const norm = e => { let t = n0(e.ts); if (t < 1e12) t *= 1000; return t; };
|
||||||
|
function mark(r, kind, detail) {
|
||||||
|
r.converted = { kind, at: Date.now(), hours: Math.round((Date.now() - r.ts) / 3600000), detail: detail || null };
|
||||||
|
console.log('refill converted:', r.owner, kind, 'after', r.converted.hours + 'h');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- what the admin sees --------------------------------------------------------------------
|
||||||
|
function stats() {
|
||||||
|
S.records = S.records || {};
|
||||||
|
const recs = Object.values(S.records).sort((a, b) => b.ts - a.ts);
|
||||||
|
const by = { out: { sent: 0, converted: 0 }, low: { sent: 0, converted: 0 } };
|
||||||
|
const kinds = {};
|
||||||
|
let revenueCents = 0, hours = [];
|
||||||
|
for (const r of recs) {
|
||||||
|
const b = by[r.state] || (by[r.state] = { sent: 0, converted: 0 });
|
||||||
|
b.sent++;
|
||||||
|
if (r.converted) {
|
||||||
|
b.converted++; kinds[r.converted.kind] = (kinds[r.converted.kind] || 0) + 1;
|
||||||
|
hours.push(r.converted.hours);
|
||||||
|
if (r.converted.kind === 'purchase') revenueCents += n0(r.converted.detail && r.converted.detail.cents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const conv = recs.length ? Math.round(recs.filter(r => r.converted).length / recs.length * 100) : 0;
|
||||||
|
return {
|
||||||
|
enabled: cfg().on, lowPct: cfg().lowPct,
|
||||||
|
totals: { sent: recs.length, converted: recs.filter(r => r.converted).length, ratePct: conv,
|
||||||
|
purchasesUsd: Math.round(revenueCents / 100), medianHours: hours.length ? hours.sort((a, b) => a - b)[Math.floor(hours.length / 2)] : null },
|
||||||
|
byState: by, byKind: kinds,
|
||||||
|
recent: recs.slice(0, 60).map(r => ({ id: r.id, owner: r.owner, state: r.state, at: r.ts, emailed: r.emailed,
|
||||||
|
credits: r.creditsAtSend, campaigns: r.campaigns.length, imps: r.campaigns.reduce((n, c) => n + c.imps, 0),
|
||||||
|
converted: r.converted })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { init, tick, stats, PACKS };
|
||||||
@@ -39,7 +39,8 @@ const audit = require('./audit'); // counter audit: views vs delivery logs,
|
|||||||
const leaderboard = require('./leaderboard');
|
const leaderboard = require('./leaderboard');
|
||||||
const toolkit = require('./toolkit');
|
const toolkit = require('./toolkit');
|
||||||
const badge = require('./badge'); // achievement badge image drawn on the server with ffmpeg (2026-09-16)
|
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 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 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)
|
||||||
@@ -414,6 +415,15 @@ async function boot() {
|
|||||||
friday.init({ dataDir: DATA_DIR, siteConfig, chain, ads, accounts, mailer, drip,
|
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); },
|
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, '&').replace(/</g, '<').replace(/>/g, '>').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'); } });
|
notify: async (memberId, subject, text, kind) => { const a = await accounts.byMemberId(memberId); if (!a || !a.email) return; const html = '<p>' + String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').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(() => 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);
|
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' });
|
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 });
|
if (!isAdmin(req)) return json(res, 200, { admin: false });
|
||||||
return json(res, 200, { admin: true, email: ADMIN_EMAIL });
|
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 (p === '/api/admin/overview' && req.method === 'GET') {
|
||||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||||
const camps = await ads.adminList();
|
const camps = await ads.adminList();
|
||||||
|
|||||||
Reference in New Issue
Block a user