Admin portal (/admin) + free house ads; POL amounts show two decimals

- /admin: email magic-code sign-in allowlisted to ADMIN_EMAIL, 12h admin
  session (cookie iap.adm, persisted in the volume). Bearer ADMIN_PASSWORD
  API access still works. Member area shows an Admin link for that email.
- House ads: admin places banner/text/login/solo/video/featured/visits
  campaigns owned by house@instantadpay.com that cost nothing; budget is
  only a delivery cap, spend is never charged or burned.
- Admin APIs: overview, all campaigns (+pause/resume any), members
  (+re-point sponsor), reports (+resolve), pending burns, rates/site
  config get+patch, creative upload.
- fmtPol rounds to two decimals everywhere (dashboard, toasts, prices).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-09 05:15:19 -05:00
parent b6a0729335
commit 438b1921e9
18 changed files with 725 additions and 47 deletions
+18 -1
View File
@@ -159,6 +159,13 @@ const J = {
this.save();
return { ok: true, account: pub(acct) };
},
async listAll(limit) { return Object.values(this.db.byEmail).sort((a, b) => (b.created || 0) - (a.created || 0)).slice(0, limit).map(pub); },
async setSponsorRef(e, ref) {
const acct = this.db.byEmail[e];
if (!acct) return { error: 'No such account.' };
acct.sponsorRef = ref; this.save();
return { ok: true, account: pub(acct) };
},
async count() { return Object.keys(this.db.byEmail).length; }
};
@@ -268,6 +275,12 @@ const D = {
}
return { ok: true, account: await this.byEmail(e) };
},
async listAll(limit) { const rows = await db.q('SELECT * FROM accounts ORDER BY created DESC LIMIT ?', [Number(limit) || 500]); return rows.map(rowPub); },
async setSponsorRef(e, ref) {
const r = await db.q('UPDATE accounts SET sponsor_ref=? WHERE email=?', [ref, e]);
if (!r.affectedRows) return { error: 'No such account.' };
return { ok: true, account: await this.byEmail(e) };
},
async count() { const r = await db.q('SELECT COUNT(*) n FROM accounts'); return Number(r[0].n); }
};
@@ -332,6 +345,10 @@ async function linkWallet(email, address) {
return impl().linkWallet(normEmail(email), a);
}
async function count() { return impl().count(); }
// admin: newest-first account list, and re-pointing a member's sponsor (a
// username, share code, or numeric member id: the same tokens join links use)
async function listAll(limit = 500) { return impl().listAll(limit); }
async function setSponsorRef(email, ref) { return impl().setSponsorRef(normEmail(email), String(ref || '').trim().toLowerCase().slice(0, 40)); }
// resolve a member's DIRECT sponsor account (the token they joined under)
async function sponsorOf(email) {
@@ -355,7 +372,7 @@ async function getChatSettings(email) {
return { available: a ? a.chatAvailable !== false : true, mutes: await impl().getMutes(String(email || '').toLowerCase()) };
}
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername,
module.exports = { init, signup, login, ensure, byEmail, byAddress, byCode, byUsername, listAll, setSponsorRef,
setUsername, setMemberId, namesForMembers, listByReferrer, downline, linkWallet, count,
setLineBanner: (e, b, t) => impl().setLineBanner(String(e || '').toLowerCase(), b, t),
setProfile: (e, a, bio, socials) => impl().setProfile(String(e || '').toLowerCase(), a, bio, socials),