Country-tier targeting for campaigns: DB-IP lite lookup, tier lists in Settings, Show-to tiers on every format, geo-restricted ads kept off the network, per-country serve stats

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-11 12:02:40 -05:00
parent 0f855292f5
commit cd26161db5
7 changed files with 158 additions and 9 deletions
+9 -2
View File
@@ -595,6 +595,7 @@
const r = await (await fetch('/api/my/campaigns')).json();
if (r.error) return;
lastRates = r.rates;
if ($('cGeoHint') && r.tiers) $('cGeoHint').textContent = 'All three ticked = everyone. Tier 1: ' + r.tiers.t1.join(', ') + '. Tier 2: ' + r.tiers.t2.join(', ') + '. Tier 3: every other country. Geo applies to delivery on this site; a narrowed banner or text ad is kept off the worldwide partner network. ' + (r.geoReady ? '' : 'Country data is still loading, so narrowed campaigns pause until it is ready. ') + 'IP geolocation by DB-IP.';
// populate the banner-size dropdown once (ids map to NAS width/height)
if (r.bannerSizes && $('cSize') && !$('cSize').options.length)
$('cSize').innerHTML = r.bannerSizes.map(s => '<option value="' + s.id + '">' + s.label + '</option>').join('');
@@ -622,8 +623,8 @@
tbl.className = 'tablewrap';
tbl.innerHTML = '<table><thead><tr><th>Name</th><th>Type</th><th class="num">Views here</th><th class="num" title="Impressions delivered by Network Ad Space across the wider network (banner and text ads only)">Network views</th><th class="num">Clicks</th>'
+ '<th class="num">Spent</th><th class="num">Budget</th><th>Status</th><th></th></tr></thead><tbody>'
+ r.campaigns.map(c => '<tr><td><b>' + c.name + '</b>' + hourBars(r.hours && r.hours[c.id]) + '</td>'
+ '<td>' + c.type + (c.type === 'banner' && c.width ? ' <span class="muted small">' + c.width + '×' + c.height + '</span>' : '') + (c.dailyCap ? ' <span class="muted small" title="daily cap: ' + c.dailyCap + ' credits, ' + (c.daySpent || 0) + ' spent today">cap ' + c.dailyCap + '/day</span>' : '') + schedChips(c) + '</td>'
+ r.campaigns.map(c => '<tr><td><b>' + c.name + '</b>' + hourBars(r.hours && r.hours[c.id]) + geoLine(r.geo && r.geo[c.id]) + '</td>'
+ '<td>' + c.type + (c.type === 'banner' && c.width ? ' <span class="muted small">' + c.width + '×' + c.height + '</span>' : '') + (c.dailyCap ? ' <span class="muted small" title="daily cap: ' + c.dailyCap + ' credits, ' + (c.daySpent || 0) + ' spent today">cap ' + c.dailyCap + '/day</span>' : '') + (c.geo ? ' <span class="muted small" title="shown only to viewers in these country tiers">tier ' + esc(c.geo.replace(/,/g, '+')) + '</span>' : '') + schedChips(c) + '</td>'
+ '<td class="num">' + c.imps.toLocaleString() + '</td>'
+ '<td class="num">' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : '<span class="muted small" title="only banner and text ads syndicate to the network">n/a</span>') + '</td>'
+ '<td class="num">' + c.clicks + (r.clickSources && r.clickSources[c.id] ? '<div class="muted small" style="white-space:nowrap" title="where the clicks happened">' + Object.entries(r.clickSources[c.id]).sort((a, b) => b[1] - a[1]).map(([k, v]) => esc(k) + ' ' + v).join(' · ') + (c.impsNas ? ' · network: see Network views' : '') + '</div>' : '') + '</td>'
@@ -820,6 +821,7 @@
days: Number($('cFeatDays').value), startDay: featStartDay,
count: Number($('cVisitCount').value),
dailyCap: ($('cDailyCap') && (t === 'banner' || t === 'text')) ? Number($('cDailyCap').value) || 0 : 0,
geo: [...document.querySelectorAll('.geoTier:checked')].map(x => x.value).join(','),
startsAt: ($('cStartAt') && $('cStartAt').value && !isFeat) ? new Date($('cStartAt').value).getTime() : 0,
endsAt: ($('cEndAt') && $('cEndAt').value && !isFeat) ? new Date($('cEndAt').value).getTime() : 0,
ctaLabel: isVideo ? $('cVideoCta').value : $('cCtaLabel').value,
@@ -830,6 +832,7 @@
['cName', 'cBudget', 'cTarget', 'cImage', 'cTitle', 'cBody', 'cCtaLabel',
'cVideoUrl', 'cVideoTitle', 'cVideoCta', 'cVisitTitle', 'cVisitCount', 'cFeatTitle', 'cDailyCap', 'cStartAt', 'cEndAt']
.forEach(id => { if ($(id)) $(id).value = ''; });
document.querySelectorAll('.geoTier').forEach(x => { x.checked = true; });
$('cSoloEd').innerHTML = ''; if ($('cSoloRaw')) $('cSoloRaw').value = '';
$('cVideoInfo').textContent = ''; $('cVideoPrev').hidden = true; $('cVideoPrev').innerHTML = '';
if ($('edMediaInfo')) $('edMediaInfo').textContent = '';
@@ -1098,6 +1101,10 @@
if (c.expires && c.type !== 'featured') s += ' <span class="muted small">' + (c.expires > now ? 'ends ' : 'ended ') + fmtWhen(c.expires) + '</span>';
return s;
}
function geoLine(rows) {
if (!rows || !rows.length) return '';
return '<div class="muted small" style="font-size:10px" title="on-site serves by viewer country">' + rows.map(r => esc(r.cc) + ' ' + r.n).join(' · ') + '</div>';
}
function hourBars(utc) {
if (!utc || !utc.some(n => n)) return '';
// rotate the 24 UTC buckets into the viewer's local hours
+8 -1
View File
@@ -447,6 +447,13 @@
<p id="cBudgetRow"><input id="cBudget" type="number" placeholder="Budget (credits)" min="10" style="width:100%"></p>
<p id="cCapRow" style="grid-column:1/-1"><input id="cDailyCap" type="number" placeholder="Daily cap in credits (optional) — paces the budget across days" min="10" style="width:100%">
<span class="small muted">Network Ad Space can deliver a lot of impressions fast. A daily cap spreads a banner or text budget over days instead of hours.</span></p>
<div id="cGeoRow" style="grid-column:1/-1">
<span class="small muted">Show to:</span>
<label class="small" style="margin-left:10px"><input type="checkbox" class="geoTier" value="1" checked style="width:auto"> Tier 1 countries</label>
<label class="small" style="margin-left:10px"><input type="checkbox" class="geoTier" value="2" checked style="width:auto"> Tier 2</label>
<label class="small" style="margin-left:10px"><input type="checkbox" class="geoTier" value="3" checked style="width:auto"> Tier 3 (everyone else)</label>
<span class="small muted" id="cGeoHint" style="display:block;margin-top:4px">All three ticked = everyone. Untick tiers to narrow it. Geo applies to delivery on this site; a narrowed banner or text ad is kept off the worldwide partner network. IP geolocation by DB-IP.</span>
</div>
<div id="cSchedRow" style="grid-column:1/-1">
<div style="display:flex;gap:10px;flex-wrap:wrap">
<label class="small muted" style="flex:1 1 200px;display:block"><span id="cStartLbl">Start (optional)</span><input id="cStartAt" type="datetime-local" style="width:100%;margin-top:4px"></label>
@@ -902,7 +909,7 @@
<script src="/assets/common.js?v=20260911c"></script>
<script src="/assets/wallet.js?v=20260911a"></script>
<script src="/assets/promo.js?v=20260911a"></script>
<script src="/assets/my.js?v=20260911v"></script>
<script src="/assets/my.js?v=20260911w"></script>
<script src="/assets/chat.js?v=20260907l"></script>
</body>
</html>