Five Dollar Friday bridge page, with the member's link on it
Marty's ask: use the Friday promo as a tool where the sponsor gets the
credit, and as a bridge-page entry point. The sponsor part already worked
(any public page + ?ref sets the sponsor cookie, 2026-09-21); what was
missing was a page that is ABOUT the offer, so a click on a "20% bonus
credits today" image did not land on the general pitch and leak.
/friday: the banner as the hero, the live state from /api/friday (live now
with today's pack count and bonus credits issued, or the next date with a
countdown to midnight Central found by scanning minutes so DST cannot put it
an hour out), three steps, the email-first join with a new `friday` angle so
it shows as its own row in Admin > Traffic, and the credit definition. No
attribution logic added: last touch, walk-up, as everywhere.
Promo tools: a {{FRIDAY}} token that turns the member's invite link into
/friday?ref=<username>; posts for X, Facebook and Telegram and a text that
carry it; the three new banners (loft 1200x630, dark 1200x630, square
1080x1080) as a group in the kit with a copy-my-Friday-link button on it.
Chatbot answer points at the page and drops the stale "first one is
September 25" line. /friday in the traffic list, the sitemap and the QA
public walk. Cache tags bumped on my.js, promo.js and site.css.
QA member walk: 0 bugs.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -25,7 +25,7 @@ const CANNED = [
|
|||||||
{ re: /(viral link|any page.{0,30}(link|referr)|blog.{0,40}(referral|my link|credit)|(referral|invite|my) link.{0,30}(blog|article|any page)|\?ref=|ref=)/i,
|
{ re: /(viral link|any page.{0,30}(link|referr)|blog.{0,40}(referral|my link|credit)|(referral|invite|my) link.{0,30}(blog|article|any page)|\?ref=|ref=)/i,
|
||||||
a: 'Any page on instantadpay.com becomes your referral link when you add ?ref= and your username to the end, for example instantadpay.com/blog/what-is-a-feeder-program?ref=yourname. Whoever opens it is tagged to you for 30 days, exactly like your invite link, so if they join from any page on the site the credit is yours. Promo tools > Viral links builds these links for you, and every blog article shows your link when you are signed in.' },
|
a: 'Any page on instantadpay.com becomes your referral link when you add ?ref= and your username to the end, for example instantadpay.com/blog/what-is-a-feeder-program?ref=yourname. Whoever opens it is tagged to you for 30 days, exactly like your invite link, so if they join from any page on the site the credit is yours. Promo tools > Viral links builds these links for you, and every blog article shows your link when you are signed in.' },
|
||||||
{ re: /(five dollar friday|5 dollar friday|\$5 friday|friday (bonus|promo|deal|special)|what happens on friday)/i,
|
{ re: /(five dollar friday|5 dollar friday|\$5 friday|friday (bonus|promo|deal|special)|what happens on friday)/i,
|
||||||
a: 'Five Dollar Friday: every Friday (Central time), any ad package from $5 up earns +20% bonus credits, added the moment the purchase settles and labelled in your credit log. The first one is Friday, September 25. Every pack pays its sponsor line in the same on-chain transaction, so when the whole network buys on the same day every line gets paid at once. Five Fridays in a row earn the Five Fridays badge. Buy under My account > Buy packages.' },
|
a: 'Five Dollar Friday: every Friday (Central time), any ad package from $5 up earns +20% bonus credits, added the moment the purchase settles and labelled in your credit log. The page instantadpay.com/friday shows whether it is live right now and the next date; add ?ref=yourusername to that address and anyone who joins from it is your referral. Every pack pays its sponsor line in the same on-chain transaction, so when the whole network buys on the same day every line gets paid at once. Five Fridays in a row earn the Five Fridays badge. Buy under My account > Buy packages.' },
|
||||||
// 2026-09-18: sessions were shortened to 4h on purpose. Members WILL ask why they keep being
|
// 2026-09-18: sessions were shortened to 4h on purpose. Members WILL ask why they keep being
|
||||||
// signed out, and the honest answer reads far better than letting it look like a fault.
|
// signed out, and the honest answer reads far better than letting it look like a fault.
|
||||||
{ re: /(logged out|log(ged)? me out|signed (me |us )?out|keep.{0,20}(logging|signing) in|session (expired|timed? out)|why.{0,25}(log ?in|sign ?in) again|have to log ?in again|stay logged in)/i,
|
{ re: /(logged out|log(ged)? me out|signed (me |us )?out|keep.{0,20}(logging|signing) in|session (expired|timed? out)|why.{0,25}(log ?in|sign ?in) again|have to log ?in again|stay logged in)/i,
|
||||||
|
|||||||
@@ -2088,6 +2088,10 @@
|
|||||||
bwrap.dataset.filled = '1';
|
bwrap.dataset.filled = '1';
|
||||||
// one collapsed accordion per size / use, so the kit stays scannable as it grows
|
// one collapsed accordion per size / use, so the kit stays scannable as it grows
|
||||||
const GROUPS = [
|
const GROUPS = [
|
||||||
|
{ title: 'Five Dollar Friday · share with your /friday link', friday: true, items: [
|
||||||
|
{ file: 'iap-five-dollar-friday-v2-1200x630.jpg', size: '1200×630 · Five Dollar Friday · loft, Central-time window' },
|
||||||
|
{ file: 'iap-five-dollar-friday-dark-1200x630.jpg', size: '1200×630 · Five Dollar Friday · dark studio' },
|
||||||
|
{ file: 'iap-five-dollar-friday-v2-1080x1080.jpg', size: '1080×1080 · Five Dollar Friday · square for Instagram and feeds' } ] },
|
||||||
{ title: '1200×630 · social posts, link previews, Daily News covers', items: [
|
{ title: '1200×630 · social posts, link previews, Daily News covers', items: [
|
||||||
{ file: 'iap-hero-1200x630.png', size: '1200×630 · hero · advertise and earn' },
|
{ file: 'iap-hero-1200x630.png', size: '1200×630 · hero · advertise and earn' },
|
||||||
{ file: 'iap-advertise-earn-1200x630.jpg', size: '1200×630 · advertise and earn instantly, locked in code' },
|
{ file: 'iap-advertise-earn-1200x630.jpg', size: '1200×630 · advertise and earn instantly, locked in code' },
|
||||||
@@ -2135,6 +2139,19 @@
|
|||||||
det.className = 'pb-acc';
|
det.className = 'pb-acc';
|
||||||
det.innerHTML = '<summary><span>' + esc(g.title) + '</span><span class="pb-count">' + g.items.length + (g.items.length === 1 ? ' banner' : ' banners') + '</span></summary><div class="pb-grid"></div>';
|
det.innerHTML = '<summary><span>' + esc(g.title) + '</span><span class="pb-count">' + g.items.length + (g.items.length === 1 ? ' banner' : ' banners') + '</span></summary><div class="pb-grid"></div>';
|
||||||
const grid = det.querySelector('.pb-grid');
|
const grid = det.querySelector('.pb-grid');
|
||||||
|
if (g.friday) {
|
||||||
|
// the bridge page with this member's referral: any public page + ?ref sets the sponsor cookie
|
||||||
|
const inv = (document.getElementById('inviteLine') || {}).textContent || '';
|
||||||
|
const fl = inv.replace(/\/join\/([^/?#]+).*$/, '/friday?ref=$1');
|
||||||
|
const note = document.createElement('div');
|
||||||
|
note.className = 'pb-note';
|
||||||
|
note.innerHTML = '<span>Post any of these with your Five Dollar Friday link: <b class="mono"></b></span>';
|
||||||
|
note.querySelector('b').textContent = fl;
|
||||||
|
const cp = document.createElement('button'); cp.className = 'btn small'; cp.textContent = 'Copy my Friday link';
|
||||||
|
cp.addEventListener('click', async () => { try { await navigator.clipboard.writeText(fl); IAP.status('Friday link copied.', 'ok'); } catch (e) { IAP.status('Copy failed.', 'bad'); } });
|
||||||
|
note.appendChild(cp);
|
||||||
|
grid.before(note);
|
||||||
|
}
|
||||||
for (const b of g.items) {
|
for (const b of g.items) {
|
||||||
const url = location.origin + '/banners/' + b.file;
|
const url = location.origin + '/banners/' + b.file;
|
||||||
const d = document.createElement('div');
|
const d = document.createElement('div');
|
||||||
|
|||||||
@@ -8,9 +8,15 @@ window.IAPPromo = (function () {
|
|||||||
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
||||||
const status = (m, c) => (window.IAP && IAP.status) ? IAP.status(m, c) : console.log(m);
|
const status = (m, c) => (window.IAP && IAP.status) ? IAP.status(m, c) : console.log(m);
|
||||||
const angleLink = (link, angle) => angle ? link + '?v=' + angle : link;
|
const angleLink = (link, angle) => angle ? link + '?v=' + angle : link;
|
||||||
|
// The Five Dollar Friday bridge page with the member's referral: /friday?ref=<username>. Any
|
||||||
|
// public page plus ?ref sets the same sponsor cookie as the invite link (viral links, 2026-09-21).
|
||||||
|
const fridayLink = link => link.replace(/\/join\/([^/?#]+).*$/, '/friday?ref=$1');
|
||||||
|
|
||||||
// ── social posts ──
|
// ── social posts ──
|
||||||
const POSTS = [
|
const POSTS = [
|
||||||
|
{ net: 'X', label: 'X · Five Dollar Friday', text: 'Five Dollar Friday on InstantAdPay: any ad package from $5 up earns 20% bonus credits, every Friday, Central time. The pack pays the sponsor line in the same transaction, on a public ledger. Free to join: {{FRIDAY}}' },
|
||||||
|
{ net: 'Facebook', label: 'Facebook · Five Dollar Friday', text: 'Every Friday I point people at this. Any ad package from $5 up gets 20% extra credits on InstantAdPay, added the second the purchase settles, no code, nothing to claim. And every pack pays the sponsor line in the same blockchain transaction, so there is no pending anything. It runs midnight to midnight Central. Free to join by email: {{FRIDAY}}' },
|
||||||
|
{ net: 'Telegram', label: 'Telegram or WhatsApp group · Five Dollar Friday', text: 'Quick one for the group: Five Dollar Friday on InstantAdPay. Any ad package from $5 up earns 20% bonus credits today (Central time, until midnight). The page shows whether it is live right now: {{FRIDAY}}' },
|
||||||
{ net: 'X', label: 'X · Five Dollar Friday', text: 'Five Dollar Friday on InstantAdPay: every Friday, any ad package from $5 earns +20% bonus credits, and every pack pays its sponsor line in the same on-chain transaction. The whole network buys on the same day. Join free and line yours up: {{LINK}}' },
|
{ net: 'X', label: 'X · Five Dollar Friday', text: 'Five Dollar Friday on InstantAdPay: every Friday, any ad package from $5 earns +20% bonus credits, and every pack pays its sponsor line in the same on-chain transaction. The whole network buys on the same day. Join free and line yours up: {{LINK}}' },
|
||||||
{ net: 'Telegram', label: 'Telegram or WhatsApp · Five Dollar Friday', text: 'Reminder for the group: tomorrow is Five Dollar Friday on InstantAdPay. Any pack from $5 up earns +20% credits, and your pack pays your sponsor line the second it settles. Everyone pushes on the same day, and every line gets paid at once. Free to join: {{LINK}}' },
|
{ net: 'Telegram', label: 'Telegram or WhatsApp · Five Dollar Friday', text: 'Reminder for the group: tomorrow is Five Dollar Friday on InstantAdPay. Any pack from $5 up earns +20% credits, and your pack pays your sponsor line the second it settles. Everyone pushes on the same day, and every line gets paid at once. Free to join: {{LINK}}' },
|
||||||
{ net: 'X', label: 'X · instant payout', text: 'No withdraw button. A smart contract on Polygon splits every ad package the second it sells: 50% to the sponsor, 20% to level 2, 10% to level 3. Lands in your own wallet. Watch the ledger move: {{LINK:instant}}' },
|
{ net: 'X', label: 'X · instant payout', text: 'No withdraw button. A smart contract on Polygon splits every ad package the second it sells: 50% to the sponsor, 20% to level 2, 10% to level 3. Lands in your own wallet. Watch the ledger move: {{LINK:instant}}' },
|
||||||
@@ -25,6 +31,7 @@ window.IAPPromo = (function () {
|
|||||||
|
|
||||||
// ── text a friend (SMS-sized) ──
|
// ── text a friend (SMS-sized) ──
|
||||||
const TEXTS = [
|
const TEXTS = [
|
||||||
|
{ angle: 'friday', title: '"Five Dollar Friday"', text: 'Heads up: Fridays on InstantAdPay any ad package from $5 up gets 20% bonus credits, Central time. Page tells you if it is live right now: {{FRIDAY}}' },
|
||||||
{ angle: '', title: '"Thought of you"', text: 'Hey, found an ad platform where the commission lands in your wallet the second someone buys. No pending, no withdraw button. Free to look: {{LINK}}' },
|
{ angle: '', title: '"Thought of you"', text: 'Hey, found an ad platform where the commission lands in your wallet the second someone buys. No pending, no withdraw button. Free to look: {{LINK}}' },
|
||||||
{ angle: 'instant', title: '"Before the page reloads"', text: 'Random thought. What if your commission showed up before the thank-you page finished loading? That is literally how this works: {{LINK:instant}}' },
|
{ angle: 'instant', title: '"Before the page reloads"', text: 'Random thought. What if your commission showed up before the thank-you page finished loading? That is literally how this works: {{LINK:instant}}' },
|
||||||
{ angle: 'adspend', title: '"You buy ads anyway"', text: 'You already buy traffic. This one pays your line every time someone in it buys ads, same transaction, on Polygon. Two-minute look: {{LINK:adspend}}' },
|
{ angle: 'adspend', title: '"You buy ads anyway"', text: 'You already buy traffic. This one pays your line every time someone in it buys ads, same transaction, on Polygon. Two-minute look: {{LINK:adspend}}' },
|
||||||
@@ -55,7 +62,7 @@ window.IAPPromo = (function () {
|
|||||||
];
|
];
|
||||||
|
|
||||||
function fillLink(text, link) {
|
function fillLink(text, link) {
|
||||||
return text.replace(/\{\{LINK(?::([a-z]+))?\}\}/g, (m, a) => angleLink(link, a));
|
return text.replace(/\{\{FRIDAY\}\}/g, fridayLink(link)).replace(/\{\{LINK(?::([a-z]+))?\}\}/g, (m, a) => angleLink(link, a));
|
||||||
}
|
}
|
||||||
function copyBtn(text, label) {
|
function copyBtn(text, label) {
|
||||||
const b = document.createElement('button');
|
const b = document.createElement('button');
|
||||||
|
|||||||
@@ -317,6 +317,8 @@ textarea{resize:vertical;font:inherit}
|
|||||||
}
|
}
|
||||||
/* ── promo banners ── */
|
/* ── promo banners ── */
|
||||||
.promo-banners{display:flex;flex-direction:column;gap:10px;margin-top:12px}
|
.promo-banners{display:flex;flex-direction:column;gap:10px;margin-top:12px}
|
||||||
|
.pb-note{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px 14px;padding:10px 16px 12px;font-size:13.5px;color:var(--muted);border-bottom:1px solid var(--line)}
|
||||||
|
.pb-note b.mono{color:var(--ink);font-family:var(--mono);font-size:12.5px;word-break:break-all}
|
||||||
.pb-acc{margin:0}
|
.pb-acc{margin:0}
|
||||||
.pb-acc>summary{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:13px 16px;font-size:14.5px}
|
.pb-acc>summary{display:flex;justify-content:space-between;align-items:center;gap:10px;padding:13px 16px;font-size:14.5px}
|
||||||
.pb-acc>summary::before{content:none}
|
.pb-acc>summary::before{content:none}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 106 KiB |
@@ -0,0 +1,110 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Five Dollar Friday: 20% bonus credits on every $5+ ad package | InstantAdPay</title>
|
||||||
|
<meta name="description" content="Every Friday (US Central time), any InstantAdPay ad package from $5 up earns 20% bonus credits the moment it settles. Every pack pays its sponsor line in the same transaction. Join free by email.">
|
||||||
|
<meta name="theme-color" content="#043b2f">
|
||||||
|
<meta property="og:type" content="website"><meta property="og:site_name" content="InstantAdPay">
|
||||||
|
<meta property="og:title" content="Five Dollar Friday: 20% bonus credits on every $5+ ad package">
|
||||||
|
<meta property="og:description" content="Every Friday, US Central time, midnight to midnight. Any ad package from $5 up earns 20% bonus credits, added the moment the purchase settles. Join free by email.">
|
||||||
|
<meta property="og:image" content="https://instantadpay.com/banners/iap-five-dollar-friday-v2-1200x630.jpg">
|
||||||
|
<meta property="og:url" content="https://instantadpay.com/friday">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
|
<link rel="stylesheet" href="/assets/site.css?v=20260912b">
|
||||||
|
<style>
|
||||||
|
.fr{max-width:860px}
|
||||||
|
.fr .hero{padding:44px 0 10px}
|
||||||
|
.fr-banner{display:block;width:100%;max-width:860px;border-radius:16px;border:1px solid var(--line);margin:18px 0 14px}
|
||||||
|
.fr-state{display:flex;flex-wrap:wrap;align-items:center;gap:10px 18px;padding:14px 18px;border-radius:14px;border:1px solid var(--line);background:rgba(67,232,195,.06);margin:0 0 26px;font-size:15px}
|
||||||
|
.fr-state.live{border-color:rgba(255,178,56,.55);background:rgba(255,178,56,.09)}
|
||||||
|
.fr-tag{font-family:var(--mono);font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;padding:4px 9px;border-radius:999px;background:var(--mint);color:#04120e}
|
||||||
|
.fr-state.live .fr-tag{background:#ffb238}
|
||||||
|
.fr-state b{font-family:var(--disp)}
|
||||||
|
.fr-num{font-family:var(--mono);font-variant-numeric:tabular-nums}
|
||||||
|
.fr-steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin:8px 0 26px}
|
||||||
|
.fr-step{border:1px solid var(--line);border-radius:14px;padding:16px 18px;background:rgba(255,255,255,.02)}
|
||||||
|
.fr-step .n{font-family:var(--mono);font-size:11px;letter-spacing:.12em;color:var(--mint);text-transform:uppercase}
|
||||||
|
.fr-step h3{font-family:var(--disp);font-size:17px;margin:6px 0 6px}
|
||||||
|
.fr-step p{margin:0;font-size:14.5px;color:var(--muted)}
|
||||||
|
.fr-cta{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:6px 0 30px}
|
||||||
|
.fr-cta .btn.big{font-size:17px;padding:14px 22px}
|
||||||
|
.fr-rule{border-left:4px solid var(--mint);background:rgba(67,232,195,.07);padding:14px 18px;border-radius:0 12px 12px 0;margin:0 0 22px;font-size:14.5px}
|
||||||
|
.fr-share{border:1px dashed var(--line);border-radius:14px;padding:16px 18px;margin:0 0 30px;font-size:14.5px;color:var(--muted)}
|
||||||
|
.fr-share b{color:var(--ink)}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap fr">
|
||||||
|
<section class="hero">
|
||||||
|
<p class="eyebrow">Every Friday · US Central time · midnight to midnight</p>
|
||||||
|
<h1>Five Dollar Friday: <em>20% bonus credits</em> on every $5+ ad package.</h1>
|
||||||
|
<p class="lead">Buy any InstantAdPay ad package from $5 up on a Friday and 20% extra credits land the moment the purchase settles. No code, nothing to claim. And like every pack, it pays the sponsor line in the same transaction.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<img class="fr-banner" src="/banners/iap-five-dollar-friday-v2-1200x630.jpg" alt="Five Dollar Friday: 20% bonus credits on every $5+ ad package. Fridays 12:00 AM to 11:59 PM US Central time." width="1200" height="630">
|
||||||
|
|
||||||
|
<div class="fr-state" id="frState"><span class="fr-tag">Checking</span><span>Loading today’s status…</span></div>
|
||||||
|
|
||||||
|
<div class="fr-cta">
|
||||||
|
<a class="btn big" id="frJoin" href="/join?v=friday">Join free by email</a>
|
||||||
|
<a class="btn sec" href="/my#buy">Already a member? Get your pack</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fr-steps">
|
||||||
|
<div class="fr-step"><div class="n">Step 1</div><h3>Join free, by email.</h3><p>Email first, wallet later, inside your account. Nothing to install to look around.</p></div>
|
||||||
|
<div class="fr-step"><div class="n">Step 2</div><h3>Buy any pack from $5 on a Friday.</h3><p>Central time, midnight to midnight. The 20% bonus is added to your credits the moment the purchase settles, and labelled in your credit log.</p></div>
|
||||||
|
<div class="fr-step"><div class="n">Step 3</div><h3>Your pack pays the line above you. Theirs pays you.</h3><p>Every package is one Polygon transaction that splits to the sponsor line instantly. Public ledger, nothing held, nothing pending.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fr-rule"><b>What a credit is.</b> One credit is one cent of ad delivery across the network: banners, text ads, video, verified visits, solo ads and more. Credits are not money and cannot be withdrawn. They buy advertising for your own offers. The POL the contract pays to wallets is separate and comes only from packages bought in your line.</div>
|
||||||
|
|
||||||
|
<div class="fr-share" id="frShare"><b>Members: this page is your Friday promo tool.</b> Add <span class="fr-num">?ref=yourusername</span> to this address and anyone who joins from it is your referral, the same as your invite link. Banners, ready-made posts and your exact link are in <a href="/my#promo">Promo tools</a>.</div>
|
||||||
|
|
||||||
|
<p class="muted small" style="margin-top:10px">No income is guaranteed. Results depend on your effort. Cryptocurrency involves risk of loss. InstantAdPay sells advertising; it is not an investment product.</p>
|
||||||
|
<footer>
|
||||||
|
<div>InstantAdPay · <a href="/">Home</a> · <a href="/ledger">Live ledger</a> · <a href="/earning">How earning works</a></div>
|
||||||
|
<div class="small">Advertising services with a performance referral program. Not an investment product; no income guarantees.</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var el = document.getElementById('frState');
|
||||||
|
function n(x) { return Number(x || 0).toLocaleString('en-US'); }
|
||||||
|
// the minute (Central) when the next Friday starts, found by scanning rather than assumed,
|
||||||
|
// so daylight-saving changes cannot put the countdown an hour out
|
||||||
|
function startsAt(dayKey) {
|
||||||
|
var now = Date.now();
|
||||||
|
for (var m = 0; m < 8 * 1440; m++) {
|
||||||
|
var t = now + m * 60000;
|
||||||
|
var d = new Date(t).toLocaleDateString('en-CA', { timeZone: 'America/Chicago' });
|
||||||
|
if (d === dayKey) return t;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
function render(f) {
|
||||||
|
if (!f || !f.on) { el.innerHTML = '<span class="fr-tag">Paused</span><span>Five Dollar Friday is not running right now. Join free and you will hear when it is back.</span>'; return; }
|
||||||
|
if (f.live) {
|
||||||
|
var t = f.today || {};
|
||||||
|
el.className = 'fr-state live';
|
||||||
|
el.innerHTML = '<span class="fr-tag">Live now</span><span><b>It is Five Dollar Friday.</b> Bonus runs until midnight Central.</span>'
|
||||||
|
+ '<span><span class="fr-num">' + n(t.packs) + '</span> pack' + (t.packs === 1 ? '' : 's') + ' so far today</span>'
|
||||||
|
+ '<span><span class="fr-num">' + n(t.bonusCredits) + '</span> bonus credits issued</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var at = f.nextFriday ? startsAt(f.nextFriday) : null;
|
||||||
|
var left = at ? Math.max(0, at - Date.now()) : 0;
|
||||||
|
var hrs = Math.floor(left / 3600000), mins = Math.floor((left % 3600000) / 60000), days = Math.floor(hrs / 24);
|
||||||
|
var when = at ? (days >= 1 ? days + ' day' + (days === 1 ? '' : 's') + ' ' + (hrs % 24) + 'h' : hrs + 'h ' + mins + 'm') : '';
|
||||||
|
el.className = 'fr-state';
|
||||||
|
el.innerHTML = '<span class="fr-tag">Next</span><span><b>Next Five Dollar Friday: ' + (f.nextLabel || f.nextFriday || 'soon') + '.</b> Starts at midnight Central' + (when ? ', in ' + when : '') + '.</span>'
|
||||||
|
+ '<span>Join now so your account is ready before it starts.</span>';
|
||||||
|
}
|
||||||
|
fetch('/api/friday').then(function (r) { return r.ok ? r.json() : null; }).catch(function () { return null; }).then(render);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script src="/assets/common.js?v=20260923b"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+1079
-1079
File diff suppressed because it is too large
Load Diff
+170
-170
@@ -1,170 +1,170 @@
|
|||||||
// InstantAdPay QA harness: site walk.
|
// InstantAdPay QA harness: site walk.
|
||||||
// node qa/walk.mjs public -> live public pages (no sign-in): errors, failed requests, broken images, dead links, mobile
|
// node qa/walk.mjs public -> live public pages (no sign-in): errors, failed requests, broken images, dead links, mobile
|
||||||
// node qa/walk.mjs member -> local copy: sign in, every member pane + sub-tab, every admin pane, forms
|
// node qa/walk.mjs member -> local copy: sign in, every member pane + sub-tab, every admin pane, forms
|
||||||
// node qa/walk.mjs all -> both
|
// node qa/walk.mjs all -> both
|
||||||
// Env: LIVE (default https://instantadpay.com), LOCAL (default http://127.0.0.1:8796), OUT (report dir),
|
// Env: LIVE (default https://instantadpay.com), LOCAL (default http://127.0.0.1:8796), OUT (report dir),
|
||||||
// PW (playwright package dir; default D:/Projects/MarketingAgent/qa-tester/node_modules/playwright)
|
// PW (playwright package dir; default D:/Projects/MarketingAgent/qa-tester/node_modules/playwright)
|
||||||
// Exit code 1 when any [bug] finding remains after noise filtering.
|
// Exit code 1 when any [bug] finding remains after noise filtering.
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
const PW = process.env.PW || 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
|
const PW = process.env.PW || 'D:/Projects/MarketingAgent/qa-tester/node_modules/playwright';
|
||||||
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
|
const { chromium } = (await import(pathToFileURL(PW + '/index.js').href)).default;
|
||||||
const MODE = process.argv[2] || 'all';
|
const MODE = process.argv[2] || 'all';
|
||||||
const LIVE = process.env.LIVE || 'https://instantadpay.com';
|
const LIVE = process.env.LIVE || 'https://instantadpay.com';
|
||||||
const LOCAL = process.env.LOCAL || 'http://127.0.0.1:8796';
|
const LOCAL = process.env.LOCAL || 'http://127.0.0.1:8796';
|
||||||
const OUT = process.env.OUT || 'qa/out';
|
const OUT = process.env.OUT || 'qa/out';
|
||||||
fs.mkdirSync(OUT, { recursive: true });
|
fs.mkdirSync(OUT, { recursive: true });
|
||||||
const findings = [];
|
const findings = [];
|
||||||
const note = (sev, where, what) => findings.push({ sev, where, what });
|
const note = (sev, where, what) => findings.push({ sev, where, what });
|
||||||
const NOISE = /walletconnect|reown|web3modal|coingecko|fonts\.|\/api\/feed\/live|\/api\/auth\/logout/;
|
const NOISE = /walletconnect|reown|web3modal|coingecko|fonts\.|\/api\/feed\/live|\/api\/auth\/logout/;
|
||||||
|
|
||||||
function watch(page, base) {
|
function watch(page, base) {
|
||||||
const bag = { console: [], failed: [], status: [] };
|
const bag = { console: [], failed: [], status: [] };
|
||||||
page.on('pageerror', e => bag.console.push('pageerror: ' + e.message));
|
page.on('pageerror', e => bag.console.push('pageerror: ' + e.message));
|
||||||
page.on('console', m => {
|
page.on('console', m => {
|
||||||
if (m.type() !== 'error') return;
|
if (m.type() !== 'error') return;
|
||||||
const loc = (m.location() && m.location().url) || '';
|
const loc = (m.location() && m.location().url) || '';
|
||||||
if (loc && !loc.startsWith(base)) return; // third-party or framed page, not ours
|
if (loc && !loc.startsWith(base)) return; // third-party or framed page, not ours
|
||||||
if (/status of (400|401|404)/.test(m.text())) return; // expected API answers surface as console noise
|
if (/status of (400|401|404)/.test(m.text())) return; // expected API answers surface as console noise
|
||||||
bag.console.push(m.text());
|
bag.console.push(m.text());
|
||||||
});
|
});
|
||||||
page.on('requestfailed', r => { const u = r.url(); if (u.startsWith(base) && !NOISE.test(u)) bag.failed.push(u + ' ' + (r.failure() && r.failure().errorText)); });
|
page.on('requestfailed', r => { const u = r.url(); if (u.startsWith(base) && !NOISE.test(u)) bag.failed.push(u + ' ' + (r.failure() && r.failure().errorText)); });
|
||||||
page.on('response', r => { const st = r.status(); const u = r.url(); if (st >= 500 && u.startsWith(base)) bag.status.push(st + ' ' + u); });
|
page.on('response', r => { const st = r.status(); const u = r.url(); if (st >= 500 && u.startsWith(base)) bag.status.push(st + ' ' + u); });
|
||||||
return bag;
|
return bag;
|
||||||
}
|
}
|
||||||
function flush(bag, label) {
|
function flush(bag, label) {
|
||||||
for (const c of bag.console) note('bug', label, 'console: ' + c.slice(0, 200));
|
for (const c of bag.console) note('bug', label, 'console: ' + c.slice(0, 200));
|
||||||
for (const f of bag.failed) note('bug', label, 'request failed: ' + f.slice(0, 200));
|
for (const f of bag.failed) note('bug', label, 'request failed: ' + f.slice(0, 200));
|
||||||
for (const s of bag.status) note('bug', label, 'HTTP ' + s.slice(0, 200));
|
for (const s of bag.status) note('bug', label, 'HTTP ' + s.slice(0, 200));
|
||||||
bag.console.length = bag.failed.length = bag.status.length = 0;
|
bag.console.length = bag.failed.length = bag.status.length = 0;
|
||||||
}
|
}
|
||||||
async function domChecks(page, label) {
|
async function domChecks(page, label) {
|
||||||
const r = await page.evaluate(() => {
|
const r = await page.evaluate(() => {
|
||||||
const vis = el => el.offsetParent !== null;
|
const vis = el => el.offsetParent !== null;
|
||||||
const brokenImgs = [...document.images].filter(i => i.complete && i.naturalWidth === 0 && i.src && vis(i)).map(i => i.src);
|
const brokenImgs = [...document.images].filter(i => i.complete && i.naturalWidth === 0 && i.src && vis(i)).map(i => i.src);
|
||||||
const unfilled = [...document.querySelectorAll('body *')].filter(el => el.children.length === 0 && (el.textContent || '').trim() === '…' && vis(el)).length;
|
const unfilled = [...document.querySelectorAll('body *')].filter(el => el.children.length === 0 && (el.textContent || '').trim() === '…' && vis(el)).length;
|
||||||
const overflow = document.documentElement.scrollWidth > document.documentElement.clientWidth + 2;
|
const overflow = document.documentElement.scrollWidth > document.documentElement.clientWidth + 2;
|
||||||
return { brokenImgs, unfilled, overflow, title: document.title };
|
return { brokenImgs, unfilled, overflow, title: document.title };
|
||||||
});
|
});
|
||||||
if (r.brokenImgs.length) note('bug', label, 'broken images: ' + r.brokenImgs.slice(0, 3).join(', '));
|
if (r.brokenImgs.length) note('bug', label, 'broken images: ' + r.brokenImgs.slice(0, 3).join(', '));
|
||||||
if (r.unfilled) note('warn', label, r.unfilled + ' element(s) still showing the loading ellipsis');
|
if (r.unfilled) note('warn', label, r.unfilled + ' element(s) still showing the loading ellipsis');
|
||||||
if (r.overflow) note('warn', label, 'page scrolls horizontally');
|
if (r.overflow) note('warn', label, 'page scrolls horizontally');
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
const hide = page => page.evaluate(() => { document.querySelectorAll('.modal-back,.lgate,.iap-welcome').forEach(m => m.hidden = true); });
|
const hide = page => page.evaluate(() => { document.querySelectorAll('.modal-back,.lgate,.iap-welcome').forEach(m => m.hidden = true); });
|
||||||
|
|
||||||
const browser = await chromium.launch();
|
const browser = await chromium.launch();
|
||||||
|
|
||||||
if (MODE === 'public' || MODE === 'all') {
|
if (MODE === 'public' || MODE === 'all') {
|
||||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
||||||
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
||||||
const page = await ctx.newPage(); const bag = watch(page, LIVE);
|
const page = await ctx.newPage(); const bag = watch(page, LIVE);
|
||||||
const PUBLIC = ['/', '/ledger', '/contract', '/terms', '/privacy', '/disclaimer', '/wall/martbost', '/join/martbost',
|
const PUBLIC = ['/', '/ledger', '/contract', '/terms', '/privacy', '/disclaimer', '/wall/martbost', '/join/martbost',
|
||||||
'/join/martbost?v=instant', '/join/martbost?v=adspend', '/join/martbost?v=free', '/join/martbost?v=ledger', '/join/martbost?v=two',
|
'/join/martbost?v=instant', '/join/martbost?v=adspend', '/join/martbost?v=free', '/join/martbost?v=ledger', '/join/martbost?v=two',
|
||||||
'/admin', '/my', '/shorts', '/nope-404'];
|
'/admin', '/my', '/shorts', '/nope-404'];
|
||||||
for (const p of PUBLIC) {
|
for (const p of PUBLIC) {
|
||||||
const label = 'LIVE ' + p;
|
const label = 'LIVE ' + p;
|
||||||
try {
|
try {
|
||||||
const resp = await page.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
const resp = await page.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 });
|
||||||
await page.waitForTimeout(2500);
|
await page.waitForTimeout(2500);
|
||||||
const st = resp ? resp.status() : 0;
|
const st = resp ? resp.status() : 0;
|
||||||
if (p === '/nope-404') { if (st !== 404) note('warn', label, 'expected 404, got ' + st); }
|
if (p === '/nope-404') { if (st !== 404) note('warn', label, 'expected 404, got ' + st); }
|
||||||
else if (st >= 400) note('bug', label, 'page HTTP ' + st);
|
else if (st >= 400) note('bug', label, 'page HTTP ' + st);
|
||||||
const d = await domChecks(page, label);
|
const d = await domChecks(page, label);
|
||||||
const hrefs = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => h.startsWith(location.origin) && !h.includes('#') && !h.includes('/api/')))]);
|
const hrefs = await page.evaluate(() => [...new Set([...document.querySelectorAll('a[href]')].map(a => a.href).filter(h => h.startsWith(location.origin) && !h.includes('#') && !h.includes('/api/')))]);
|
||||||
for (const h of hrefs.slice(0, 40)) {
|
for (const h of hrefs.slice(0, 40)) {
|
||||||
try { const r = await page.request.head(h, { timeout: 15000 }); if (r.status() >= 400) note('bug', label, 'dead link ' + h + ' -> ' + r.status()); }
|
try { const r = await page.request.head(h, { timeout: 15000 }); if (r.status() >= 400) note('bug', label, 'dead link ' + h + ' -> ' + r.status()); }
|
||||||
catch (e) { note('warn', label, 'link check failed ' + h); }
|
catch (e) { note('warn', label, 'link check failed ' + h); }
|
||||||
}
|
}
|
||||||
flush(bag, label); console.log('ok', label, '|', d.title);
|
flush(bag, label); console.log('ok', label, '|', d.title);
|
||||||
} catch (e) { note('bug', label, 'navigation failed: ' + e.message.slice(0, 160)); flush(bag, label); }
|
} catch (e) { note('bug', label, 'navigation failed: ' + e.message.slice(0, 160)); flush(bag, label); }
|
||||||
}
|
}
|
||||||
const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true });
|
const m = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true });
|
||||||
await m.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
await m.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
||||||
const mp = await m.newPage(); const mbag = watch(mp, LIVE);
|
const mp = await m.newPage(); const mbag = watch(mp, LIVE);
|
||||||
for (const p of ['/', '/join/martbost?v=instant', '/wall/martbost', '/my']) {
|
for (const p of ['/', '/join/martbost?v=instant', '/wall/martbost', '/my']) {
|
||||||
await mp.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => note('bug', 'LIVE mobile ' + p, e.message));
|
await mp.goto(LIVE + p, { waitUntil: 'domcontentloaded', timeout: 45000 }).catch(e => note('bug', 'LIVE mobile ' + p, e.message));
|
||||||
await mp.waitForTimeout(2000); await domChecks(mp, 'LIVE mobile ' + p); flush(mbag, 'LIVE mobile ' + p);
|
await mp.waitForTimeout(2000); await domChecks(mp, 'LIVE mobile ' + p); flush(mbag, 'LIVE mobile ' + p);
|
||||||
await mp.screenshot({ path: OUT + '/mobile' + p.replace(/[^a-z0-9]+/gi, '-') + '.png' }).catch(() => {});
|
await mp.screenshot({ path: OUT + '/mobile' + p.replace(/[^a-z0-9]+/gi, '-') + '.png' }).catch(() => {});
|
||||||
}
|
}
|
||||||
await ctx.close(); await m.close();
|
await ctx.close(); await m.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MODE === 'member' || MODE === 'all') {
|
if (MODE === 'member' || MODE === 'all') {
|
||||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 950 } });
|
||||||
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
await ctx.addInitScript(() => { try { sessionStorage.setItem('iap.welcome.v1', '1'); } catch (e) {} }); // the session pop-up would block every click in an automated run
|
||||||
const page = await ctx.newPage(); const bag = watch(page, LOCAL);
|
const page = await ctx.newPage(); const bag = watch(page, LOCAL);
|
||||||
page.on('dialog', d => d.accept());
|
page.on('dialog', d => d.accept());
|
||||||
const L = 'LOCAL ';
|
const L = 'LOCAL ';
|
||||||
await page.goto(LOCAL + '/my', { waitUntil: 'networkidle' });
|
await page.goto(LOCAL + '/my', { waitUntil: 'networkidle' });
|
||||||
await page.fill('#mcEmail', 'qa-walk@example.com'); await page.click('#mcSendBtn');
|
await page.fill('#mcEmail', 'qa-walk@example.com'); await page.click('#mcSendBtn');
|
||||||
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
|
await page.waitForSelector('#mcVerifyBtn:not([hidden])'); await page.click('#mcVerifyBtn'); await page.waitForTimeout(2000);
|
||||||
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
|
if (await page.$('#onboardModal:not([hidden])')) { await page.fill('#obUsername', 'qawalker'); await page.click('#obSave'); await page.waitForTimeout(1200); }
|
||||||
await hide(page); flush(bag, L + 'sign-in');
|
await hide(page); flush(bag, L + 'sign-in');
|
||||||
const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
|
const PANES = ['overview', 'line', 'pipeline', 'buy', 'campaigns', 'earn', 'earnings', 'promo', 'training', 'wallet', 'profile'];
|
||||||
for (const pn of PANES) {
|
for (const pn of PANES) {
|
||||||
const label = L + 'my#' + pn;
|
const label = L + 'my#' + pn;
|
||||||
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
|
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200); await hide(page);
|
||||||
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
|
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
|
||||||
if (!vis) note('bug', label, 'pane did not render');
|
if (!vis) note('bug', label, 'pane did not render');
|
||||||
await domChecks(page, label);
|
await domChecks(page, label);
|
||||||
const subs = await page.$$('#pane-' + pn + ' .subtabs [data-earn], #pane-' + pn + ' .promo-pills [data-promo]');
|
const subs = await page.$$('#pane-' + pn + ' .subtabs [data-earn], #pane-' + pn + ' .promo-pills [data-promo]');
|
||||||
for (const s of subs) { try { await s.click(); await page.waitForTimeout(500); } catch (e) {} }
|
for (const s of subs) { try { await s.click(); await page.waitForTimeout(500); } catch (e) {} }
|
||||||
if (subs.length) await domChecks(page, label + ' (sub-tabs)');
|
if (subs.length) await domChecks(page, label + ' (sub-tabs)');
|
||||||
flush(bag, label);
|
flush(bag, label);
|
||||||
await page.screenshot({ path: OUT + '/my-' + pn + '.png' }).catch(() => {});
|
await page.screenshot({ path: OUT + '/my-' + pn + '.png' }).catch(() => {});
|
||||||
console.log('ok', label, 'subtabs:', subs.length);
|
console.log('ok', label, 'subtabs:', subs.length);
|
||||||
}
|
}
|
||||||
await page.click('.bo-menu [data-pane="promo"]'); await page.waitForTimeout(800);
|
await page.click('.bo-menu [data-pane="promo"]'); await page.waitForTimeout(800);
|
||||||
if (!(await page.$$('#promoPosts .promo-block')).length) note('bug', L + 'promo', 'no post cards rendered');
|
if (!(await page.$$('#promoPosts .promo-block')).length) note('bug', L + 'promo', 'no post cards rendered');
|
||||||
// viral links: the builder renders a ?ref= link; any page + ?ref=<member> redirects clean and sets the sponsor cookie; an unknown ref sets nothing
|
// viral links: the builder renders a ?ref= link; any page + ?ref=<member> redirects clean and sets the sponsor cookie; an unknown ref sets nothing
|
||||||
await page.click('.promo-pills [data-promo="viral"]'); await page.waitForTimeout(1200);
|
await page.click('.promo-pills [data-promo="viral"]'); await page.waitForTimeout(1200);
|
||||||
const vl = await page.$eval('#viralLink', e => e.textContent).catch(() => '');
|
const vl = await page.$eval('#viralLink', e => e.textContent).catch(() => '');
|
||||||
if (!/^https:\/\/instantadpay\.com\/.*[?&]ref=[a-z0-9_]+$/i.test(vl)) note('bug', L + 'promo/viral', 'builder link not rendered: ' + vl);
|
if (!/^https:\/\/instantadpay\.com\/.*[?&]ref=[a-z0-9_]+$/i.test(vl)) note('bug', L + 'promo/viral', 'builder link not rendered: ' + vl);
|
||||||
const me = await page.evaluate(() => fetch('/api/me').then(r => r.json()));
|
const me = await page.evaluate(() => fetch('/api/me').then(r => r.json()));
|
||||||
const tok = me.username || me.refCode;
|
const tok = me.username || me.refCode;
|
||||||
// served in place (no redirect: Facebook drops the name otherwise), cookie on the response, og:url carries the ref
|
// served in place (no redirect: Facebook drops the name otherwise), cookie on the response, og:url carries the ref
|
||||||
const rr = await page.request.get(LOCAL + '/?ref=' + tok + '&x=1', { maxRedirects: 0 });
|
const rr = await page.request.get(LOCAL + '/?ref=' + tok + '&x=1', { maxRedirects: 0 });
|
||||||
const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
const sc = (rr.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
||||||
const rb = await rr.text();
|
const rb = await rr.text();
|
||||||
if (rr.status() !== 200 || !/<title>/.test(rb)) note('bug', L + 'viral/inplace', 'expected the page at the decorated address, got ' + rr.status());
|
if (rr.status() !== 200 || !/<title>/.test(rb)) note('bug', L + 'viral/inplace', 'expected the page at the decorated address, got ' + rr.status());
|
||||||
if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc);
|
if (!new RegExp('iap\\.sponsor=' + tok + ';').test(sc) || !/iap\.angle=page;/.test(sc)) note('bug', L + 'viral/cookie', 'sponsor/angle cookie not set: ' + sc);
|
||||||
if (!new RegExp('property="og:url" content="[^"]*[?&]ref=' + tok + '"').test(rb)) note('bug', L + 'viral/og', 'og:url does not carry the ref');
|
if (!new RegExp('property="og:url" content="[^"]*[?&]ref=' + tok + '"').test(rb)) note('bug', L + 'viral/og', 'og:url does not carry the ref');
|
||||||
const ru = await page.request.get(LOCAL + '/?ref=nobody_zz9', { maxRedirects: 0 });
|
const ru = await page.request.get(LOCAL + '/?ref=nobody_zz9', { maxRedirects: 0 });
|
||||||
const su = (ru.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
const su = (ru.headersArray().filter(h => h.name.toLowerCase() === 'set-cookie').map(h => h.value)).join('; ');
|
||||||
const ub = await ru.text();
|
const ub = await ru.text();
|
||||||
if (ru.status() !== 200 || /iap\.sponsor=/.test(su) || /ref=nobody_zz9/.test(ub)) note('bug', L + 'viral/unknown', 'unknown ref must serve the plain page with no cookie and a clean og:url: ' + ru.status() + ' ' + su);
|
if (ru.status() !== 200 || /iap\.sponsor=/.test(su) || /ref=nobody_zz9/.test(ub)) note('bug', L + 'viral/unknown', 'unknown ref must serve the plain page with no cookie and a clean og:url: ' + ru.status() + ' ' + su);
|
||||||
const rj = await page.request.get(LOCAL + '/join/' + tok + '?ref=' + tok, { maxRedirects: 0 });
|
const rj = await page.request.get(LOCAL + '/join/' + tok + '?ref=' + tok, { maxRedirects: 0 });
|
||||||
if (rj.status() !== 200) note('bug', L + 'viral/join', '/join keeps its own ?ref handling, got ' + rj.status());
|
if (rj.status() !== 200) note('bug', L + 'viral/join', '/join keeps its own ?ref handling, got ' + rj.status());
|
||||||
console.log('ok viral links: builder + redirect + cookie');
|
console.log('ok viral links: builder + redirect + cookie');
|
||||||
const chat = await page.$('#chatMenuBtn'); if (chat) { await chat.click(); await page.waitForTimeout(800); await domChecks(page, L + 'messages'); flush(bag, L + 'messages'); }
|
const chat = await page.$('#chatMenuBtn'); if (chat) { await chat.click(); await page.waitForTimeout(800); await domChecks(page, L + 'messages'); flush(bag, L + 'messages'); }
|
||||||
const lo = await page.$('#logoutLink'); if (lo) { await lo.click(); await page.waitForTimeout(1000); }
|
const lo = await page.$('#logoutLink'); if (lo) { await lo.click(); await page.waitForTimeout(1000); }
|
||||||
if (!(await page.$('#authArea:not([hidden])'))) note('bug', L + 'logout', 'auth card not shown after log out');
|
if (!(await page.$('#authArea:not([hidden])'))) note('bug', L + 'logout', 'auth card not shown after log out');
|
||||||
flush(bag, L + 'logout');
|
flush(bag, L + 'logout');
|
||||||
// admin
|
// admin
|
||||||
await page.goto(LOCAL + '/admin', { waitUntil: 'networkidle' });
|
await page.goto(LOCAL + '/admin', { waitUntil: 'networkidle' });
|
||||||
await page.fill('#adEmail', process.env.ADMIN_EMAIL || 'martybostick@gmail.com'); await page.click('#adSend'); await page.waitForSelector('#adVerify:not([hidden])'); await page.click('#adVerify'); await page.waitForTimeout(1200);
|
await page.fill('#adEmail', process.env.ADMIN_EMAIL || 'martybostick@gmail.com'); await page.click('#adSend'); await page.waitForSelector('#adVerify:not([hidden])'); await page.click('#adVerify'); await page.waitForTimeout(1200);
|
||||||
for (const pn of ['overview', 'house', 'campaigns', 'members', 'reports', 'traffic', 'blog', 'releases', 'pnl', 'settings']) {
|
for (const pn of ['overview', 'house', 'campaigns', 'members', 'reports', 'traffic', 'blog', 'releases', 'pnl', 'settings']) {
|
||||||
const label = L + 'admin#' + pn;
|
const label = L + 'admin#' + pn;
|
||||||
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200);
|
await page.click('.bo-menu [data-pane="' + pn + '"]'); await page.waitForTimeout(1200);
|
||||||
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
|
const vis = await page.evaluate(id => { const el = document.getElementById('pane-' + id); return el && !el.hidden && el.offsetHeight > 40; }, pn);
|
||||||
if (!vis) note('bug', label, 'pane did not render');
|
if (!vis) note('bug', label, 'pane did not render');
|
||||||
await domChecks(page, label); flush(bag, label);
|
await domChecks(page, label); flush(bag, label);
|
||||||
await page.screenshot({ path: OUT + '/admin-' + pn + '.png' }).catch(() => {});
|
await page.screenshot({ path: OUT + '/admin-' + pn + '.png' }).catch(() => {});
|
||||||
console.log('ok', label);
|
console.log('ok', label);
|
||||||
}
|
}
|
||||||
await page.click('.bo-menu [data-pane="house"]'); await page.waitForTimeout(500);
|
await page.click('.bo-menu [data-pane="house"]'); await page.waitForTimeout(500);
|
||||||
for (const t of ['banner', 'text', 'login', 'solo', 'video', 'featured', 'visits']) { await page.selectOption('#hType', t); await page.waitForTimeout(120); }
|
for (const t of ['banner', 'text', 'login', 'solo', 'video', 'featured', 'visits']) { await page.selectOption('#hType', t); await page.waitForTimeout(120); }
|
||||||
await page.selectOption('#hType', 'text'); await page.click('#hCreate'); await page.waitForTimeout(800);
|
await page.selectOption('#hType', 'text'); await page.click('#hCreate'); await page.waitForTimeout(800);
|
||||||
if (!(await page.$('#hErr:not([hidden])'))) note('warn', L + 'admin house form', 'empty submit showed no validation message');
|
if (!(await page.$('#hErr:not([hidden])'))) note('warn', L + 'admin house form', 'empty submit showed no validation message');
|
||||||
flush(bag, L + 'admin house form');
|
flush(bag, L + 'admin house form');
|
||||||
await ctx.close();
|
await ctx.close();
|
||||||
}
|
}
|
||||||
await browser.close();
|
await browser.close();
|
||||||
|
|
||||||
const bugs = findings.filter(f => f.sev === 'bug'), warns = findings.filter(f => f.sev === 'warn');
|
const bugs = findings.filter(f => f.sev === 'bug'), warns = findings.filter(f => f.sev === 'warn');
|
||||||
const lines = ['===== QA WALK (' + MODE + ') ' + new Date().toISOString() + ' =====', 'bugs: ' + bugs.length + ' | warnings: ' + warns.length,
|
const lines = ['===== QA WALK (' + MODE + ') ' + new Date().toISOString() + ' =====', 'bugs: ' + bugs.length + ' | warnings: ' + warns.length,
|
||||||
...findings.map(f => '[' + f.sev + '] ' + f.where + ' :: ' + f.what)];
|
...findings.map(f => '[' + f.sev + '] ' + f.where + ' :: ' + f.what)];
|
||||||
console.log('\n' + lines.join('\n'));
|
console.log('\n' + lines.join('\n'));
|
||||||
fs.writeFileSync(OUT + '/walk-report.txt', lines.join('\n') + '\n');
|
fs.writeFileSync(OUT + '/walk-report.txt', lines.join('\n') + '\n');
|
||||||
process.exit(bugs.length ? 1 : 0);
|
process.exit(bugs.length ? 1 : 0);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const refill = require('./refill'); // campaign running-low / ran-out notices,
|
|||||||
const snapshot = require('./snapshot'); // daily growth snapshot -> Telegram payments feed (Marty, 2026-09-15)
|
const snapshot = require('./snapshot'); // daily growth snapshot -> Telegram payments feed (Marty, 2026-09-15)
|
||||||
const pipeline = require('./pipeline'); // sponsor follow-up board (coming soon until site setting pipelineMode = on) (Marty, 2026-09-15)
|
const pipeline = require('./pipeline'); // sponsor follow-up board (coming soon until site setting pipelineMode = on) (Marty, 2026-09-15)
|
||||||
const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
const videomaker = require('./videomaker'); // Circuit tool: promo videos with the member's own end card (ffmpeg in the image) // badge-gated promo toolkit + AI Copy Engine (Surge and up) (Marty, 2026-09-14) // referral contest: /leaderboard, Overview card, weekly + monthly winners (Marty, 2026-09-14) // release notes + roadmap: /whats-new, Overview card, Admin > Releases (Marty, 2026-09-14) // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard']);
|
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog', '/whats-new', '/leaderboard', '/friday']);
|
||||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||||
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
||||||
@@ -331,6 +331,7 @@ const JOIN_ANGLES = {
|
|||||||
instant: { t: 'Paid before the page reloads.', d: 'A smart contract on Polygon splits every ad package the moment it sells. Same transaction, real wallets, public ledger. Join free by email.' },
|
instant: { t: 'Paid before the page reloads.', d: 'A smart contract on Polygon splits every ad package the moment it sells. Same transaction, real wallets, public ledger. Join free by email.' },
|
||||||
adspend: { t: 'You were buying ads anyway.', d: 'Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, packages from $5. Join free.' },
|
adspend: { t: 'You were buying ads anyway.', d: 'Here the ad spend in your line pays you, in the same transaction, on a public ledger. Seven formats, packages from $5. Join free.' },
|
||||||
free: { t: 'Watch first. Spend never.', d: 'Join free, view a few ads, earn credits, run your first campaign for zero dollars. Every payout public on Polygon.' },
|
free: { t: 'Watch first. Spend never.', d: 'Join free, view a few ads, earn credits, run your first campaign for zero dollars. Every payout public on Polygon.' },
|
||||||
|
friday: { t: 'Five Dollar Friday: 20% bonus credits.', d: 'Every Friday, US Central time, any ad package from $5 up earns 20% bonus credits the moment it settles, and every pack pays its sponsor line in the same transaction. Join free by email.' },
|
||||||
ledger: { t: 'No back office. No payday.', d: 'Every payout is a public transaction on Polygon you can read yourself. Nothing is ever held. Join free by email.' },
|
ledger: { t: 'No back office. No payday.', d: 'Every payout is a public transaction on Polygon you can read yourself. Nothing is ever held. Join free by email.' },
|
||||||
two: { t: 'Two buyers open level two.', d: 'Every direct buyer pays you 50 percent from their first package. Two qualifying buyers open level two, five open level three. Written in a verified contract.' }
|
two: { t: 'Two buyers open level two.', d: 'Every direct buyer pays you 50 percent from their first package. Two qualifying buyers open level two, five open level three. Written in a verified contract.' }
|
||||||
};
|
};
|
||||||
@@ -1267,7 +1268,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
if (p === '/api/friday' && req.method === 'GET') return json(res, 200, friday.view(), { 'Cache-Control': 'public, max-age=60' });
|
if (p === '/api/friday' && req.method === 'GET') return json(res, 200, friday.view(), { 'Cache-Control': 'public, max-age=60' });
|
||||||
// -- the pages a member can turn into a viral link (promo tools > Viral links)
|
// -- the pages a member can turn into a viral link (promo tools > Viral links)
|
||||||
if (p === '/api/pages' && req.method === 'GET') {
|
if (p === '/api/pages' && req.method === 'GET') {
|
||||||
const pages = [{ path: '/', title: 'Home page' }, { path: '/blog', title: 'Blog' }, { path: '/earning', title: 'How earning works' }, { path: '/ledger', title: 'Live ledger' }, { path: '/leaderboard', title: 'Leaderboard' }, { path: '/contract', title: 'The contract' }, { path: '/plays', title: 'Team-building plays' }, { path: '/wallets', title: 'Wallet guide' }, { path: '/whats-new', title: "What's new" }, { path: '/shorts', title: 'Shorts' }];
|
const pages = [{ path: '/', title: 'Home page' }, { path: '/blog', title: 'Blog' }, { path: '/earning', title: 'How earning works' }, { path: '/ledger', title: 'Live ledger' }, { path: '/leaderboard', title: 'Leaderboard' }, { path: '/contract', title: 'The contract' }, { path: '/plays', title: 'Team-building plays' }, { path: '/wallets', title: 'Wallet guide' }, { path: '/friday', title: 'Five Dollar Friday' }, { path: '/whats-new', title: "What's new" }, { path: '/shorts', title: 'Shorts' }];
|
||||||
try { for (const b of (await blog.listPublished()).slice(0, 40)) pages.push({ path: '/blog/' + b.slug, title: 'Article: ' + b.title, cover: b.cover || null }); } catch (e) {}
|
try { for (const b of (await blog.listPublished()).slice(0, 40)) pages.push({ path: '/blog/' + b.slug, title: 'Article: ' + b.title, cover: b.cover || null }); } catch (e) {}
|
||||||
return json(res, 200, { pages });
|
return json(res, 200, { pages });
|
||||||
}
|
}
|
||||||
@@ -3390,6 +3391,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
return res.end(blog.renderPost(post, related));
|
return res.end(blog.renderPost(post, related));
|
||||||
}
|
}
|
||||||
if (p === '/wallets') return sendFile(res, path.join(PUBLIC_DIR, 'wallets.html'));
|
if (p === '/wallets') return sendFile(res, path.join(PUBLIC_DIR, 'wallets.html'));
|
||||||
|
if (p === '/friday') return sendFile(res, path.join(PUBLIC_DIR, 'friday.html')); // Five Dollar Friday bridge page: ?ref= works like any page (Marty, 2026-09-25)
|
||||||
if (p === '/launch') return sendFile(res, path.join(PUBLIC_DIR, 'launch.html'));
|
if (p === '/launch') return sendFile(res, path.join(PUBLIC_DIR, 'launch.html'));
|
||||||
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
||||||
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
m = /^\/uploads\/([a-z0-9]{24}\.(?:png|jpg|webp|gif|mp4|webm))$/.exec(p);
|
||||||
|
|||||||
Reference in New Issue
Block a user