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
+79
View File
@@ -0,0 +1,79 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="noindex"><meta name="description" content="Traffic Desk — put a team banner on our own ad network, pointed at your invite link. Monthly impressions included with your position."><title>Traffic Desk | The Circle Suite</title><link rel="icon" type="image/png" href="/favicon.png"><link rel="stylesheet" href="/styles.css">
<style>
.tw{max-width:940px;margin:0 auto;padding:24px 20px 70px}
.tw h1{font-size:clamp(26px,4.5vw,36px);margin:6px 0 8px}
.tw .lede{color:var(--muted);line-height:1.6;margin-bottom:18px}
#tfGate{display:none;border:2px solid var(--gold);border-radius:12px;padding:16px 20px;margin-bottom:18px;line-height:1.6}
.tf-bal{display:flex;gap:18px;flex-wrap:wrap;align-items:center;border:2px solid var(--teal);border-radius:14px;padding:14px 18px;margin-bottom:18px}
.tf-bal .big{font-size:30px;font-weight:900;color:var(--gold);font-variant-numeric:tabular-nums;line-height:1}
.tf-bal .lab{font-size:12px;font-weight:800;letter-spacing:1.4px;text-transform:uppercase;color:var(--muted);margin-top:3px}
.tf-bal .note{color:var(--muted);font-size:13px;line-height:1.5;flex:1;min-width:220px}
.tf-card{background:var(--card,#0d2236);border:1px solid var(--line,#24425d);border-radius:14px;padding:18px 20px;margin-bottom:16px}
label.tf-lab{display:block;font-weight:700;font-size:14px;margin:14px 0 6px}
label.tf-lab:first-of-type{margin-top:0}
.tf-chips{display:flex;flex-wrap:wrap;gap:8px}
.tf-chip{border:1px solid var(--line);background:transparent;color:var(--muted);border-radius:999px;
padding:7px 14px;font-size:13.5px;font-weight:700;cursor:pointer}
.tf-chip.on{border-color:var(--teal);background:rgba(78,214,203,.12);color:var(--teal)}
.tf-creatives{display:flex;flex-wrap:wrap;gap:10px;margin-top:4px}
.tf-cre{border:2px solid var(--line);border-radius:10px;padding:6px;cursor:pointer;background:var(--bg,#0b1d2e);line-height:0}
.tf-cre.on{border-color:var(--gold)}
.tf-cre img{max-width:230px;max-height:120px;width:auto;height:auto;display:block}
#tfImp{width:150px;background:var(--bg,#0b1d2e);border:1px solid var(--line);border-radius:10px;color:var(--text);padding:10px 12px;font-size:15px}
.tf-hint{color:var(--muted);font-size:12.5px;margin-top:6px}
.tf-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap;margin-top:16px}
#tfErr{display:none;color:#f0a05a;margin-top:12px;font-size:14px;line-height:1.55}
#tfOk{display:none;border:2px solid var(--teal);border-radius:12px;padding:14px 18px;margin-top:14px;line-height:1.6}
.tf-live{margin-top:22px}
.tf-live table{width:100%;border-collapse:collapse;font-size:14px}
.tf-live th{text-align:left;padding:8px 10px;font-size:11px;letter-spacing:1px;text-transform:uppercase;color:var(--muted);border-bottom:2px solid var(--line)}
.tf-live td{padding:9px 10px;border-bottom:1px solid var(--line);color:var(--muted);font-variant-numeric:tabular-nums}
.tf-live td b{color:var(--text)}
.tf-note{margin-top:22px;font-size:12.5px;color:var(--muted);line-height:1.6;border-top:1px solid var(--line);padding-top:12px}
.spin{display:inline-block;width:13px;height:13px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--gold);
border-radius:50%;animation:sp .8s linear infinite;vertical-align:-2px;margin-right:7px}
@keyframes sp{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spin{animation:none}}
</style></head>
<body>
<header class="wrap nav"><a class="brand" href="/suite"><img class="brand-mark" src="/logo.jpg" alt="The RM Circle" width="42" height="42"><span><span id="brandName">RM Circle</span><small>Traffic Desk</small></span></a><div class="nav-actions"><a class="btn btn-secondary hide-mobile" href="/suite">← The Suite</a><a class="btn btn-primary" href="/my">My Dashboard</a></div></header>
<main class="tw">
<div class="eyebrow" style="color:var(--gold)">CIRCLE SUITE · INCLUDED AT EVERY PAID LEVEL</div>
<h1>Traffic <span class="gold">Desk.</span></h1>
<p class="lede">Put a team banner on our own advertising network, pointed at your invite link. Impressions are included with your position and scale as you level up — nothing to buy, nothing to configure.</p>
<div id="tfGate"></div>
<div class="tf-bal" id="tfBal" style="display:none">
<div><div class="big" id="tfRemain">—</div><div class="lab">impressions left this month</div></div>
<div><div class="big" id="tfLimit" style="color:var(--teal)">—</div><div class="lab">your monthly allowance</div></div>
<div class="note" id="tfNote"></div>
</div>
<div class="tf-card" id="tfCard">
<label class="tf-lab">Banner size</label>
<div class="tf-chips" id="tfSizes"></div>
<label class="tf-lab">Pick a design</label>
<div class="tf-creatives" id="tfCreatives"></div>
<label class="tf-lab">Where should it send people?</label>
<div class="tf-chips" id="tfTargets"></div>
<label class="tf-lab" for="tfImp">How many impressions?</label>
<input type="number" id="tfImp" min="100" step="100" value="2500">
<div class="tf-hint">Comes out of this month's allowance. It refills on the 1st.</div>
<div class="tf-row">
<button id="tfGo" class="btn btn-primary">🚦 Launch my banner</button>
</div>
<div id="tfErr"></div>
<div id="tfOk"></div>
</div>
<div class="tf-live" id="tfLiveWrap" style="display:none">
<h2 style="font-size:20px;margin:0 0 8px">Your running banners</h2>
<div class="table-wrap"><table><thead><tr><th>Size</th><th>Sends to</th><th>Bought</th><th>Served</th><th>Left</th><th>Clicks</th><th>Status</th></tr></thead><tbody id="tfLive"></tbody></table></div>
</div>
<p class="tf-note"><strong style="color:var(--text)">What this is, honestly:</strong> display advertising is <strong style="color:var(--text)">awareness volume</strong> — it puts your link in front of people browsing the network. It is not a lead list, and click rates on display inventory are low by nature. The conversations you have with people you know are still what actually builds a team; this runs quietly in the background while you do that. <br><br>All banners use approved team creative, so every ad on the network stays on-brand. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-traffic.js"></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+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);
});
})();
+1 -1
View File
@@ -18,7 +18,7 @@
{ lv: 3, ico: '📧', name: 'Email Engine', desc: 'Welcome series, follow-up sequences and broadcasts in the team voice — exportable to any autoresponder.', href: '/suite/email', live: true },
{ lv: 3, ico: '🎥', name: 'Video Maker', desc: 'The team’s master promo videos rendered with your personal end-card — your name, your QR, your link.', href: '/suite/video', live: true },
{ lv: 4, ico: '🗣️', name: 'Voice Profile + Funnels', desc: 'Output that sounds like YOU, plus multi-page funnels on your own team subdomain.', live: false },
{ lv: 5, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising: monthly ad credits on the team’s own network, banner and text placements, rotator priority, and AI campaign packs.', live: false },
{ lv: 1, ico: '🚦', name: 'Traffic Desk', desc: 'Syndicated network display advertising — put a banner on the team’s own ad network. Monthly impressions scale with your level: 2,500 at Scintilla up to 150,000 at Corona.', href: '/suite/traffic', live: true },
{ lv: 6, ico: '🏭', name: 'Funnel Factory', desc: 'Complete hosted funnels with A/B variants, replay funnels (your team webinar as an on-demand registration page with a timed CTA), and a lead CRM.', live: false },
{ lv: 7, ico: '🧭', name: 'Leader Ops', desc: 'Team radar, AI coaching digests for your legs, and cohort training rooms.', live: false },
{ lv: 8, ico: '👑', name: 'Founder Desk', desc: 'Your own AI operator running your promotion, API access, and the inner circle.', live: false }