Campaigns: archive a finished one, get the credits back, keep the numbers
A member asked for a delete button. Deleting would orphan the credit ledger, the hourly view rows and the P&L, and throw away the figures they actually want, so a campaign is archived instead: out of the working table, listed in full underneath with its views, clicks and spend, and restorable. The credits are the real point. Reserved budget is computed from campaigns that are active or paused, so a paused campaign quietly holds credits the member cannot spend anywhere else. Archiving releases them the moment the status changes, with nothing to refund and nothing that can double-refund. Only a paused or finished campaign can be archived, so pause stays the deliberate first step, and restoring brings it back paused rather than live. The syndication rails are stopped on the way in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2848,10 +2848,14 @@ const server = http.createServer(async (req, res) => {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const memberId = await auth.refreshMemberId(s);
|
||||
const out = { campaigns: await ads.listCampaigns(s.email), rates: ads.rates(), bannerSizes: ads.bannerSizes() };
|
||||
out.clickSources = await coach.clickSources(out.campaigns.map(c => c.id));
|
||||
out.hours = await ads.hoursFor(out.campaigns.map(c => c.id)); // on-site views per UTC hour, last 7 days
|
||||
out.geo = await ads.geoFor(out.campaigns.map(c => c.id)); // on-site serves per viewer country
|
||||
const all = await ads.listCampaigns(s.email);
|
||||
// archived ones keep their full stats but leave the working list (Marty, 2026-09-23)
|
||||
const out = { campaigns: all.filter(c => c.status !== 'archived'), archived: all.filter(c => c.status === 'archived'),
|
||||
rates: ads.rates(), bannerSizes: ads.bannerSizes() };
|
||||
const ids = all.map(c => c.id); // archived included: their numbers are the whole point of keeping them
|
||||
out.clickSources = await coach.clickSources(ids);
|
||||
out.hours = await ads.hoursFor(ids); // on-site views per UTC hour, last 7 days
|
||||
out.geo = await ads.geoFor(ids); // on-site serves per viewer country
|
||||
{ const t = geo.tierLists(siteConfig()); out.tiers = { t1: [...t.t1], t2: [...t.t2] }; out.geoReady = geo.status().loaded; }
|
||||
const pool = await ads.balances((await myMemberIds(s)).ids, s.email);
|
||||
out.purchasedCredits = pool.total;
|
||||
@@ -2905,6 +2909,13 @@ const server = http.createServer(async (req, res) => {
|
||||
const r = await ads.extendFeatured(s.email, memberId, m[1], b.days);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
m = /^\/api\/my\/campaigns\/(\d+)\/(archive|unarchive)$/.exec(p);
|
||||
if (m && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
|
||||
const r = m[2] === 'archive' ? await ads.archive(s.email, m[1]) : await ads.unarchive(s.email, m[1]);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
m = /^\/api\/my\/campaigns\/(\d+)\/(pause|resume)$/.exec(p);
|
||||
if (m && req.method === 'POST') {
|
||||
const s = await auth.fromRequest(req);
|
||||
|
||||
Reference in New Issue
Block a user