Featured links: Extend run (book more days) replaces Buy more views; visit packs top up by visits; campaign rows say paid up front and runs through
Hugh and Michael both added credits to a featured run expecting more days. Featured is a flat daily buy, so the credits sat unspent until the run ended and their balance read 0 meanwhile. Now: /api/my/campaigns/:id/extend books extra days after the current run (or from today if it ended) and charges them at once; top-up refuses featured; a visits top-up buys more visits at the pack rate, charged now; rows show paid up front + day count + runs through <date> / run ended; explainer under the campaign table; chatbot canned answer + AI prompt updated; my.js v tag bumped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+29
-4
@@ -819,19 +819,43 @@
|
||||
+ '<td class="num" data-l="Network views">' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : '<span class="muted small" title="only banner and text ads syndicate to the network">n/a</span>') + '</td>'
|
||||
+ '<td class="num" data-l="Clicks">' + c.clicks + (r.clickSources && r.clickSources[c.id] ? '<div class="muted small clk-src" title="where the clicks happened">' + Object.entries(r.clickSources[c.id]).sort((a, b) => b[1] - a[1]).map(([k, v]) => '<span>' + esc(k) + ' ' + v + '</span>').join('') + (c.impsNas ? ' · network: see Network views' : '') + '</div>' : '') + '</td>'
|
||||
+ (c.type === 'visits' ? '<td class="num" data-l="Spent"><span class="muted small" title="visit packs are paid in full when created; nothing more is charged as visits arrive">paid up front</span></td><td class="num" data-l="Delivered"><b>' + c.imps.toLocaleString() + '</b> of ' + Number(c.body || 0).toLocaleString() + ' visits</td>'
|
||||
: c.type === 'featured' ? '<td class="num" data-l="Spent"><span class="muted small" title="featured runs are paid in full when booked (' + (r.rates.featuredPerDay || 40) + ' credits a day); nothing more is charged while the run is live">paid up front</span></td><td class="num" data-l="Run"><b>' + Number(c.body || 0) + '</b> day' + (Number(c.body || 0) === 1 ? '' : 's') + '</td>'
|
||||
: '<td class="num" data-l="Spent">' + c.spent + '</td><td class="num" data-l="Budget">' + c.budget + '</td>')
|
||||
+ '<td data-l="Status">' + (c.status === 'out' ? (c.type === 'visits' ? '<span class="badge">pack delivered</span>' : '<span class="badge amber">budget spent</span>') : c.status === 'done' ? '<span class="muted">ended</span>' : c.scheduled ? '<span class="badge">scheduled</span>' : c.status) + '</td>'
|
||||
+ '<td data-l="Status">' + (c.status === 'out' ? (c.type === 'visits' ? '<span class="badge">pack delivered</span>' : '<span class="badge amber">budget spent</span>') : c.status === 'done' ? '<span class="muted">' + (c.type === 'featured' ? 'run ended' : 'ended') + '</span>' : c.scheduled ? '<span class="badge">scheduled</span>' : c.status) + '</td>'
|
||||
+ '<td class="act">' + (c.status === 'active' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="pause">Pause</button>'
|
||||
: c.status === 'paused' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="resume">Resume</button>' : '')
|
||||
+ ' <button class="btn small sec" data-topup="' + c.id + '">Buy more views</button>'
|
||||
+ (c.type === 'featured' ? ' <button class="btn small sec" data-extend="' + c.id + '" title="book more days at the daily rate">Extend run</button>'
|
||||
: c.type === 'visits' ? ' <button class="btn small sec" data-morevisits="' + c.id + '">Buy more visits</button>'
|
||||
: ' <button class="btn small sec" data-topup="' + c.id + '">Buy more views</button>')
|
||||
+ '</td></tr>').join('') + '</tbody></table>';
|
||||
el.appendChild(tbl);
|
||||
const note = document.createElement('p'); note.className = 'muted small';
|
||||
note.textContent = 'Credits you put into a campaign are set aside the moment you start it, so your Available number drops right away and the campaign spends from its own budget. Featured links and verified-visit packs are paid in full up front; a featured run is sold by the day, so use Extend run to keep it going. Whatever a campaign has not spent comes back to Available when it ends.';
|
||||
el.appendChild(note);
|
||||
el.querySelectorAll('button[data-extend]').forEach(b => b.addEventListener('click', async () => {
|
||||
const rr = r.rates || {}; const durs = rr.featuredDurations || [1, 2, 7]; const per = rr.featuredPerDay || 40;
|
||||
const d = await IAP.ask({ title: 'Extend featured run', text: 'How many more days? Choose ' + durs.join(', ') + '. ' + per + ' credits a day, charged now. The days are added after the current run, or start today if it has ended.', type: 'number', placeholder: 'e.g. ' + durs[durs.length - 1], ok: 'Extend' });
|
||||
if (!d) return;
|
||||
try { const x = await api('/api/my/campaigns/' + b.dataset.extend + '/extend', { days: Number(d) });
|
||||
IAP.status('Extended ' + x.days + ' day' + (x.days === 1 ? '' : 's') + ' for ' + x.cost + ' credits' + (x.resumed ? '. The run is live again.' : '.'), 'ok');
|
||||
await loadCampaigns(); loadDashboard();
|
||||
} catch (e) { IAP.status(e.message, 'bad'); }
|
||||
}));
|
||||
el.querySelectorAll('button[data-morevisits]').forEach(b => b.addEventListener('click', async () => {
|
||||
const per = (r.rates && r.rates.visitCostPerVisit) || 3;
|
||||
const n = await IAP.ask({ title: 'Buy more visits', text: 'How many more verified visits? ' + per + ' credits each, charged now like the original pack.', type: 'number', placeholder: 'e.g. 50', ok: 'Buy visits' });
|
||||
if (!n) return;
|
||||
try { const x = await api('/api/my/campaigns/' + b.dataset.morevisits + '/topup', { credits: Math.floor(Number(n)) * per });
|
||||
IAP.status('Added ' + x.visits + ' visits (' + x.added + ' credits)' + (x.reactivated ? '. The pack is open again.' : '.'), 'ok');
|
||||
await loadCampaigns(); loadDashboard();
|
||||
} catch (e) { IAP.status(e.message, 'bad'); }
|
||||
}));
|
||||
el.querySelectorAll('button[data-camp]').forEach(b => b.addEventListener('click', async () => {
|
||||
try { await api('/api/my/campaigns/' + b.dataset.camp + '/' + b.dataset.act); await loadCampaigns(); }
|
||||
catch (e) { IAP.status(e.message, 'bad'); }
|
||||
}));
|
||||
el.querySelectorAll('button[data-topup]').forEach(b => b.addEventListener('click', async () => {
|
||||
const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' });
|
||||
const n = await IAP.ask({ title: 'Add credits', text: 'How many credits to add to this campaign? More credits buy more views. They are set aside now and spend as views are delivered; anything unspent comes back when the campaign ends.', type: 'number', placeholder: 'e.g. 100', ok: 'Add credits' });
|
||||
if (!n) return;
|
||||
try { const r = await api('/api/my/campaigns/' + b.dataset.topup + '/topup', { credits: Number(n) });
|
||||
IAP.status('Added ' + r.added + ' credits' + (r.reactivated ? ' — campaign is live again.' : '.'), 'ok');
|
||||
@@ -1307,7 +1331,8 @@
|
||||
function schedChips(c) {
|
||||
const now = Date.now(); let s = '';
|
||||
if (c.starts && c.starts > now) s += ' <span class="muted small">starts ' + fmtWhen(c.starts) + '</span>';
|
||||
if (c.expires && c.type !== 'featured') s += ' <span class="muted small">' + (c.expires > now ? 'ends ' : 'ended ') + fmtWhen(c.expires) + '</span>';
|
||||
if (c.expires && c.type === 'featured') { const day = new Date(c.expires - 1).toLocaleDateString([], { month: 'short', day: 'numeric' }); s += ' <span class="muted small">' + (c.expires > now ? 'runs through ' : 'ran through ') + day + '</span>'; }
|
||||
else if (c.expires) s += ' <span class="muted small">' + (c.expires > now ? 'ends ' : 'ended ') + fmtWhen(c.expires) + '</span>';
|
||||
return s;
|
||||
}
|
||||
function geoLine(rows) {
|
||||
|
||||
Reference in New Issue
Block a user