Video sources: check the house path too, and re-check the live ones
Two holes left by videoCheck, which only ever ran when a campaign was saved. The house-ad route never called it at all. It guarded /api/my/campaigns and not /api/admin/campaigns, so a bad source pasted into a house video would hand every viewer a black player exactly as #146 did. And nothing re-checked anything after it went live. #146 pointed at https://yourdomain.com/ from 16 September; the check shipped on the 22nd and the campaign sat live for another two days, burning a daily video slot for everyone who drew it, until Marty hit it himself and asked why nothing played. A save-time check can never catch a campaign that predates it, or a link that rots later. videosweep re-checks every live video source daily. A source that fails is PAUSED, never deleted: the advertiser keeps every unspent credit, because the reservation is computed rather than deducted, and can restart it once the link is fixed. It reports what it paused to the admin channel rather than doing it quietly, since a guard nobody hears from is how the first one went unnoticed. POST /api/admin/videosweep runs it on demand, with { dry: true } to look first. QA green, 0 bugs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ const adminMember = require('./adminmember');
|
||||
const syndicate = require('./syndicate');
|
||||
const releases = require('./releases');
|
||||
const ledger = require('./ledger');
|
||||
const videosweep = require('./videosweep');
|
||||
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 leaderboard = require('./leaderboard');
|
||||
@@ -479,6 +480,16 @@ async function boot() {
|
||||
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
|
||||
releases.init({ dataDir: DATA_DIR });
|
||||
ledger.init({ dataDir: DATA_DIR });
|
||||
// re-check live video sources on a schedule: videoCheck only ever runs at save time, so a
|
||||
// campaign created before it existed, or a link that rots later, is otherwise invisible
|
||||
videosweep.init({ dataDir: DATA_DIR, fs, path, db, videoCheck,
|
||||
alert: async text => {
|
||||
const sc = siteConfig();
|
||||
if (sc.telegramBotToken && sc.telegramAdminChatId) return telegramSend(sc.telegramAdminChatId, text);
|
||||
if (sc.telegramBotToken && sc.telegramEchoChatId) return telegramSend(sc.telegramEchoChatId, text, sc.telegramEchoTopicId);
|
||||
if (ADMIN_EMAIL && mailer.hasKey()) return mailer.send(ADMIN_EMAIL, 'InstantAdPay: broken video sources', text.replace(/<[^>]+>/g, ''));
|
||||
} });
|
||||
videosweep.start();
|
||||
toolkit.init({ dataDir: DATA_DIR, ads, accounts, siteConfig, coach, messages, promos, videomaker, chain });
|
||||
updates.init({ dataDir: DATA_DIR, accounts, releases, mailer, drip, sendy, adminEmail: ADMIN_EMAIL });
|
||||
audit.init({ dataDir: DATA_DIR, notify: text => { const sc = siteConfig(); if (sc.telegramBotToken && sc.telegramAdminChatId) telegramSend(sc.telegramAdminChatId, text).catch(() => {}); else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: counter audit', text).catch(() => {}); } });
|
||||
@@ -3066,6 +3077,9 @@ const server = http.createServer(async (req, res) => {
|
||||
if (p === '/api/admin/campaigns' && req.method === 'POST') { // free house ad
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
// A house video was never checked: videoCheck guarded the member path only, so a bad
|
||||
// source pasted here would hand every viewer a black player exactly as #146 did.
|
||||
if (String(b.type) === 'video') { const vc = await videoCheck(b.videoUrl); if (!vc.ok) return json(res, 400, { error: vc.reason }); }
|
||||
if (!['login', 'solo', 'video', 'featured'].includes(String(b.type || ''))) {
|
||||
if (b.type === 'banner' || (b.type === 'login' && b.imageUrl)) { const ic = await imageCheck(b.imageUrl); if (!ic.ok) return json(res, 400, { error: ic.reason }); }
|
||||
const fc = await frameCheck(b.targetUrl);
|
||||
@@ -3225,6 +3239,11 @@ const server = http.createServer(async (req, res) => {
|
||||
const r = ledger.remove(String(b.id || ''));
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/admin/videosweep' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
return json(res, 200, await videosweep.run({ dry: !!b.dry }));
|
||||
}
|
||||
if (p === '/api/admin/burner' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
return json(res, 200, burner.status());
|
||||
|
||||
Reference in New Issue
Block a user