diff --git a/ads.js b/ads.js
index 99d511b..5cc0df6 100644
Binary files a/ads.js and b/ads.js differ
diff --git a/db.js b/db.js
index 07d0ce1..ca5bb05 100644
--- a/db.js
+++ b/db.js
@@ -115,6 +115,7 @@ async function bootstrap() {
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_ad_id INT NULL'); // syndicated NAS sponsorads.ID
await alterSafe('ALTER TABLE campaigns ADD COLUMN nas_served INT NOT NULL DEFAULT 0'); // NAS impressions already reconciled into spend
await alterSafe('ALTER TABLE campaigns ADD COLUMN expires BIGINT NULL'); // featured rotation end time
+ await alterSafe('ALTER TABLE campaigns ADD COLUMN starts BIGINT NULL'); // featured run start (booked day)
await q(`CREATE TABLE IF NOT EXISTS sponsor_messages (
id INT AUTO_INCREMENT PRIMARY KEY,
from_member INT NOT NULL,
diff --git a/public/assets/my.js b/public/assets/my.js
index b009319..201275e 100644
--- a/public/assets/my.js
+++ b/public/assets/my.js
@@ -514,17 +514,29 @@
if (t === 'video') videoHint();
if (t === 'featured') featHint();
});
- function featHint() {
+ let featStartDay = 0; // selected start-day offset (0 = today)
+ async function featHint() {
if (!lastRates || !lastRates.featuredDurations) return;
if ($('cFeatDays') && !$('cFeatDays').options.length)
$('cFeatDays').innerHTML = lastRates.featuredDurations.map(dys =>
'' + dys + ' day' + (dys > 1 ? 's' : '') + ' — ' + (dys * lastRates.featuredPerDay) + ' credits ').join('');
- fetch('/api/featured/stats').then(r => r.json()).then(s => {
- const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]);
- $('cFeatHint').textContent = (s.activeCount || 0) + ' link' + (s.activeCount === 1 ? '' : 's')
- + ' currently share the rotation — yours would make ' + ((s.activeCount || 0) + 1)
- + '. Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' for ' + (dys * lastRates.featuredPerDay) + ' credits.';
- }).catch(() => {});
+ let s = null;
+ try { s = await (await fetch('/api/featured/stats')).json(); } catch (e) {}
+ if (s && s.occupancy) {
+ const grid = $('cFeatDaysGrid');
+ grid.innerHTML = s.occupancy.map(d => {
+ const label = d.offset === 0 ? 'Today' : d.offset === 1 ? 'Tomorrow' : new Date(d.day + 'T00:00:00Z').toLocaleDateString(undefined, { weekday: 'short', month: 'numeric', day: 'numeric' });
+ const full = d.open <= 0;
+ return '
'
+ + '
' + label + '
' + d.count + '/' + d.cap + (full ? ' full' : ' left ' + d.open) + '
';
+ }).join('');
+ grid.querySelectorAll('.feat-day:not(.full)').forEach(el =>
+ el.addEventListener('click', () => { featStartDay = Number(el.dataset.off); featHint(); }));
+ }
+ const dys = Number($('cFeatDays').value) || (lastRates.featuredDurations[0]);
+ const dayLabel = featStartDay === 0 ? 'today' : featStartDay === 1 ? 'tomorrow' : 'in ' + featStartDay + ' days';
+ $('cFeatHint').textContent = 'Runs ' + dys + ' day' + (dys > 1 ? 's' : '') + ' starting ' + dayLabel
+ + ' for ' + (dys * lastRates.featuredPerDay) + ' credits. Max ' + (s ? s.slotsPerDay : 10) + ' links share any day.';
}
document.addEventListener('change', e => { if (e.target && e.target.id === 'cFeatDays') featHint(); });
// video composer: tier dropdown + upload + price hint
@@ -566,7 +578,7 @@
title: isVideo ? $('cVideoTitle').value : isFeat ? $('cFeatTitle').value : $('cTitle').value,
body: t === 'solo' ? soloBody : $('cBody').value,
videoUrl: $('cVideoUrl').value, watchSecs: Number($('cWatchSecs').value),
- days: Number($('cFeatDays').value),
+ days: Number($('cFeatDays').value), startDay: featStartDay,
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');
diff --git a/public/assets/site.css b/public/assets/site.css
index cd6e341..84acd5e 100644
--- a/public/assets/site.css
+++ b/public/assets/site.css
@@ -259,6 +259,15 @@ textarea{resize:vertical;font:inherit}
#boBurger{display:block}
.bo-content{padding:18px}
}
+/* ── featured day-slot picker ── */
+.feat-days{display:flex;gap:8px;flex-wrap:wrap}
+.feat-day{flex:0 0 auto;min-width:82px;padding:8px 10px;border:1px solid var(--line-strong);border-radius:10px;
+ background:var(--panel);cursor:pointer;text-align:center}
+.feat-day.on{border-color:var(--mint);box-shadow:0 0 0 1px var(--mint)}
+.feat-day.full{opacity:.45;cursor:not-allowed}
+.feat-day .fd-day{font-weight:700;font-size:12px}
+.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-strip{display:flex;flex-direction:column;gap:8px;margin-top:10px}
.feat-strip a{display:flex;justify-content:space-between;gap:10px;padding:11px 14px;border-radius:10px;
diff --git a/public/contract.html b/public/contract.html
index a42efe2..1e9d182 100644
--- a/public/contract.html
+++ b/public/contract.html
@@ -5,7 +5,7 @@
The contract | InstantAdPay
-
+
@@ -129,8 +129,8 @@
Advertising services with a performance referral program. Not an investment product; no income guarantees. Crypto transactions are irreversible. Never spend what you cannot afford.
-
-
-
+
+
+