Earn: video stops when leaving the tab/pane, 'all watched today' message, open earn tokens survive a redeploy; admin member search matches as you type, clearer old-site line; QA earn harness follows the done screen
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+4
-3
@@ -234,8 +234,9 @@
|
|||||||
|
|
||||||
<div class="pane" id="pane-members" hidden>
|
<div class="pane" id="pane-members" hidden>
|
||||||
<div class="card" id="memSearchCard">
|
<div class="card" id="memSearchCard">
|
||||||
<div class="card-head"><h3>Find a member</h3><span class="sub">email, @username, member #, share code or wallet address</span></div>
|
<div class="card-head"><h3>Find a member</h3><span class="sub">matches as you type; Enter opens the first match</span></div>
|
||||||
<div style="display:flex;gap:8px;flex-wrap:wrap"><input id="memSearch" placeholder="jim@example.com, @teameb, #24, 0x1234…" style="flex:1;min-width:240px"><button type="button" class="btn small" id="memOpen">Open</button></div>
|
<div style="display:flex;gap:8px;flex-wrap:wrap"><input id="memSearch" autocomplete="off" placeholder="Start typing a name, username, email, member # or wallet…" style="flex:1;min-width:240px"><button type="button" class="btn small" id="memOpen">Open</button></div>
|
||||||
|
<div id="memHits" hidden style="margin-top:6px;border:1px solid var(--line);border-radius:10px;overflow:hidden"></div>
|
||||||
<p class="small" id="memSearchMsg" hidden style="margin:8px 0 0"></p>
|
<p class="small" id="memSearchMsg" hidden style="margin:8px 0 0"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card" id="memCard" hidden>
|
<div class="card" id="memCard" hidden>
|
||||||
@@ -420,6 +421,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/assets/common.js?v=20260913a"></script>
|
<script src="/assets/common.js?v=20260913a"></script>
|
||||||
<script src="/assets/admin.js?v=20260913a"></script>
|
<script src="/assets/admin.js?v=20260913b"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+22
-4
@@ -318,7 +318,7 @@
|
|||||||
}
|
}
|
||||||
function kv(rows) { return '<table class="adm-table kv">' + rows.map(r => '<tr><th style="width:170px">' + r[0] + '</th><td>' + r[1] + '</td></tr>').join('') + '</table>'; }
|
function kv(rows) { return '<table class="adm-table kv">' + rows.map(r => '<tr><th style="width:170px">' + r[0] + '</th><td>' + r[1] + '</td></tr>').join('') + '</table>'; }
|
||||||
function renderMember(d) {
|
function renderMember(d) {
|
||||||
mcCur = d; const a = d.account;
|
mcCur = d; const a = d.account; $('memHits').hidden = true;
|
||||||
$('memCard').hidden = false; document.querySelectorAll('#pane-members > .card').forEach(c => { if (c.id !== 'memCard' && c.id !== 'memSearchCard') c.hidden = true; });
|
$('memCard').hidden = false; document.querySelectorAll('#pane-members > .card').forEach(c => { if (c.id !== 'memCard' && c.id !== 'memSearchCard') c.hidden = true; });
|
||||||
$('mcName').textContent = (a.username ? '@' + a.username : a.email) + (a.memberId ? ' · member #' + a.memberId : ' · free member');
|
$('mcName').textContent = (a.username ? '@' + a.username : a.email) + (a.memberId ? ' · member #' + a.memberId : ' · free member');
|
||||||
$('mcSub').textContent = 'joined ' + when(a.created) + ' · last seen ' + ago(a.lastSeen);
|
$('mcSub').textContent = 'joined ' + when(a.created) + ' · last seen ' + ago(a.lastSeen);
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
['Payouts received', t.payoutsIn + (t.payoutsIn ? ' · ' + polOf(t.receivedWei) + ' POL' : '')],
|
['Payouts received', t.payoutsIn + (t.payoutsIn ? ' · ' + polOf(t.receivedWei) + ' POL' : '')],
|
||||||
['Credits', cr ? cr.available.toLocaleString() + ' available · ' + cr.inCampaigns.toLocaleString() + ' in campaigns · ' + cr.total.toLocaleString() + ' total' : '<span class="muted">-</span>'],
|
['Credits', cr ? cr.available.toLocaleString() + ' available · ' + cr.inCampaigns.toLocaleString() + ' in campaigns · ' + cr.total.toLocaleString() + ' total' : '<span class="muted">-</span>'],
|
||||||
['Earned pool', d.earnedSplit ? d.earnedSplit.total.toLocaleString() + ' (' + (d.earnedSplit.grade || 0).toLocaleString() + ' purchased-grade)' : '-'],
|
['Earned pool', d.earnedSplit ? d.earnedSplit.total.toLocaleString() + ' (' + (d.earnedSplit.grade || 0).toLocaleString() + ' purchased-grade)' : '-'],
|
||||||
['Legacy', d.legacy ? esc(d.legacy.brand) + ' ' + d.legacy.seg + (d.legacy.grant ? ' · ' + d.legacy.grant.credits + ' credits granted ' + when(d.legacy.grant.at) : ' · not granted') : '<span class="muted">not on the legacy list</span>'],
|
['Old-site account', d.legacy ? 'had a ' + (d.legacy.brand === 'both' ? 'Faucet Wave and Tier One Ads' : d.legacy.brand === 'tier1ads' ? 'Tier One Ads' : 'Faucet Wave') + ' account (' + d.legacy.seg + ') · welcome-back credits ' + (d.legacy.grant ? d.legacy.grant.credits + ' issued ' + when(d.legacy.grant.at) : 'not issued (joined outside the legacy bridge)') : '<span class="muted">none on record</span>'],
|
||||||
['Promo codes', d.promos.length ? d.promos.map(p => esc(p.code) + ' (' + p.credits + ', ' + when(p.ts) + ')').join('<br>') : '<span class="muted">none</span>'],
|
['Promo codes', d.promos.length ? d.promos.map(p => esc(p.code) + ' (' + p.credits + ', ' + when(p.ts) + ')').join('<br>') : '<span class="muted">none</span>'],
|
||||||
['Drip', d.drip ? (d.drip.stopped ? 'stopped' : 'step ' + d.drip.step + ', next ' + when(d.drip.next_at)) + (d.drip.angle ? ' · ' + esc(d.drip.angle) : '') : '<span class="muted">-</span>'],
|
['Drip', d.drip ? (d.drip.stopped ? 'stopped' : 'step ' + d.drip.step + ', next ' + when(d.drip.next_at)) + (d.drip.angle ? ' · ' + esc(d.drip.angle) : '') : '<span class="muted">-</span>'],
|
||||||
['Holding tank', d.tank ? (d.tank.waiting ? '<b>waiting for a sponsor</b>' : 'not in tank') + (d.tank.adoptedBy.length ? ' · adopted by ' + d.tank.adoptedBy.map(x => memLink(x.email, x.name)).join(', ') : '') + (d.tank.adopted.length ? ' · adopted ' + d.tank.adopted.map(x => memLink(x.email, x.name)).join(', ') : '') : '-'],
|
['Holding tank', d.tank ? (d.tank.waiting ? '<b>waiting for a sponsor</b>' : 'not in tank') + (d.tank.adoptedBy.length ? ' · adopted by ' + d.tank.adoptedBy.map(x => memLink(x.email, x.name)).join(', ') : '') + (d.tank.adopted.length ? ' · adopted ' + d.tank.adopted.map(x => memLink(x.email, x.name)).join(', ') : '') : '-'],
|
||||||
@@ -366,8 +366,26 @@
|
|||||||
if (location.hash !== '#members') history.replaceState(null, '', '#members');
|
if (location.hash !== '#members') history.replaceState(null, '', '#members');
|
||||||
}
|
}
|
||||||
function closeMember() { $('memCard').hidden = true; document.querySelectorAll('#pane-members > .card').forEach(c => { c.hidden = false; }); }
|
function closeMember() { $('memCard').hidden = true; document.querySelectorAll('#pane-members > .card').forEach(c => { c.hidden = false; }); }
|
||||||
$('memOpen').addEventListener('click', () => { const q = $('memSearch').value.trim(); if (q) openMember(q); });
|
// live matches while typing: any part of the username, email, member #, share code or wallet
|
||||||
$('memSearch').addEventListener('keydown', e => { if (e.key === 'Enter') $('memOpen').click(); });
|
let memHitList = [];
|
||||||
|
function memMatches(q) {
|
||||||
|
q = q.toLowerCase();
|
||||||
|
return allMembers.filter(a => [a.username, a.email, a.memberId ? '#' + a.memberId : '', a.memberId, a.code, a.address, a.sponsorName].filter(Boolean).join(' ').toLowerCase().includes(q)).slice(0, 12);
|
||||||
|
}
|
||||||
|
async function memTypeahead() {
|
||||||
|
const q = $('memSearch').value.trim();
|
||||||
|
if (!allMembers.length) { try { const r = await api('/api/admin/members'); allMembers = r.members || []; } catch (e) {} }
|
||||||
|
if (q.length < 2) { $('memHits').hidden = true; memHitList = []; return; }
|
||||||
|
memHitList = memMatches(q);
|
||||||
|
$('memHits').innerHTML = memHitList.length ? memHitList.map(a => '<button type="button" data-mcopen="' + esc(a.email) + '" style="display:flex;gap:12px;width:100%;text-align:left;background:transparent;border:0;border-bottom:1px solid var(--line);padding:8px 12px;color:inherit;cursor:pointer;font:inherit"><b style="min-width:140px">' + (a.username ? '@' + esc(a.username) : '<span class="muted">no username</span>') + '</b><span>' + esc(a.email) + '</span><span class="muted">' + (a.memberId ? '#' + a.memberId : 'free') + (a.sponsorName ? ' · under ' + esc(a.sponsorName) : '') + '</span></button>').join('')
|
||||||
|
: '<p class="muted small" style="margin:0;padding:8px 12px">No member matches that.</p>';
|
||||||
|
$('memHits').hidden = false;
|
||||||
|
}
|
||||||
|
$('memSearch').addEventListener('input', memTypeahead);
|
||||||
|
$('memSearch').addEventListener('focus', memTypeahead);
|
||||||
|
$('memOpen').addEventListener('click', () => { const q = $('memSearch').value.trim(); if (!q) return; if (memHitList.length) openMember(memHitList[0].email); else openMember(q); });
|
||||||
|
$('memSearch').addEventListener('keydown', e => { if (e.key === 'Enter') $('memOpen').click(); if (e.key === 'Escape') $('memHits').hidden = true; });
|
||||||
|
document.addEventListener('click', e => { if (!e.target.closest('#memSearchCard')) $('memHits').hidden = true; });
|
||||||
$('mcBack').addEventListener('click', closeMember);
|
$('mcBack').addEventListener('click', closeMember);
|
||||||
document.addEventListener('click', e => { const l = e.target.closest('[data-mcopen]'); if (l) { e.preventDefault(); openMember(l.dataset.mcopen); } });
|
document.addEventListener('click', e => { const l = e.target.closest('[data-mcopen]'); if (l) { e.preventDefault(); openMember(l.dataset.mcopen); } });
|
||||||
document.querySelectorAll('[data-mcact]').forEach(b => b.addEventListener('click', busy(b, async () => {
|
document.querySelectorAll('[data-mcact]').forEach(b => b.addEventListener('click', busy(b, async () => {
|
||||||
|
|||||||
+17
-2
@@ -513,6 +513,7 @@
|
|||||||
IAP.adSlot('text', 'adStripTop');
|
IAP.adSlot('text', 'adStripTop');
|
||||||
if ($('adSlotPane-' + name)) IAP.adSlot('banner', 'adSlotPane-' + name);
|
if ($('adSlotPane-' + name)) IAP.adSlot('banner', 'adSlotPane-' + name);
|
||||||
if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active
|
if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active
|
||||||
|
else if (vidState.token) stopVideo();
|
||||||
if (name === 'profile') loadLineBanner();
|
if (name === 'profile') loadLineBanner();
|
||||||
if (name === 'line') { loadLineage(); loadUplineMessages(); loadCoach(); loadLinkStats(); loadProspects(); }
|
if (name === 'line') { loadLineage(); loadUplineMessages(); loadCoach(); loadLinkStats(); loadProspects(); }
|
||||||
if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits
|
if (name === 'campaigns') ['cTarget', 'cImage', 'cVideoUrl'].forEach(id => { if ($(id)) $(id).value = ''; }); // no residual URL between visits
|
||||||
@@ -989,13 +990,26 @@
|
|||||||
$('vidStartBtn').hidden = false;
|
$('vidStartBtn').hidden = false;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
// leaving the player (other sub-tab, other pane, page hidden) stops the clip: nothing plays or earns in the background (Marty, 2026-09-13)
|
||||||
|
function stopVideo() {
|
||||||
|
const p = $('vidPlayer'); if (!p) return;
|
||||||
|
const was = !!vidState.token && !vidState.done;
|
||||||
|
try { p.pause(); p.ontimeupdate = null; p.onseeking = null; p.removeAttribute('src'); p.load(); } catch (e) {}
|
||||||
|
vidState.token = null; vidState.done = false; vidState.credited = false; vidState.maxSeen = 0;
|
||||||
|
if ($('vidWrap')) $('vidWrap').hidden = true;
|
||||||
|
if ($('vidBox')) { $('vidBox').hidden = false; if (was) $('vidBox').innerHTML = '<span class="muted small">Video stopped when you left the tab. Tap Load a video to start a fresh one.</span>'; }
|
||||||
|
if ($('vidStartBtn')) { $('vidStartBtn').disabled = false; $('vidStartBtn').textContent = 'Load a video'; }
|
||||||
|
}
|
||||||
|
document.addEventListener('visibilitychange', () => { if (document.hidden && vidState.token && !vidState.done) stopVideo(); });
|
||||||
async function loadVideoAd() {
|
async function loadVideoAd() {
|
||||||
let r = null;
|
let r = null;
|
||||||
try { r = await (await fetch('/api/my/videos?orientation=landscape')).json(); } catch (e) {}
|
try { r = await (await fetch('/api/my/videos?orientation=landscape')).json(); } catch (e) {}
|
||||||
if (!r || !r.ad) {
|
if (!r || !r.ad) {
|
||||||
$('vidBox').innerHTML = '<span class="muted small">' + (r && r.status && r.status.left <= 0
|
$('vidBox').innerHTML = '<span class="muted small">' + (r && r.status && r.status.left <= 0
|
||||||
? 'That is today\'s video set. Come back tomorrow.'
|
? 'That is today\'s video set. Come back tomorrow.'
|
||||||
|
: r && r.allWatched ? 'You have watched every live video for today (each one pays once a day). New ones appear as members launch video campaigns.'
|
||||||
: 'No member videos are live right now. Check back when a campaign is running.') + '</span>';
|
: 'No member videos are live right now. Check back when a campaign is running.') + '</span>';
|
||||||
|
$('vidBox').hidden = false;
|
||||||
$('vidWrap').hidden = true;
|
$('vidWrap').hidden = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1029,10 +1043,10 @@
|
|||||||
try {
|
try {
|
||||||
const r = await (await fetch('/api/my/videowatch', { method: 'POST',
|
const r = await (await fetch('/api/my/videowatch', { method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: vidState.token }) })).json();
|
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: vidState.token }) })).json();
|
||||||
if (r.error) { IAP.status(r.error, 'bad'); $('vidHint').textContent = r.error; }
|
if (r.error) { IAP.status(r.error, 'bad'); $('vidHint').textContent = r.error + (/no longer open|stale/.test(r.error) ? ' Loading a fresh one.' : ''); if (/no longer open|stale/.test(r.error)) setTimeout(() => loadVideoAd(), 1200); }
|
||||||
else if (r.credited) { IAP.status('+' + r.credited + ' credits earned for watching.', 'ok'); $('vidHint').textContent = '+' + r.credited + ' credits earned. Load the next one.'; loadDashboard(); }
|
else if (r.credited) { IAP.status('+' + r.credited + ' credits earned for watching.', 'ok'); $('vidHint').textContent = '+' + r.credited + ' credits earned. Load the next one.'; loadDashboard(); }
|
||||||
else $('vidHint').textContent = 'That video just ran out of budget — load another.';
|
else $('vidHint').textContent = 'That video just ran out of budget — load another.';
|
||||||
$('vidProgress').textContent = 'today: ' + ((r.status && r.status.count) || 0) + ' / ' + ((r.status && r.status.cap) || 0) + ' videos watched';
|
if (r.status) $('vidProgress').textContent = 'today: ' + (r.status.count || 0) + ' / ' + r.status.cap + ' videos watched';
|
||||||
} catch (e) { IAP.status('Could not confirm that watch. Try the next one.', 'bad'); }
|
} catch (e) { IAP.status('Could not confirm that watch. Try the next one.', 'bad'); }
|
||||||
$('vidStartBtn').disabled = false;
|
$('vidStartBtn').disabled = false;
|
||||||
$('vidStartBtn').textContent = 'Next video';
|
$('vidStartBtn').textContent = 'Next video';
|
||||||
@@ -1305,6 +1319,7 @@
|
|||||||
let earnSub = 'watch';
|
let earnSub = 'watch';
|
||||||
function setEarnSub(which) {
|
function setEarnSub(which) {
|
||||||
earnSub = ['inbox', 'videos', 'visits'].includes(which) ? which : 'watch';
|
earnSub = ['inbox', 'videos', 'visits'].includes(which) ? which : 'watch';
|
||||||
|
if (earnSub !== 'videos' && vidState.token) stopVideo();
|
||||||
const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'), vs = $('earn-visits');
|
const w = $('earn-watch'), i = $('earn-inbox'), v = $('earn-videos'), vs = $('earn-visits');
|
||||||
if (w) w.hidden = earnSub !== 'watch';
|
if (w) w.hidden = earnSub !== 'watch';
|
||||||
if (i) i.hidden = earnSub !== 'inbox';
|
if (i) i.hidden = earnSub !== 'inbox';
|
||||||
|
|||||||
+1
-1
@@ -915,7 +915,7 @@
|
|||||||
<script src="/assets/common.js?v=20260913a"></script>
|
<script src="/assets/common.js?v=20260913a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260913a"></script>
|
<script src="/assets/my.js?v=20260913b"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+2
-1
@@ -69,7 +69,8 @@ for (let i = 0; i < 5; i++) {
|
|||||||
}
|
}
|
||||||
await page.waitForTimeout(800);
|
await page.waitForTimeout(800);
|
||||||
log('after set:', await page.textContent('#earnProgress'), '| claim visible:', !!(await page.$('#earnClaimBtn:not([hidden])')));
|
log('after set:', await page.textContent('#earnProgress'), '| claim visible:', !!(await page.$('#earnClaimBtn:not([hidden])')));
|
||||||
await page.click('#earnStartBtn'); await page.waitForTimeout(900); log('view-after-complete says:', (await page.textContent('#earnAdBox')).trim());
|
if (await page.$('#earnStartBtn:not([hidden])')) { await page.click('#earnStartBtn'); await page.waitForTimeout(900); log('view-after-complete says:', (await page.textContent('#earnAdBox')).trim()); }
|
||||||
|
else log('view button hidden after the set (done screen with claim), as designed since 2026-09-12');
|
||||||
if (await page.$('#earnClaimBtn:not([hidden])')) { await page.click('#earnClaimBtn'); await page.waitForTimeout(1000); log('claimed; balance:', await page.textContent('#earnBalance')); }
|
if (await page.$('#earnClaimBtn:not([hidden])')) { await page.click('#earnClaimBtn'); await page.waitForTimeout(1000); log('claimed; balance:', await page.textContent('#earnBalance')); }
|
||||||
else if (credited === 5) problems.push('watch: 5 views credited but claim button not shown');
|
else if (credited === 5) problems.push('watch: 5 views credited but claim button not shown');
|
||||||
|
|
||||||
|
|||||||
@@ -72,10 +72,40 @@ fs.mkdirSync(DATA_DIR, { recursive: true });
|
|||||||
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads'); // solo-ad media lives on the volume
|
const UPLOADS_DIR = path.join(DATA_DIR, 'uploads'); // solo-ad media lives on the volume
|
||||||
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
fs.mkdirSync(UPLOADS_DIR, { recursive: true });
|
||||||
const uploadCounts = new Map(); // email:day -> uploads today
|
const uploadCounts = new Map(); // email:day -> uploads today
|
||||||
const gauntletTokens = new Map(); // email -> welcome-tour token (server-clock dwell floor)
|
// open earn tokens (ad view / video / visit / tour) live in memory but mirror to the volume so a
|
||||||
const videoTokens = new Map(); // email -> watch-to-earn video token (server-clock watch floor)
|
// redeploy mid-watch does not lose them (2026-09-13: a member's video watch died in a restart)
|
||||||
|
const OPEN_TOKENS_FILE = () => path.join(DATA_DIR, 'open-tokens.json');
|
||||||
|
const persistedMaps = {};
|
||||||
|
let tokenSaveTimer = null;
|
||||||
|
function saveOpenTokens() {
|
||||||
|
tokenSaveTimer = null;
|
||||||
|
try {
|
||||||
|
const out = {};
|
||||||
|
for (const [name, m] of Object.entries(persistedMaps)) out[name] = Object.fromEntries(m);
|
||||||
|
fs.writeFileSync(OPEN_TOKENS_FILE(), JSON.stringify(out));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
function persistedMap(name) {
|
||||||
|
const m = new Map();
|
||||||
|
const touch = () => { if (!tokenSaveTimer) tokenSaveTimer = setTimeout(saveOpenTokens, 500); };
|
||||||
|
const set = m.set.bind(m), del = m.delete.bind(m);
|
||||||
|
m.set = (k, v) => { set(k, v); touch(); return m; };
|
||||||
|
m.delete = k => { const r = del(k); if (r) touch(); return r; };
|
||||||
|
persistedMaps[name] = m;
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
function loadOpenTokens() {
|
||||||
|
let saved = null; try { saved = JSON.parse(fs.readFileSync(OPEN_TOKENS_FILE(), 'utf8')); } catch (e) { return; }
|
||||||
|
const cutoff = Date.now() - 2 * 3600 * 1000; let n = 0;
|
||||||
|
for (const [name, m] of Object.entries(persistedMaps)) {
|
||||||
|
for (const [k, v] of Object.entries(saved[name] || {})) if (v && Number(v.ts || 0) > cutoff) { Map.prototype.set.call(m, k, v); n++; }
|
||||||
|
}
|
||||||
|
if (n) console.log('open earn tokens restored:', n);
|
||||||
|
}
|
||||||
|
const gauntletTokens = persistedMap('gauntlet'); // email -> welcome-tour token (server-clock dwell floor)
|
||||||
|
const videoTokens = persistedMap('video'); // email -> watch-to-earn video token (server-clock watch floor)
|
||||||
const faucetHits = new Map(); // address -> last faucet ts (rehearsal test-POL faucet rate limit)
|
const faucetHits = new Map(); // address -> last faucet ts (rehearsal test-POL faucet rate limit)
|
||||||
const visitTokens = new Map(); // email -> verified-visit token (dwell + captcha floor)
|
const visitTokens = persistedMap('visit'); // email -> verified-visit token (dwell + captcha floor)
|
||||||
// walk the referral chain upward via sponsorRef (code/username/member id)
|
// walk the referral chain upward via sponsorRef (code/username/member id)
|
||||||
async function uplineSlides(email, depth = 3) {
|
async function uplineSlides(email, depth = 3) {
|
||||||
const out = [];
|
const out = [];
|
||||||
@@ -189,7 +219,7 @@ function codeTrip(req, ip) {
|
|||||||
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up guard tripped', text).catch(() => {});
|
else if (ADMIN_EMAIL && mailer.hasKey()) mailer.send(ADMIN_EMAIL, 'InstantAdPay: sign-up guard tripped', text).catch(() => {});
|
||||||
}
|
}
|
||||||
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
|
// earn-view tokens: emailLower -> {token, ts} (one live token per member)
|
||||||
const earnTokens = new Map();
|
const earnTokens = persistedMap('earn');
|
||||||
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
|
// human-check pairs for the view verifier: [emoji shown, word named in the prompt]
|
||||||
const CAPTCHA = [['🚀', 'rocket'], ['⚡', 'lightning bolt'], ['🔑', 'key'], ['🎯', 'target'],
|
const CAPTCHA = [['🚀', 'rocket'], ['⚡', 'lightning bolt'], ['🔑', 'key'], ['🎯', 'target'],
|
||||||
['🌊', 'wave'], ['🔥', 'flame'], ['💎', 'diamond'], ['🧲', 'magnet'], ['🔔', 'bell'], ['🌙', 'moon']];
|
['🌊', 'wave'], ['🔥', 'flame'], ['💎', 'diamond'], ['🧲', 'magnet'], ['🔔', 'bell'], ['🌙', 'moon']];
|
||||||
@@ -332,6 +362,7 @@ async function boot() {
|
|||||||
promos.init({ dataDir: DATA_DIR });
|
promos.init({ dataDir: DATA_DIR });
|
||||||
blog.init({ dataDir: DATA_DIR });
|
blog.init({ dataDir: DATA_DIR });
|
||||||
adminMember.init({ accounts, ads, chain, tank, legacy, promos, messages, dataDir: DATA_DIR });
|
adminMember.init({ accounts, ads, chain, tank, legacy, promos, messages, dataDir: DATA_DIR });
|
||||||
|
loadOpenTokens();
|
||||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||||
@@ -1775,7 +1806,10 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (status.left <= 0) return json(res, 200, { ad: null, status });
|
if (status.left <= 0) return json(res, 200, { ad: null, status });
|
||||||
const orientation = String(u.searchParams.get('orientation') || ''); // 'portrait' = Shorts reel, 'landscape' = Watch videos tab
|
const orientation = String(u.searchParams.get('orientation') || ''); // 'portrait' = Shorts reel, 'landscape' = Watch videos tab
|
||||||
const ad = await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation }, viewerGeo(req))); // never your own video
|
const ad = await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation }, viewerGeo(req))); // never your own video
|
||||||
if (!ad) return json(res, 200, { ad: null, status });
|
if (!ad) { // distinguish "you have watched every live video today" from "nothing is live" (Marty, 2026-09-13)
|
||||||
|
let allWatched = false; try { allWatched = !!(await ads.serveVideo(Object.assign({ excludeEmail: s.email, orientation, ignoreSeen: true }, viewerGeo(req)))); } catch (e) {}
|
||||||
|
return json(res, 200, { ad: null, status, allWatched });
|
||||||
|
}
|
||||||
const token = crypto.randomBytes(16).toString('hex');
|
const token = crypto.randomBytes(16).toString('hex');
|
||||||
videoTokens.set(s.email, { token, ts: Date.now(), id: ad.id, secs: ad.watchSecs });
|
videoTokens.set(s.email, { token, ts: Date.now(), id: ad.id, secs: ad.watchSecs });
|
||||||
return json(res, 200, { ad, token, status });
|
return json(res, 200, { ad, token, status });
|
||||||
|
|||||||
Reference in New Issue
Block a user