Files
rm-circle-team-router/public/suite-funnel.js
T
martbost 3f8d23d66c Circle Suite: complete the ladder — every paid level now unlocks live software
Four new tools, so no tier is a placeholder any more:

L4 Voice Profile (/suite/voice) — five short answers that ride along with
every Copy Engine and Email Engine generation. It sits BEFORE the compliance
block in the prompt on purpose: a personal voice must never be able to talk
the model out of the honesty rules. suite-email builds its own prompt, so the
profile is threaded in there separately or email would keep sounding generic
while the Copy Engine sounded like the member.

L5 Split Tester (/suite/split) — 2-3 variants launched as one test with the
impressions split evenly, then click counters compared. Deliberately
conservative: it will not declare a winner until both arms have real volume
AND the leader is clearly ahead, because a 3-click gap on 400 impressions is
noise. A genuine tie is reported as a tie, which is useful information too.
A failed arm rolls back the whole test rather than leaving half of it running.
Apex is the right home for it — that is where impressions jump to 50,000.

L6 Funnel Factory (/suite/funnel) — extra named pages at /p/<id>/<slug>, so a
leader can run one page per audience and point different ads at each. Missing
slugs fall back to the member's main page, same no-dead-ends rule as /p/<id>.

L7 Leader Ops (/suite/leader) — the coaching radar already existed in chain.js
and only admin could see it, which was backwards: the leaders running those
legs need it more than anyone. Now scoped to the member's own organisation,
with a written plan built on contract-read facts only. Follows the
teach-forward rule — the digest gives the leader words to hand down, not just
numbers to act on.

Tile copy across the wall and the payout alerts now describes what actually
exists rather than what was sketched. suite-tools.js was stale in both
directions (it still had the Traffic Desk at L5 and L3 not live).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-28 10:49:36 -05:00

160 lines
6.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Funnel Factory client — lists the member's pages and builds new named ones.
(function () {
'use strict';
var $ = function (id) { return document.getElementById(id); };
function esc(s) {
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
});
}
var state = { angles: {}, angle: 'overview', id: null, pages: [] };
function gate(msg) {
$('fGate').style.display = 'block';
$('fGate').innerHTML = msg;
}
function renderAngles() {
var host = $('fAngles');
host.innerHTML = '';
Object.keys(state.angles).forEach(function (k) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'f-chip' + (state.angle === k ? ' on' : '');
b.textContent = state.angles[k].label || k;
b.addEventListener('click', function () { state.angle = k; renderAngles(); });
host.appendChild(b);
});
}
function renderPages() {
var host = $('fPages');
host.innerHTML = '';
if (!state.pages.length) {
host.innerHTML = '<div class="f-hint">You have no pages yet. Build your main one in the ' +
'<a href="/suite/page" style="color:var(--gold);font-weight:700">Page Builder</a> first, then add more here.</div>';
return;
}
state.pages.forEach(function (p) {
var row = document.createElement('div');
row.className = 'f-page' + (p.slug ? '' : ' main');
var left = document.createElement('div');
left.style.minWidth = '210px';
left.innerHTML = '<div class="nm">' + esc(p.name || 'Page') +
(p.slug ? '' : ' <span style="color:var(--gold);font-size:11px;font-weight:900">MAIN</span>') + '</div>' +
'<div class="u">rmcircle.team' + esc(p.url) + '</div>' +
'<div class="meta">' + esc((state.angles[p.angle] && state.angles[p.angle].label) || p.angle || '') +
(p.updatedAt ? ' · updated ' + esc(String(p.updatedAt).slice(0, 10)) : '') + '</div>';
row.appendChild(left);
var acts = document.createElement('div');
acts.className = 'f-acts';
var view = document.createElement('a');
view.className = 'btn btn-secondary btn-sm';
view.href = p.url; view.target = '_blank'; view.rel = 'noopener';
view.textContent = 'View';
acts.appendChild(view);
var copy = document.createElement('button');
copy.className = 'btn btn-secondary btn-sm';
copy.textContent = 'Copy link';
copy.addEventListener('click', function () {
var full = 'https://rmcircle.team' + p.url;
if (navigator.clipboard) navigator.clipboard.writeText(full);
copy.textContent = 'Copied';
setTimeout(function () { copy.textContent = 'Copy link'; }, 1400);
});
acts.appendChild(copy);
if (p.slug) {
var del = document.createElement('button');
del.className = 'btn btn-secondary btn-sm';
del.textContent = 'Delete';
del.addEventListener('click', function () { removePage(p.slug, p.name, del); });
acts.appendChild(del);
}
row.appendChild(acts);
host.appendChild(row);
});
}
async function removePage(slug, name, btn) {
if (!window.confirm('Delete "' + name + '"? Anything already pointing at that address will fall back to your main page.')) return;
btn.disabled = true;
try {
var r = await fetch('/api/public/suite-funnel', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ remove: slug })
});
var d = await r.json();
if (r.ok) { state.pages = d.pages || []; renderPages(); }
else { btn.disabled = false; }
} catch (e) { btn.disabled = false; }
}
async function build() {
$('fErr').style.display = 'none';
$('fOk').style.display = 'none';
var name = $('fName').value.trim();
if (name.length < 2) {
$('fErr').textContent = 'Give the page a short name first.';
$('fErr').style.display = 'block';
return;
}
$('fGo').disabled = true;
$('fGo').innerHTML = '<span class="spin"></span>Writing your page…';
try {
var r = await fetch('/api/public/suite-funnel', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: name, angle: state.angle,
audience: $('fAud').value.trim(), story: $('fStory').value.trim()
})
});
var d = await r.json();
if (!r.ok) {
$('fErr').textContent = d.error || 'That did not build — try again.';
$('fErr').style.display = 'block';
} else {
state.pages = d.pages || [];
renderPages();
if (d.meter) $('fMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' page builds left this month';
$('fOk').innerHTML = '✅ <b>Built.</b> It is live at <a href="' + esc(d.url) + '" target="_blank" rel="noopener" style="color:var(--gold);font-weight:700">' + esc(d.url) + '</a>';
$('fOk').style.display = 'block';
$('fName').value = ''; $('fAud').value = ''; $('fStory').value = '';
}
} catch (e) {
$('fErr').textContent = 'Connection hiccup — try again.';
$('fErr').style.display = 'block';
}
$('fGo').disabled = false;
$('fGo').innerHTML = '🏗️ Build this page';
}
async function boot() {
try {
var r = await fetch('/api/public/suite-funnel');
if (r.status === 401) { gate('Youre 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) {
var g = await r.json();
gate((g.error || 'Not 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();
state.angles = d.angles || {};
state.pages = d.pages || [];
state.id = d.id;
$('fIdA').textContent = d.id;
if (d.meter) $('fMeter').textContent = d.meter.remaining + ' of ' + d.meter.limit + ' page builds left this month';
$('fBody').style.display = 'block';
renderAngles(); renderPages();
} catch (e) {}
}
document.addEventListener('DOMContentLoaded', function () {
boot();
$('fGo').addEventListener('click', build);
});
})();