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:
martbost
2026-09-13 06:46:08 -05:00
parent 0c7f957db4
commit 4efba20d4e
7 changed files with 85 additions and 16 deletions
+4 -3
View File
@@ -234,8 +234,9 @@
<div class="pane" id="pane-members" hidden>
<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 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 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" 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>
</div>
<div class="card" id="memCard" hidden>
@@ -420,6 +421,6 @@
</div>
<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>
</html>
+22 -4
View File
@@ -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 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; });
$('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);
@@ -342,7 +342,7 @@
['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>'],
['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>'],
['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(', ') : '') : '-'],
@@ -366,8 +366,26 @@
if (location.hash !== '#members') history.replaceState(null, '', '#members');
}
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); });
$('memSearch').addEventListener('keydown', e => { if (e.key === 'Enter') $('memOpen').click(); });
// live matches while typing: any part of the username, email, member #, share code or wallet
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);
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 () => {
+17 -2
View File
@@ -513,6 +513,7 @@
IAP.adSlot('text', 'adStripTop');
if ($('adSlotPane-' + name)) IAP.adSlot('banner', 'adSlotPane-' + name);
if (name === 'earn') setEarnSub(earnSub); // refresh whichever sub-tab is active
else if (vidState.token) stopVideo();
if (name === 'profile') loadLineBanner();
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
@@ -989,13 +990,26 @@
$('vidStartBtn').hidden = false;
} 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() {
let r = null;
try { r = await (await fetch('/api/my/videos?orientation=landscape')).json(); } catch (e) {}
if (!r || !r.ad) {
$('vidBox').innerHTML = '<span class="muted small">' + (r && r.status && r.status.left <= 0
? '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>';
$('vidBox').hidden = false;
$('vidWrap').hidden = true;
return;
}
@@ -1029,10 +1043,10 @@
try {
const r = await (await fetch('/api/my/videowatch', { method: 'POST',
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 $('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'); }
$('vidStartBtn').disabled = false;
$('vidStartBtn').textContent = 'Next video';
@@ -1305,6 +1319,7 @@
let earnSub = 'watch';
function setEarnSub(which) {
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');
if (w) w.hidden = earnSub !== 'watch';
if (i) i.hidden = earnSub !== 'inbox';
+1 -1
View File
@@ -915,7 +915,7 @@
<script src="/assets/common.js?v=20260913a"></script>
<script src="/assets/wallet.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>
</body>
</html>