View-fraud protection + real ad placement surfaces
Earning views now run on single-use server-issued tokens: the view only counts when the full dwell elapses on the server clock; instant, forged, replayed, and stale posts are all rejected, and the client countdown pauses whenever the tab loses visibility or focus. New placements: login ads on the sign-in screen (the per-day format's real home), a banner slot in the back-office Overview, a text slot in the member sidebar, and a member-ads banner on the homepage. Assets v=20260905g. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,7 @@
|
||||
recalc();
|
||||
}
|
||||
|
||||
IAP.adSlot('banner', 'adSlotHome');
|
||||
loadLadder();
|
||||
loadStats();
|
||||
loadTicker();
|
||||
|
||||
+24
-9
@@ -286,33 +286,42 @@
|
||||
} catch (e) { return null; }
|
||||
}
|
||||
async function earnShowAd() {
|
||||
const st = await earnRefresh();
|
||||
if (!st || st.views >= st.target) return;
|
||||
const type = earnState.types[earnState.i++ % earnState.types.length];
|
||||
let ad = null;
|
||||
try { ad = (await (await fetch('/api/ads/slot?type=' + type)).json()).ad; } catch (e) {}
|
||||
let r = null;
|
||||
try { r = await (await fetch('/api/my/earnview?type=' + type)).json(); } catch (e) {}
|
||||
const box = $('earnAdBox');
|
||||
if (!ad) {
|
||||
box.innerHTML = '<span class="muted small">No member ads are live in rotation right now. '
|
||||
+ 'Views resume the moment a campaign is running.</span>';
|
||||
if (!r || !r.ad) {
|
||||
await earnRefresh();
|
||||
box.innerHTML = '<span class="muted small">' + (r && r.status && r.status.views >= r.status.target
|
||||
? 'Set complete for today.'
|
||||
: 'No member ads are live in rotation right now. Views resume the moment a campaign is running.') + '</span>';
|
||||
return;
|
||||
}
|
||||
const ad = r.ad;
|
||||
box.innerHTML = ad.imageUrl
|
||||
? '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow"><img src="' + ad.imageUrl + '" alt="member ad" style="max-height:200px"></a>'
|
||||
: '<a href="' + ad.targetUrl + '" target="_blank" rel="noopener nofollow" style="font-size:17px"><b>' + ad.title + '</b>' + (ad.body ? '<br><span class="muted">' + ad.body + '</span>' : '') + '</a>';
|
||||
const btn = $('earnNextBtn');
|
||||
btn.hidden = false;
|
||||
btn.disabled = true;
|
||||
let left = st.dwell;
|
||||
// the countdown only runs while this tab is visible AND focused; the
|
||||
// server separately enforces the full dwell on its own clock
|
||||
let left = r.status.dwell;
|
||||
btn.textContent = 'Next ad (' + left + 's)';
|
||||
clearInterval(earnState.timer);
|
||||
earnState.timer = setInterval(async () => {
|
||||
if (document.visibilityState !== 'visible' || !document.hasFocus()) {
|
||||
btn.textContent = 'Paused: stay on this tab (' + left + 's)';
|
||||
return;
|
||||
}
|
||||
left -= 1;
|
||||
if (left > 0) { btn.textContent = 'Next ad (' + left + 's)'; return; }
|
||||
clearInterval(earnState.timer);
|
||||
btn.textContent = 'Next ad';
|
||||
const v = await (await fetch('/api/my/adview', { method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: r.token }) })).json();
|
||||
if (v.error) { IAP.status(v.error, 'bad'); btn.disabled = false; return; }
|
||||
btn.disabled = false;
|
||||
await fetch('/api/my/adview', { method: 'POST' }); // dwell served: count it
|
||||
await earnRefresh();
|
||||
}, 1000);
|
||||
}
|
||||
@@ -406,5 +415,11 @@
|
||||
await render();
|
||||
});
|
||||
|
||||
// ad surfaces: login ads greet the sign-in screen; members see live
|
||||
// banner + text placements inside the back office (they ARE the audience)
|
||||
IAP.adSlot('login', 'adSlotLogin');
|
||||
IAP.adSlot('banner', 'adSlotOverview');
|
||||
IAP.adSlot('text', 'adSlotSide');
|
||||
|
||||
render();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user