From 4991fe96e87b829a87f86776fcfa63e74eac3844 Mon Sep 17 00:00:00 2001
From: martbost
Date: Mon, 7 Sep 2026 07:57:12 -0500
Subject: [PATCH] Ad reporting + featured rotates one link + clear campaign
form on submit
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Ad reporting (auto-approved ads need a safety valve): new reports.js store
(dual-mode) + ad_reports table + POST /api/report-ad; a "⚠ report" control on
served ad slots opens a reason prompt and notifies the admin by email
(siteConfig.adminEmail / ADMIN_EMAIL).
- Featured links now show ONE link at a time and cycle (true rotation), instead
of listing all links at once.
- Campaign form now clears EVERY field on submit (target/creative/title/etc.).
A leftover target URL was carrying into the next campaign — the same reason a
video short was saved with the wrong (massifly) CTA target.
Co-Authored-By: Claude Opus 4.8
---
db.js | 10 +++++++
public/assets/common.js | 22 +++++++++++++---
public/assets/my.js | 25 +++++++++++++-----
public/contract.html | 2 +-
public/index.html | 2 +-
public/ledger.html | 2 +-
public/my.html | 6 ++---
public/tx.html | 2 +-
public/wall.html | 2 +-
reports.js | 58 +++++++++++++++++++++++++++++++++++++++++
server.js | 20 ++++++++++++++
11 files changed, 133 insertions(+), 18 deletions(-)
create mode 100644 reports.js
diff --git a/db.js b/db.js
index 6cfc415..c8b2a73 100644
--- a/db.js
+++ b/db.js
@@ -142,6 +142,16 @@ 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 ad_reports (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ campaign_id INT NOT NULL,
+ reporter VARCHAR(190) NOT NULL DEFAULT '',
+ reason VARCHAR(20) NOT NULL,
+ note VARCHAR(600) NULL,
+ ts BIGINT NOT NULL,
+ resolved TINYINT NOT NULL DEFAULT 0,
+ INDEX (resolved, ts), INDEX (campaign_id)
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await q(`CREATE TABLE IF NOT EXISTS burns (
id VARCHAR(32) PRIMARY KEY,
member_id INT NOT NULL,
diff --git a/public/assets/common.js b/public/assets/common.js
index 8638280..9dd379a 100644
--- a/public/assets/common.js
+++ b/public/assets/common.js
@@ -96,6 +96,21 @@ window.IAP = (function () {
return div;
}
// Render one served ad into #. Silent if no inventory.
+ // report an ad (auto-approved ads need a member-facing flag → admin notified)
+ function reportAd(campaignId) {
+ if (!campaignId) return;
+ const reason = (prompt('Report this ad. Reason: broken, inappropriate, spam, scam, or other', 'broken') || '').trim().toLowerCase();
+ if (!reason) return;
+ const note = prompt('Anything to add? (optional)') || '';
+ fetch('/api/report-ad', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ campaignId, reason, note }) })
+ .then(() => status('Thanks — this ad was reported to the admin for review.', 'ok'))
+ .catch(() => status('Could not send the report. Try again.', 'bad'));
+ }
+ const reportTag = ad => ' ⚠ report';
+ function wireReport(el) {
+ const rl = el.querySelector('.ad-report');
+ if (rl) rl.addEventListener('click', e => { e.preventDefault(); reportAd(Number(rl.dataset.cid)); });
+ }
async function adSlot(type, elId) {
try {
const { ad } = await (await fetch('/api/ads/slot?type=' + type)).json();
@@ -104,13 +119,14 @@ window.IAP = (function () {
if (ad.imageUrl) {
el.innerHTML = ''
+ '
'
- + 'member ad
';
+ + 'member ad' + reportTag(ad) + '
';
} else {
el.innerHTML = '' + ad.title + ''
- + (ad.body ? ' · ' + ad.body : '') + ' member ad';
+ + (ad.body ? ' · ' + ad.body : '') + ' member ad' + reportTag(ad) + '';
}
+ wireReport(el);
el.hidden = false;
} catch (e) {}
}
- return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, $ };
+ return { getConfig, fmtPol, fmtUsd, status, renderNav, refreshNavWallet, describeEvent, feedRow, adSlot, reportAd, $ };
})();
diff --git a/public/assets/my.js b/public/assets/my.js
index 38f2ba9..ffd9fe7 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -28,16 +28,24 @@
return 'Fully qualified. Every level pays you, and you catch the pass-ups that under-qualified positions below you let slip. Keep sharing and keep your campaigns running.';
}
// featured rotation strip on the overview
+ let featItems = [], featIdx = 0, featTimer = null;
+ function renderOneFeatured() {
+ if (!featItems.length) return;
+ const i = featItems[featIdx % featItems.length];
+ $('featStrip').innerHTML = '' + esc(i.title) + ''
+ + (i.by ? '' + esc(i.by) + '' : '') + '';
+ }
async function loadFeatured() {
try {
const r = await (await fetch('/api/featured')).json();
const card = $('featuredCard'); if (!card) return;
if (!r.items || !r.items.length) { card.hidden = true; return; }
card.hidden = false;
+ featItems = r.items; featIdx = Math.floor(Math.random() * featItems.length);
$('featSub').textContent = r.items.length + ' link' + (r.items.length === 1 ? '' : 's') + ' in rotation';
- $('featStrip').innerHTML = r.items.map(i =>
- '' + esc(i.title) + ''
- + (i.by ? '' + esc(i.by) + '' : '') + '').join('');
+ renderOneFeatured(); // show ONE at a time (true rotation), cycle if more than one
+ clearInterval(featTimer);
+ if (featItems.length > 1) featTimer = setInterval(() => { featIdx++; renderOneFeatured(); }, 6000);
} catch (e) {}
}
@@ -665,11 +673,14 @@
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
budget: isFeat ? (Number($('cFeatDays').value) * (lastRates.featuredPerDay || 40)) : Number($('cBudget').value) });
IAP.status('Campaign is live. It starts serving right away.', 'ok');
- $('cName').value = ''; $('cBudget').value = '';
- $('cSoloEd').innerHTML = ''; $('cSoloRaw').value = ''; $('cCtaLabel').value = '';
- $('cVideoUrl').value = ''; $('cVideoTitle').value = ''; $('cVideoCta').value = '';
+ // clear EVERY field so no target/creative carries into the next campaign
+ ['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel',
+ 'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle']
+ .forEach(id => { if ($(id)) $(id).value = ''; });
+ $('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = '';
$('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = '';
- $('edMediaInfo').textContent = '';
+ if ($('edMediaInfo')) $('edMediaInfo').textContent = '';
+ cVidDims = null;
await loadCampaigns();
}));
// defers the busy() lookup to click time (busy is declared below)
diff --git a/public/contract.html b/public/contract.html
index 0bf7e6d..441d286 100644
--- a/public/contract.html
+++ b/public/contract.html
@@ -140,7 +140,7 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
-
+
@@ -641,9 +641,9 @@
-
+
-
+