Traffic Desk ships: signed NAS bridge client, monthly impression allowance by level (2.5k Scintilla -> 150k Corona), team-kit creative only (no member uploads = no moderation risk), local grant ledger, live serve stats; tile moved to L1 since credits are included at every paid level

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 09:50:03 -05:00
parent 6c73b5d9c5
commit f7276589e9
5 changed files with 421 additions and 2 deletions
+160
View File
@@ -0,0 +1,160 @@
// 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 = '';
[['join', 'My invite page'], ['page', 'My personal page (/p/' + state.id + ')']].forEach(function (t) {
host.appendChild(chip(t[1], state.target === t[0], function () {
state.target = t[0]; renderTargets();
}));
});
}
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;
tr.innerHTML = '<td><b>' + c.size + '</b></td>' +
'<td>' + (c.target.indexOf('/p/') !== -1 ? 'personal page' : 'invite page') + '</td>' +
'<td>' + Number(c.impressions).toLocaleString() + '</td>' +
'<td><b>' + Number(served).toLocaleString() + '</b></td>' +
'<td>' + (l.remaining != null ? Number(l.remaining).toLocaleString() : '—') + '</td>' +
'<td>' + (l.hits != null ? l.hits : '—') + '</td>' +
'<td>' + (l.live ? '<span style="color:var(--teal)">running</span>' : 'finished') + '</td>';
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 (!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 <b style="color:var(--text)">' +
Number(st.limit).toLocaleString() + '</b> 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 boot() {
try {
var r = await fetch('/api/public/suite-traffic');
if (r.status === 401) { gate('You’re not signed in yet. <a href="/suite" style="color:var(--gold);font-weight:700">Open the Suite</a> 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. <a href="/suite" style="color:var(--gold);font-weight:700">See your Suite</a>.'); 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 = '<span class="spin"></span>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 = '✅ <b>Your banner is live on the network.</b><br>' +
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);
});
})();