Credit activity ledger: every earn and spend logged with a reason, listed under Campaigns

Hugh earned a 10-credit sign-in bonus, then bought a 280-credit extension; earned credits spend
first, so Earned read 0 and the bonus looked lost. There was no record a member could check.
Now every real credit movement writes a credit_log row (JSON store in file mode): welcome, sign-in
bonus with streak, daily claim, inbox reads, video watches, verified visits, milestone/leaderboard
bonuses, partner codes, admin/team grants, AI Copy Engine charges + refunds, flat buys (featured
run, extension, visit pack, more visits), login-ad days, and metered delivery rolled up per
campaign per day; on-chain purchased-credit spends are marked. GET /api/my/credits/activity;
dashboard shows the last 40 under Campaigns (also when there are no campaigns) with a link from
Earn credits. Chatbot canned answer + AI prompt updated; my.js tag bumped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-16 08:41:43 -05:00
parent ffacf741da
commit 69d672665c
8 changed files with 50 additions and 14 deletions
+18 -1
View File
@@ -779,6 +779,21 @@
} catch (e) {}
}
// credit activity ledger under the campaign table (2026-09-16): every earn and spend with its reason
async function loadCreditActivity(el) {
try {
const r = await (await fetch('/api/my/credits/activity')).json();
if (!r.items || !r.items.length) return;
const box = document.createElement('div'); box.id = 'creditActivity'; box.style.marginTop = '16px';
const when = ts => new Date(ts).toLocaleString([], { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' });
box.innerHTML = '<h4 style="margin:0 0 4px">Credit activity</h4>'
+ '<p class="muted small" style="margin:0 0 8px">Every credit that came in or went out, newest first. Earned credits are spent before purchased ones, so an earn followed by a buy shows as two lines here. Views a campaign delivers roll up into one line per day.</p>'
+ '<div class="tablewrap"><table class="camp-table act-table"><tbody>'
+ r.items.map(i => '<tr><td class="muted small" style="white-space:nowrap">' + when(i.ts) + '</td><td class="num" style="white-space:nowrap"><b style="color:' + (i.delta > 0 ? '#3ecf7a' : 'inherit') + '">' + (i.delta > 0 ? '+' : '') + i.delta.toLocaleString() + '</b></td><td>' + esc(i.note) + '</td></tr>').join('')
+ '</tbody></table></div>';
el.appendChild(box);
} catch (e) {}
}
async function loadCampaigns() {
try {
const r = await (await fetch('/api/my/campaigns')).json();
@@ -808,7 +823,7 @@
+ (r.rates.soloCostPerRecipient || 5) + 'cr/delivery';
const el = $('campList');
el.innerHTML = '';
if (!r.campaigns.length) { el.innerHTML = '<p class="muted small">No campaigns yet. Launch your first below.</p>'; return; }
if (!r.campaigns.length) { el.innerHTML = '<p class="muted small">No campaigns yet. Launch your first below.</p>'; loadCreditActivity(el); return; }
const tbl = document.createElement('div');
tbl.className = 'tablewrap';
tbl.innerHTML = '<table class="camp-table"><thead><tr><th>Name</th><th>Type</th><th class="num">Views here</th><th class="num" title="Impressions delivered by Network Ad Space across the wider network (banner and text ads only)">Network views</th><th class="num">Clicks</th>'
@@ -832,6 +847,7 @@
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);
loadCreditActivity(el);
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' });
@@ -2100,6 +2116,7 @@
if (st.error) return null;
$('earnProgress').textContent = 'today: ' + st.views + ' / ' + st.target + ' ads viewed';
$('earnBalance').textContent = (st.earnedAvailable != null ? st.earnedAvailable : st.earned) + ' earned credits available' + (st.reserved ? ' · ' + st.reserved + ' in live campaigns' : '');
if ($('earnActLink')) $('earnActLink').onclick = e => { e.preventDefault(); jumpTo('campaigns', 'creditActivity'); };
const done = st.views >= st.target;
$('earnClaimBtn').hidden = !(done && !st.claimed);
if (st.claimed) {