diff --git a/ads.js b/ads.js index 33d6cd0..f552251 100644 Binary files a/ads.js and b/ads.js differ diff --git a/db.js b/db.js index 4bd412a..96af0c5 100644 --- a/db.js +++ b/db.js @@ -72,6 +72,8 @@ async function bootstrap() { ) 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; } }; 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) const alterSafe = async sql => { try { await q(sql); } diff --git a/public/assets/my.js b/public/assets/my.js index 704f441..6ab9aac 100644 --- a/public/assets/my.js +++ b/public/assets/my.js @@ -285,6 +285,15 @@ chatSync(d); MYID = d.memberId || MYID; 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; $('dbCredits').textContent = ((d.credits || 0) + wc).toLocaleString(); $('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), // so hide the free-form Budget field for them to avoid confusion $('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(); $('cTitle').placeholder = t === 'solo' ? 'Subject line (max 80)' : 'Headline (max 60)'; soloHint(); diff --git a/public/my.html b/public/my.html index 1c3ae30..77cde4f 100644 --- a/public/my.html +++ b/public/my.html @@ -644,7 +644,7 @@ - + diff --git a/server.js b/server.js index cfee36b..5d1098b 100644 --- a/server.js +++ b/server.js @@ -715,6 +715,12 @@ const server = http.createServer(async (req, res) => { accounts.touchSeen(s.email).catch(() => {}); 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 // (anvil_setBalance, no key needed). Rehearsal-only, rate-limited. if (p === '/api/my/faucet' && req.method === 'POST') {