// 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. // The network counts `remaining` DOWN for text ads and UP for banners — // measured, not documented. Reading both the same way showed every banner // as 0 served while it was quietly delivering thousands. var bought = Number(c.bought != null ? c.bought : c.impressions) || 0; var rawRem = l.remaining != null ? Number(l.remaining) : null; var servedShown, left; if (rawRem == null) { servedShown = Number(served) || 0; left = null; } else if (c.kind === 'text') { left = Math.max(0, Math.min(rawRem, bought)); servedShown = bought - left; } else { servedShown = Math.max(0, Math.min(rawRem, bought)); left = bought - servedShown; } // "running" with 0 left reads as broken. It is not: banners have no cap on // this network — the counter runs past the purchase and the ad keeps // serving until its expiry, so a fully-delivered banner is genuinely // still going. Say that plainly, and keep Stop available so a member can // actually end it rather than wonder why it never finishes. var delivered = servedShown >= bought && bought > 0; var statusCell = c.completed ? 'delivered in full' : c.stopped ? 'stopped (' + Number(c.refunded || 0).toLocaleString() + ' returned)' : !l.live ? 'finished' : delivered ? 'delivered' + '
served in full — closing out' : 'running'; var label = c.kind === 'text' ? 'Text
' + esc(c.subject || '') + '' : '' + c.size + ''; tr.innerHTML = '' + label + '' + '' + (c.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + '' + '' + Number(c.bought != null ? c.bought : c.impressions).toLocaleString() + '' + '' + Number(c.stopped ? (c.served || 0) : servedShown).toLocaleString() + '' + '' + (c.stopped ? '—' : (left != null ? left.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; state.angles = d.textAngles || state.angles; renderFormat(); renderAngles(); renderVariants(); showWriteMeter(d.textMeter); if (d.writerReady === false) { $('tfWrite').disabled = true; $('tfWriteMeter').textContent = 'The writer is warming up.'; } renderSizes(st.sizes || []); renderCreatives(); renderTargets(); goLabel(); 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…'; var payload = { target: state.target, impressions: Number($('tfImp').value) || 0 }; if (state.format === 'text') { var v = state.variants[state.variant]; if (!v) { $('tfErr').textContent = 'Write some ads first, then pick the one you want to run.'; $('tfErr').style.display = 'block'; busy = false; $('tfGo').disabled = false; goLabel(); return; } payload.kind = 'text'; payload.subject = v.subject; payload.lines = v.lines; } else { payload.kind = 'banner'; payload.size = state.size; payload.creative = state.creative; } try { var r = await fetch('/api/public/suite-traffic', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); var d = await r.json(); if (!r.ok) { $('tfErr').textContent = d.error || 'That didn’t go through — try again.'; $('tfErr').style.display = 'block'; } else { var what = d.campaign.kind === 'text' ? 'text ad' : 'banner'; $('tfOk').innerHTML = '✅ Your ' + what + ' is live on the network.
' + Number(d.campaign.impressions).toLocaleString() + ' impressions of ' + (d.campaign.kind === 'text' ? 'your text ad' : 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; goLabel(); } } document.addEventListener('DOMContentLoaded', function () { boot(); $('tfGo').addEventListener('click', run); $('tfWrite').addEventListener('click', function () { writeAds(this); }); }); })();