// Traffic Desk client — pick size, pick approved creative, pick destination,
// launch. Balance and running campaigns come from the server.
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (ch) {
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch];
});
}
var state = { size: null, creative: null, target: 'join', creatives: {}, remaining: 0, id: null,
format: 'banner', angle: 'general', angles: [], variants: [], variant: null, hasPage: false };
var busy = false;
function chip(label, on, fn) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'tf-chip' + (on ? ' on' : '');
b.textContent = label;
b.addEventListener('click', fn);
return b;
}
function renderSizes(sizes) {
var host = $('tfSizes');
host.innerHTML = '';
sizes.forEach(function (s) {
host.appendChild(chip(s, s === state.size, function () {
state.size = s;
state.creative = (state.creatives[s] || [])[0] || null;
renderSizes(sizes); renderCreatives();
}));
});
}
function renderCreatives() {
var host = $('tfCreatives');
host.innerHTML = '';
(state.creatives[state.size] || []).forEach(function (f) {
var d = document.createElement('div');
d.className = 'tf-cre' + (f === state.creative ? ' on' : '');
var img = document.createElement('img');
img.src = '/banners/' + f;
img.alt = 'Team banner ' + state.size;
img.loading = 'lazy';
d.appendChild(img);
d.addEventListener('click', function () { state.creative = f; renderCreatives(); });
host.appendChild(d);
});
}
function renderTargets() {
var host = $('tfTargets');
host.innerHTML = '';
host.appendChild(chip('My invite page', state.target === 'join', function () {
state.target = 'join'; renderTargets();
}));
if (state.hasPage) {
host.appendChild(chip('My personal page', state.target === 'page', function () {
state.target = 'page'; renderTargets();
}));
} else {
// Don't let anyone point an ad at a page that doesn't exist yet.
var d = document.createElement('span');
d.className = 'tf-hint';
d.style.cssText = 'margin:0;align-self:center';
d.innerHTML = 'Want to send traffic to your own page instead? Build it first →';
host.appendChild(d);
}
}
function renderFormat() {
var host = $('tfFormat');
host.innerHTML = '';
[['banner', 'Banner image'], ['text', 'Text ad']].forEach(function (f) {
host.appendChild(chip(f[1], state.format === f[0], function () {
state.format = f[0];
renderFormat();
goLabel();
$('tfBannerPane').style.display = f[0] === 'banner' ? '' : 'none';
$('tfTextPane').style.display = f[0] === 'text' ? '' : 'none';
}));
});
}
function renderAngles() {
var host = $('tfAngles');
host.innerHTML = '';
state.angles.forEach(function (a) {
host.appendChild(chip(a.label, state.angle === a.key, function () {
state.angle = a.key; renderAngles();
}));
});
}
function renderVariants() {
var host = $('tfVariants');
host.innerHTML = '';
if (!state.variants.length) return;
var grid = document.createElement('div');
grid.className = 'tf-ads';
state.variants.forEach(function (v, i) {
var card = document.createElement('div');
card.className = 'tf-ad' + (state.variant === i ? ' on' : '');
var s = document.createElement('div');
s.className = 's'; s.textContent = v.subject;
card.appendChild(s);
v.lines.forEach(function (l) {
if (!l) return;
var d = document.createElement('div');
d.className = 'l'; d.textContent = l;
card.appendChild(d);
});
card.addEventListener('click', function () { state.variant = i; renderVariants(); });
grid.appendChild(card);
});
host.appendChild(grid);
var hint = document.createElement('div');
hint.className = 'tf-hint';
hint.textContent = 'Pick the one you want to run, then set your impressions below and launch it.';
host.appendChild(hint);
}
async function writeAds(btn) {
btn.disabled = true;
btn.innerHTML = 'Writing…';
$('tfErr').style.display = 'none';
try {
var r = await fetch('/api/public/suite-textads', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ angle: state.angle })
});
var d = await r.json();
if (!r.ok) {
$('tfErr').textContent = d.error || 'The writer could not produce ads just now.';
$('tfErr').style.display = 'block';
} else {
state.variants = d.variants || [];
state.variant = state.variants.length ? 0 : null;
renderVariants();
if (d.meter) showWriteMeter(d.meter);
}
} catch (e) {
$('tfErr').textContent = 'Connection hiccup — try again.';
$('tfErr').style.display = 'block';
}
btn.disabled = false;
btn.innerHTML = state.variants.length ? '✍️ Write 5 more' : '✍️ Write me 5 ads';
}
function showWriteMeter(m) {
if (!m) return;
$('tfWriteMeter').textContent = m.remaining + ' of ' + m.limit + ' batches left this month';
}
function goLabel() {
if ($('tfGo').disabled && state.remaining <= 0) return;
$('tfGo').textContent = state.format === 'text' ? '🚦 Launch my text ad' : '🚦 Launch my banner';
}
function gate(msg) {
$('tfGate').style.display = 'block';
$('tfGate').innerHTML = msg;
$('tfCard').style.opacity = '.55';
$('tfGo').disabled = true;
}
function renderLive(campaigns, live) {
if (!campaigns.length) { $('tfLiveWrap').style.display = 'none'; return; }
var byId = {};
(live || []).forEach(function (l) { byId[l.ad_id] = l; });
var tb = $('tfLive');
tb.innerHTML = '';
campaigns.slice().reverse().forEach(function (c) {
var l = byId[c.adId] || {};
var tr = document.createElement('tr');
var served = l.served != null ? l.served : 0;
// The ad network's own counters sometimes drift ABOVE what we bought —
// its remaining has been observed at 3,521 on a 2,500 purchase. The cause
// is inside the ionCube-encoded ad app, so we cannot fix it at source; we
// can stop it rendering as nonsense. Clamp to what the member actually
// bought so the row always reconciles: served + left = bought. Any genuine
// over-delivery is a bonus to them and needs no explanation on this table.
var bought = Number(c.bought != null ? c.bought : c.impressions) || 0;
var rawLeft = l.remaining != null ? Number(l.remaining) : null;
var left = rawLeft == null ? null : Math.max(0, Math.min(rawLeft, bought));
var servedShown = left == null ? Number(served) : Math.max(0, bought - left);
var statusCell = c.stopped
? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)'
: (l.live ? 'running' : 'finished');
var label = c.kind === 'text'
? 'Text
' + esc(c.subject || '') + ''
: '' + c.size + '';
tr.innerHTML = '