From 67fb8193bdfa06b8bebc060df65efe18270526c1 Mon Sep 17 00:00:00 2001 From: martbost Date: Tue, 22 Sep 2026 05:26:20 -0500 Subject: [PATCH] Refill conversions: only count a genuine response. An unrelated ad already running is not a relaunch, and a still-running low campaign is not a conversion at all; adds a reset lever to re-decide verdicts Co-Authored-By: Claude Opus 5 --- refill.js | 25 ++++++++++++++++++++++--- server.js | 5 +++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/refill.js b/refill.js index 8ecfa36..7b9a6d1 100644 --- a/refill.js +++ b/refill.js @@ -180,9 +180,17 @@ async function checkConversions() { 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)))); + // 2. put an ad back up. Only two things count: a campaign created after we wrote to them, or one of + // the very campaigns we named coming back to life. An unrelated ad that was already running is not a + // response to the email, and counting it was inflating the rate to meaninglessness. + const fresh = all.find(c => c.owner === r.owner && !c.house && n0(c.created) > since); if (fresh) { mark(r, 'relaunch', { campaignId: fresh.id, name: fresh.name || null }); continue; } + // only meaningful for an ad that had actually stopped: a "running low" ad is still active by + // definition, so counting that as a response would mark every low notice converted on the spot + if (r.state === 'out') { + const revived = all.find(c => c.status === 'active' && r.campaigns.some(x => x.id === c.id) && delivered(c) < n0(c.budget)); + if (revived) { mark(r, 'resumed', { campaignId: revived.id, name: revived.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) }); } } @@ -222,4 +230,15 @@ function stats() { }; } -module.exports = { init, tick, stats, PACKS }; +// clears conversion verdicts so the corrected rule can re-decide them (used once after the +// first run, where "already had another ad running" was wrongly counted as a relaunch) +function resetConversions(kinds) { + S.records = S.records || {}; + const want = new Set(kinds && kinds.length ? kinds : ['relaunch']); + let n = 0; + for (const r of Object.values(S.records)) if (r.converted && want.has(r.converted.kind)) { r.converted = null; n++; } + save(); + return { cleared: n }; +} + +module.exports = { init, tick, stats, resetConversions, PACKS }; diff --git a/server.js b/server.js index 0dff860..b495fa8 100644 --- a/server.js +++ b/server.js @@ -2912,6 +2912,11 @@ const server = http.createServer(async (req, res) => { if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); return json(res, 200, refill.stats()); } + if (p === '/api/admin/refill/reset' && req.method === 'POST') { // re-decide conversions under a corrected rule + if (!isAdmin(req)) return json(res, 401, { error: 'auth' }); + const b = await readBody(req); + return json(res, 200, refill.resetConversions(Array.isArray(b.kinds) ? b.kinds : null)); + } 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());