Daily login bonus (streak) + solo-ad budget default

- Daily login bonus: once/day, base 5 credits + gentle streak (+1/day, cap +5)
  for consecutive days; granted after sign-in, toast + cha-ching. New
  login_day/login_streak on earned_credits; /api/my/login-bonus.
- Solo ad: selecting Solo now defaults Budget to 50 credits (5cr x 10 min
  deliveries) so entering 10 isn't rejected after submit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-07 08:19:40 -05:00
parent a8968041dc
commit 4a9e4c789e
5 changed files with 20 additions and 1 deletions
BIN
View File
Binary file not shown.
+2
View File
@@ -72,6 +72,8 @@ async function bootstrap() {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
const alterSafe0 = async sql => { try { await q(sql); } catch (e) { if (!['ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME'].includes(e.code)) throw e; } }; const alterSafe0 = async sql => { try { await q(sql); } catch (e) { if (!['ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME'].includes(e.code)) throw e; } };
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN milestones VARCHAR(255) NULL'); // comma-joined granted milestone keys await alterSafe0('ALTER TABLE earned_credits ADD COLUMN milestones VARCHAR(255) NULL'); // comma-joined granted milestone keys
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN login_day CHAR(10) NULL'); // last daily-login-bonus day
await alterSafe0('ALTER TABLE earned_credits ADD COLUMN login_streak INT NOT NULL DEFAULT 0'); // consecutive-day streak
// additive columns (MySQL 8 has no IF NOT EXISTS for columns) // additive columns (MySQL 8 has no IF NOT EXISTS for columns)
const alterSafe = async sql => { const alterSafe = async sql => {
try { await q(sql); } try { await q(sql); }
+11
View File
@@ -285,6 +285,15 @@
chatSync(d); chatSync(d);
MYID = d.memberId || MYID; MYID = d.memberId || MYID;
startLiveFeed(); startLiveFeed();
if (!window.__lbChecked) { // daily login bonus, once per session (server guards once/day)
window.__lbChecked = true;
api('/api/my/login-bonus').then(r => {
if (r && r.granted > 0) {
IAP.status('🎁 Daily login bonus: +' + r.granted + ' credits' + (r.streak > 1 ? ' · ' + r.streak + '-day streak' : '') + '!', 'ok');
playSound('chaching'); setTimeout(loadDashboard, 900);
}
}).catch(() => {});
}
const wc = d.welcomeCredits || 0; const wc = d.welcomeCredits || 0;
$('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString(); $('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString();
$('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : ''; $('dbCreditsSub').textContent = wc ? (d.credits || 0).toLocaleString() + ' purchased · ' + wc + ' welcome' : '';
@@ -592,6 +601,8 @@
// fixed-cost types derive their spend (featured = day slots, visits = flat pack), // fixed-cost types derive their spend (featured = day slots, visits = flat pack),
// so hide the free-form Budget field for them to avoid confusion // so hide the free-form Budget field for them to avoid confusion
$('cBudgetRow').hidden = (t === 'featured' || t === 'visits'); $('cBudgetRow').hidden = (t === 'featured' || t === 'visits');
// solo ads have a real floor (5cr x 10 deliveries): default to 50 so 10 isn't rejected
if (t === 'solo' && (!$('cBudget').value || Number($('cBudget').value) < 50)) $('cBudget').value = (lastRates && lastRates.soloCostPerRecipient ? lastRates.soloCostPerRecipient : 5) * 10;
if (t === 'visits') visitHint(); if (t === 'visits') visitHint();
$('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)'; $('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)';
soloHint(); soloHint();
+1 -1
View File
@@ -644,7 +644,7 @@
</div> </div>
<script src="/assets/common.js?v=20260907n"></script> <script src="/assets/common.js?v=20260907n"></script>
<script src="/assets/wallet.js?v=20260907f"></script> <script src="/assets/wallet.js?v=20260907f"></script>
<script src="/assets/my.js?v=20260907o"></script> <script src="/assets/my.js?v=20260907p"></script>
<script src="/assets/chat.js?v=20260907l"></script> <script src="/assets/chat.js?v=20260907l"></script>
</body> </body>
</html> </html>
+6
View File
@@ -715,6 +715,12 @@ const server = http.createServer(async (req, res) => {
accounts.touchSeen(s.email).catch(() => {}); accounts.touchSeen(s.email).catch(() => {});
return json(res, 200, { ok: true, chatUnread: await messages.chatUnread(s.email) }); return json(res, 200, { ok: true, chatUnread: await messages.chatUnread(s.email) });
} }
// daily login bonus (once/day, gentle streak) — granted after the login flow
if (p === '/api/my/login-bonus' && req.method === 'POST') {
const s = await auth.fromRequest(req);
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
return json(res, 200, await ads.grantLoginBonus(s.email));
}
// -- rehearsal test-POL faucet: top a connected wallet up to 10 test-POL // -- rehearsal test-POL faucet: top a connected wallet up to 10 test-POL
// (anvil_setBalance, no key needed). Rehearsal-only, rate-limited. // (anvil_setBalance, no key needed). Rehearsal-only, rate-limited.
if (p === '/api/my/faucet' && req.method === 'POST') { if (p === '/api/my/faucet' && req.method === 'POST') {