// 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); };
var state = { size: null, creative: null, target: 'join', creatives: {}, remaining: 0, id: null };
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 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;
var statusCell = c.stopped
? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)'
: (l.live ? 'running' : 'finished');
tr.innerHTML = '
' + c.size + ' | ' +
'' + (c.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + ' | ' +
'' + Number(c.bought != null ? c.bought : c.impressions).toLocaleString() + ' | ' +
'' + Number(c.stopped ? (c.served || 0) : served).toLocaleString() + ' | ' +
'' + (c.stopped ? '—' : (l.remaining != null ? Number(l.remaining).toLocaleString() : '—')) + ' | ' +
'' + (l.hits != null ? l.hits : '—') + ' | ' +
'' + statusCell + ' | ';
var act = document.createElement('td');
if (!c.stopped && l.live) {
var btn = document.createElement('button');
btn.className = 'btn btn-secondary btn-sm';
btn.textContent = 'Stop';
btn.title = 'Stop this banner and return the unserved impressions to your balance';
btn.addEventListener('click', function () { stopCampaign(c.adId, btn); });
act.appendChild(btn);
}
tr.appendChild(act);
tb.appendChild(tr);
});
$('tfLiveWrap').style.display = 'block';
}
function applyStatus(d) {
var st = d.status;
state.creatives = st.creatives || {};
state.id = d.id;
state.remaining = st.remaining;
if (typeof d.hasPage === 'boolean') state.hasPage = d.hasPage;
if (!state.hasPage && state.target === 'page') state.target = 'join';
if (!state.size) state.size = (st.sizes || [])[0] || null;
if (!state.creative) state.creative = (state.creatives[state.size] || [])[0] || null;
$('tfBal').style.display = 'flex';
$('tfRemain').textContent = Number(st.remaining).toLocaleString();
$('tfLimit').textContent = Number(st.limit).toLocaleString();
$('tfNote').innerHTML = 'Level ' + d.level + ' includes ' +
Number(st.limit).toLocaleString() + ' impressions a month on the network. Each upgrade raises it — Apex is where it jumps to 50,000.';
$('tfImp').max = st.remaining;
$('tfImp').value = Math.min(Number($('tfImp').value) || st.remaining, st.remaining) || 100;
renderSizes(st.sizes || []); renderCreatives(); renderTargets();
renderLive(st.campaigns || [], d.live || []);
if (st.remaining <= 0) {
$('tfGo').disabled = true;
$('tfGo').textContent = 'Allowance used — refills on the 1st';
}
}
async function stopCampaign(adId, btn) {
if (btn) { btn.disabled = true; btn.textContent = 'Stopping…'; }
try {
var r = await fetch('/api/public/suite-traffic-stop', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ad_id: adId })
});
var d = await r.json();
if (!r.ok) {
$('tfErr').textContent = d.error || 'Could not stop that banner.';
$('tfErr').style.display = 'block';
if (btn) { btn.disabled = false; btn.textContent = 'Stop'; }
return;
}
$('tfOk').innerHTML = '✅ Banner stopped. ' +
Number(d.stopped.refunded).toLocaleString() + ' unserved impressions went back into your balance' +
(d.stopped.served ? ' — it had served ' + Number(d.stopped.served).toLocaleString() + '.' : '.');
$('tfOk').style.display = 'block';
boot();
} catch (e) {
$('tfErr').textContent = 'Connection hiccup — try again.';
$('tfErr').style.display = 'block';
if (btn) { btn.disabled = false; btn.textContent = 'Stop'; }
}
}
async function boot() {
try {
var r = await fetch('/api/public/suite-traffic');
if (r.status === 401) { gate('You’re not signed in yet. Open the Suite and connect the wallet that holds your position, then come back.'); return; }
if (r.status === 403) { gate('The Traffic Desk isn’t open for this position yet. See your Suite.'); return; }
if (!r.ok) return;
var d = await r.json();
if (!d.configured) { gate('The ad network bridge is being finalized — this opens shortly.'); return; }
applyStatus(d);
} catch (e) {}
}
async function run() {
if (busy) return;
busy = true;
$('tfErr').style.display = 'none';
$('tfOk').style.display = 'none';
$('tfGo').disabled = true;
$('tfGo').innerHTML = 'Launching on the network…';
try {
var r = await fetch('/api/public/suite-traffic', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
size: state.size, creative: state.creative, target: state.target,
impressions: Number($('tfImp').value) || 0
})
});
var d = await r.json();
if (!r.ok) {
$('tfErr').textContent = d.error || 'That didn’t go through — try again.';
$('tfErr').style.display = 'block';
} else {
$('tfOk').innerHTML = '✅ Your banner is live on the network.
' +
Number(d.campaign.impressions).toLocaleString() + ' impressions of ' + d.campaign.size +
' pointed at your ' + (d.campaign.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') +
'. It starts rotating immediately — check back here to watch it serve.';
$('tfOk').style.display = 'block';
applyStatus({ status: d.status, level: 0, id: state.id, live: [], configured: true });
// refresh from the server so the running-banners table picks it up
setTimeout(boot, 800);
}
} catch (e) {
$('tfErr').textContent = 'Connection hiccup — try again.';
$('tfErr').style.display = 'block';
}
busy = false;
if (state.remaining > 0) { $('tfGo').disabled = false; $('tfGo').textContent = '🚦 Launch my banner'; }
}
document.addEventListener('DOMContentLoaded', function () {
boot();
$('tfGo').addEventListener('click', run);
});
})();