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:
martbost
2026-09-24 13:56:28 -05:00
parent 3d4b07a8ab
commit 2854e2a7ba
2 changed files with 94 additions and 0 deletions
+19
View File
@@ -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());