From 19b7770a4dc637bce4b7888d4555fe8548116ba1 Mon Sep 17 00:00:00 2001 From: martbost Date: Mon, 7 Sep 2026 08:13:09 -0500 Subject: [PATCH] Video once-per-day dedup + featured gold border & spacing - Video watch-to-earn: a member can no longer re-earn from the same video the same day. New video_seen table (dual-mode); serveVideo excludes today's already-watched videos; videowatch double-checks before crediting. - Featured card: equal top/bottom margin (no longer touches the stat cards) and a gold border on the card + the rotating link. Co-Authored-By: Claude Opus 4.8 --- ads.js | Bin 59931 -> 61390 bytes db.js | 8 ++++++++ public/assets/site.css | 10 +++++----- public/my.html | 2 +- server.js | 3 +++ 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ads.js b/ads.js index 4fe759a9680cf99f24521bb2901f5999a98d0af6..33d6cd08319076bbddcb4879108e6530c56333e5 100644 GIT binary patch delta 957 zcma))&ui0Q7{|ehXl~jKw${0)sA~dg=B^E~j5)PqYhkPPFw1zG_iYnxlA1Re8?NfX zn};#q%Vw{e2T$VsD?BKoh#UL++Ayh)Z+Cj1Rh2kvZCnFeiB|}zjplNQNFu7aU#xT+q*y6yS=fR>A5u4 zQNThUuZPJ=sLX4)CbSOxXuK^LhS3`Ud60Xl+M3Ok(PeR_%}rRTDv;w}cI#7)y?jcO zqE%Yx;FC1lHJc$OR5mpmA6)3Xl9s2Yz;jKKZ4p1q2Ssbt5-`DG3#bPO9OBf74<>Rm z0r(_Kkncj>_Z$c;0u(s};+lv7L*Ir^RfH^|ij9As9a9BP1B#*OBb&%+A-lmPDQp~_ zWw#Gg^Vf={r8!Q!T#8#otT`A5*-f=uuxewKi&O)uKxJm(#@sTQZU3j~;hwf*teDlO&oP16yTylMqrJlVY0Bldzkqvvi!Z3bSUb Jn?sW(@2GmJ7}o#* diff --git a/db.js b/db.js index c8b2a73..4bd412a 100644 --- a/db.js +++ b/db.js @@ -142,6 +142,14 @@ async function bootstrap() { // per-account chat settings: availability toggle (default on) + muted-member list (JSON emails) await alterSafe('ALTER TABLE accounts ADD COLUMN chat_available TINYINT NOT NULL DEFAULT 1'); await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL'); + await q(`CREATE TABLE IF NOT EXISTS video_seen ( + email VARCHAR(190) NOT NULL, + campaign_id INT NOT NULL, + day CHAR(10) NOT NULL, + ts BIGINT NOT NULL, + UNIQUE KEY uq_vseen (email, campaign_id, day), + INDEX (email, day) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); await q(`CREATE TABLE IF NOT EXISTS ad_reports ( id INT AUTO_INCREMENT PRIMARY KEY, campaign_id INT NOT NULL, diff --git a/public/assets/site.css b/public/assets/site.css index bb79f41..2044c66 100644 --- a/public/assets/site.css +++ b/public/assets/site.css @@ -274,12 +274,12 @@ textarea{resize:vertical;font:inherit} .feat-day .fd-occ{font-family:var(--mono);font-size:11px;color:var(--muted)} .feat-day.open2 .fd-occ{color:var(--mint)} /* ── featured rotation strip ── */ -.feat-top{max-width:760px;margin:0 auto 16px} +.feat-top{max-width:760px;margin:16px auto;border:1px solid rgba(255,177,56,.5)} .feat-strip{display:flex;justify-content:center;margin-top:10px} -.feat-strip a{max-width:728px;width:100%} -.feat-strip a{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px; - background:var(--panel);border:1px solid var(--line-strong);color:var(--ink);text-decoration:none;font-weight:600} -.feat-strip a:hover{border-color:var(--mint)} +.feat-strip a{max-width:728px;width:100%;display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px; + background:var(--panel);border:1px solid #ffb238;color:var(--ink);text-decoration:none;font-weight:600} +.feat-strip a:hover{border-color:#ffd15c;box-shadow:0 0 0 1px rgba(255,177,56,.4)} +.feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap} .feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap} /* ── achievement badges ── */ .badges{display:flex;gap:18px;flex-wrap:wrap;margin-top:12px} diff --git a/public/my.html b/public/my.html index 760a3b1..258c44a 100644 --- a/public/my.html +++ b/public/my.html @@ -4,7 +4,7 @@ Member area | InstantAdPay - + diff --git a/server.js b/server.js index e96ac59..9dede4b 100644 --- a/server.js +++ b/server.js @@ -934,9 +934,12 @@ const server = http.createServer(async (req, res) => { if (age < t.secs * 1000 - 600) return json(res, 400, { error: 'Watch the full video first.' }); if (age > t.secs * 1000 + 10 * 60 * 1000) { videoTokens.delete(s.email); return json(res, 400, { error: 'That watch went stale. Load a fresh video.' }); } videoTokens.delete(s.email); // single use + if (await ads.hasWatchedVideoToday(s.email, t.id)) // once-per-day-per-video: no double earning + return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), already: true }); const tier = await ads.chargeVideoView(t.id); // charge advertiser; null if it ran dry if (!tier) return json(res, 200, { ok: true, credited: 0, status: await ads.videoStatus(s.email), gone: true }); await ads.addEarned(s.email, tier.reward); + await ads.markVideoSeen(s.email, t.id); const status = await ads.recordVideoWatch(s.email); return json(res, 200, { ok: true, credited: tier.reward, status }); }