diff --git a/ads.js b/ads.js index f0bbc7a..3b96e9d 100644 Binary files a/ads.js and b/ads.js differ diff --git a/audit.js b/audit.js index 3e42fff..44c5908 100644 --- a/audit.js +++ b/audit.js @@ -3,13 +3,18 @@ // checked against actual shows. Runs on demand from Admin > Reports and once a day; any issue alerts the admin. // DB mode only (production); JSON mode reports "not checked". const db = require('./db'); -let R = null; // { notify(text) } +const fs = require('fs'); const path = require('path'); +let R = null; // { notify(text), dataDir } function init(refs) { R = refs; } +// credits already returned for past counting errors: subtract them so history that was made right does not keep flagging +function refunded() { const out = {}; try { for (const f of fs.readdirSync(R.dataDir)) if (/^refunds-.*\.json$/.test(f)) for (const i of (JSON.parse(fs.readFileSync(path.join(R.dataDir, f), 'utf8')).items || [])) out[i.id] = (out[i.id] || 0) + Number(i.credits || 0); } catch (e) {} return out; } async function run() { const out = { checkedAt: Date.now(), checks: [] }; if (!db.enabled()) { out.checks.push({ name: 'Counters', ok: true, detail: 'JSON mode: not checked', issues: [] }); return out; } const camps = await db.q("SELECT id, name, type, status, imps, clicks, spent, accrued, budget, created, house FROM campaigns WHERE house=0 OR house IS NULL"); + const back = refunded(); for (const c of camps) if (back[c.id]) c.spent = Math.max(0, Number(c.spent) - back[c.id]); // already made right + const logStart = (await db.q('SELECT MIN(day) d FROM camp_hours'))[0].d; const logStartMs = logStart ? Date.parse(logStart + 'T00:00:00Z') : 0; const byType = t => camps.filter(c => c.type === t); const add = (name, issues, detail) => out.checks.push({ name, ok: !issues.length, detail, issues }); // 1. per-view formats: campaign.imps must equal the rows in the log that paid for them @@ -23,8 +28,9 @@ async function run() { { const rows = await db.q('SELECT campaign_id, SUM(n) n FROM camp_hours GROUP BY campaign_id'); const h = {}; for (const r of rows) h[r.campaign_id] = Number(r.n); const issues = []; - for (const c of camps.filter(c => ['banner', 'text'].includes(c.type))) { const l = h[c.id] || 0; if (Math.abs(Number(c.imps) - l) > 2) issues.push('#' + c.id + ' ' + c.name.slice(0, 30) + ': ' + c.imps + ' views vs ' + l + ' in the hourly log'); } - add('banner/text views vs hourly log', issues, byType('banner').length + byType('text').length + ' campaigns compared'); + // only campaigns that started after the hourly log did + for (const c of camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs)) { const l = h[c.id] || 0; if (Math.abs(Number(c.imps) - l) > 2) issues.push('#' + c.id + ' ' + c.name.slice(0, 30) + ': ' + c.imps + ' views vs ' + l + ' in the hourly log'); } + add('banner/text views vs hourly log', issues, camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs).length + ' campaigns compared (started since the hourly log began ' + (logStart || '') + ')'); } // 3. login: days charged vs days shown (a day may only be charged after the ad was shown) { diff --git a/server.js b/server.js index 832316c..a0e04c3 100644 --- a/server.js +++ b/server.js @@ -380,7 +380,7 @@ async function boot() { releases.init({ dataDir: DATA_DIR }); 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({ 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(() => {}); } }); + 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(() => {}); } }); setTimeout(() => audit.dailyTick(), 5 * 60 * 1000); setInterval(() => audit.dailyTick(), 24 * 60 * 60 * 1000); videomaker.init({ dataDir: DATA_DIR, spaces, accounts }); leaderboard.init({ chain, accounts, ads, dataDir: DATA_DIR, siteConfig, pushFeed, adminEmail: ADMIN_EMAIL,