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 <noreply@anthropic.com>
This commit is contained in:
@@ -142,6 +142,14 @@ async function bootstrap() {
|
|||||||
// per-account chat settings: availability toggle (default on) + muted-member list (JSON emails)
|
// 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_available TINYINT NOT NULL DEFAULT 1');
|
||||||
await alterSafe('ALTER TABLE accounts ADD COLUMN chat_mutes VARCHAR(4000) NULL');
|
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 (
|
await q(`CREATE TABLE IF NOT EXISTS ad_reports (
|
||||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
campaign_id INT NOT NULL,
|
campaign_id INT NOT NULL,
|
||||||
|
|||||||
@@ -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 .fd-occ{font-family:var(--mono);font-size:11px;color:var(--muted)}
|
||||||
.feat-day.open2 .fd-occ{color:var(--mint)}
|
.feat-day.open2 .fd-occ{color:var(--mint)}
|
||||||
/* ── featured rotation strip ── */
|
/* ── 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{display:flex;justify-content:center;margin-top:10px}
|
||||||
.feat-strip a{max-width:728px;width:100%}
|
.feat-strip a{max-width:728px;width:100%;display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;
|
||||||
.feat-strip a{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}
|
||||||
background:var(--panel);border:1px solid var(--line-strong);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 a:hover{border-color:var(--mint)}
|
.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}
|
.feat-strip .by{font-size:12px;color:var(--muted);font-weight:500;white-space:nowrap}
|
||||||
/* ── achievement badges ── */
|
/* ── achievement badges ── */
|
||||||
.badges{display:flex;gap:18px;flex-wrap:wrap;margin-top:12px}
|
.badges{display:flex;gap:18px;flex-wrap:wrap;margin-top:12px}
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260907n">
|
<link rel="stylesheet" href="/assets/site.css?v=20260907o">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
|
|||||||
@@ -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 - 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.' }); }
|
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
|
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
|
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 });
|
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.addEarned(s.email, tier.reward);
|
||||||
|
await ads.markVideoSeen(s.email, t.id);
|
||||||
const status = await ads.recordVideoWatch(s.email);
|
const status = await ads.recordVideoWatch(s.email);
|
||||||
return json(res, 200, { ok: true, credited: tier.reward, status });
|
return json(res, 200, { ok: true, credited: tier.reward, status });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user