Member identity, promo tools, back-office color, seeded inventory

Real people, not numbers: usernames (unique, profile pane to set them),
shown across the ledger, activity, rosters, and the sidebar chip; vanity
invite links (/join/<username>) with late chain binding intact. Promo tools
pane ships Branded Voice share posts and an email swipe personalized with
each member's link. The earn viewer excludes a member's own campaigns, so
nobody earns from their own spend. Back office gains the cyan/violet/amber
accent family over the green base. Three house campaigns seeded so every
surface shows live inventory. Assets v=20260905i.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-05 06:28:17 -05:00
parent 28ed925443
commit e22021c266
11 changed files with 257 additions and 53 deletions
+7 -5
View File
@@ -102,9 +102,10 @@ const J = {
this.save();
return { ok: true, campaign: pubC(c) };
},
async serve(type) {
async serve(type, opts) {
const r = rates();
const pool = this.db.campaigns.filter(c => c.type === type && c.status === 'active');
const ex = opts && opts.excludeEmail;
const pool = this.db.campaigns.filter(c => c.type === type && c.status === 'active' && (!ex || c.owner !== ex));
if (!pool.length) return null;
const c = pool[Math.floor(Math.random() * pool.length)];
c.imps += 1;
@@ -194,9 +195,10 @@ const D = {
const rows = await db.q('SELECT * FROM campaigns WHERE id=?', [Number(id)]);
return { ok: true, campaign: pubC(rowC(rows[0])) };
},
async serve(type) {
async serve(type, opts) {
const r = rates();
const rows = await db.q('SELECT * FROM campaigns WHERE type=? AND status=\'active\' ORDER BY RAND() LIMIT 1', [type]);
const ex = (opts && opts.excludeEmail) || '';
const rows = await db.q('SELECT * FROM campaigns WHERE type=? AND status=\'active\' AND owner_email<>? ORDER BY RAND() LIMIT 1', [type, ex]);
if (!rows.length) return null;
const c = rowC(rows[0]);
await db.q('UPDATE campaigns SET imps=imps+1, batch_imps=batch_imps+1 WHERE id=?', [c.id]);
@@ -411,7 +413,7 @@ async function setStatus(owner, id, status) {
if (!['active', 'paused'].includes(status)) return { error: 'Bad status.' };
return impl().setStatus(owner, id, status);
}
async function serve(type) { return TYPES.includes(type) ? impl().serve(type) : null; }
async function serve(type, opts) { return TYPES.includes(type) ? impl().serve(type, opts) : null; }
async function click(id) { return impl().click(id); }
async function dailySweep() { return impl().dailySweep(); }
async function pendingBurns() { return impl().pendingBurns(); }