3f8d23d66c
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>
129 lines
6.0 KiB
JavaScript
129 lines
6.0 KiB
JavaScript
// Leader Ops client — renders the coaching radar for the signed-in leader's own
|
||
// organisation, then asks the engine for a written plan on request.
|
||
(function () {
|
||
'use strict';
|
||
var $ = function (id) { return document.getElementById(id); };
|
||
function esc(s) {
|
||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||
});
|
||
}
|
||
function n(v) { return Number(v || 0).toLocaleString(); }
|
||
|
||
function gate(msg) {
|
||
$('lGate').style.display = 'block';
|
||
$('lGate').innerHTML = msg;
|
||
}
|
||
|
||
function tile(value, label, cls) {
|
||
return '<div class="l-tile ' + (cls || '') + '"><div class="big">' + value + '</div>' +
|
||
'<div class="lab">' + label + '</div></div>';
|
||
}
|
||
|
||
function renderScan(scan) {
|
||
if (!scan || !scan.ready) {
|
||
gate('The on-chain index is still catching up. Give it a moment and refresh.');
|
||
return;
|
||
}
|
||
$('lBody').style.display = 'block';
|
||
var t = scan.totals || {};
|
||
|
||
$('lTiles').innerHTML =
|
||
tile(n(scan.scanned), 'positions below you', 'cool') +
|
||
tile(n(t.atRiskPol), 'POL forming that someone below cannot catch', t.atRiskPol > 0 ? 'hot' : '') +
|
||
tile(n(t.oneAwayCount), 'one direct short of qualifying', t.oneAwayCount > 0 ? 'hot' : '') +
|
||
tile(n(t.rollForwardCount), 'qualified but still on Scintilla', '');
|
||
|
||
var out = [];
|
||
|
||
if ((scan.atRisk || []).length) {
|
||
var rows = scan.atRisk.map(function (r) {
|
||
return '<tr><td><b>#' + esc(r.id) + '</b></td>' +
|
||
'<td>' + esc(r.levelName) + '</td>' +
|
||
'<td>' + (r.qualified
|
||
? '<span class="pill u">upgrade to ' + esc(r.neededLevelName || '') + '</span>'
|
||
: '<span class="pill q">' + esc(r.directCount) + ' of 2 directs</span>') + '</td>' +
|
||
'<td><b>' + n(r.atRiskPol) + '</b></td>' +
|
||
'<td>' + n(r.earnedPol) + '</td>' +
|
||
'<td>' + (r.upgradeCost ? n(r.upgradeCost) : '—') + '</td></tr>';
|
||
}).join('');
|
||
out.push('<div class="l-sec"><h2>Money forming they can\'t catch</h2>' +
|
||
'<div class="why">A position only catches an upgrade payment if it is qualified <em>and</em> already at or above the level being bought. These people have activity underneath them that is passing them by — the fastest, kindest conversation you can have this week.</div>' +
|
||
'<div class="table-wrap"><table class="l-tbl"><thead><tr><th>Position</th><th>Level</th><th>What they need</th><th>POL passing them</th><th>Earned</th><th>Upgrade cost</th></tr></thead><tbody>' +
|
||
rows + '</tbody></table></div></div>');
|
||
}
|
||
|
||
if ((scan.rollForward || []).length) {
|
||
var rf = scan.rollForward.map(function (r) {
|
||
return '<tr><td><b>#' + esc(r.id) + '</b></td><td>' + n(r.earnedPol) + '</td><td>' + n(r.ascensusCost) + '</td></tr>';
|
||
}).join('');
|
||
out.push('<div class="l-sec"><h2>Already qualified, still on Scintilla</h2>' +
|
||
'<div class="why">They have their two and their entry rewards are sitting there. Ascensus is the upgrade that starts catching their directs\' payments — for several of these, what they have already earned covers it.</div>' +
|
||
'<div class="table-wrap"><table class="l-tbl"><thead><tr><th>Position</th><th>Earned (POL)</th><th>Ascensus costs</th></tr></thead><tbody>' +
|
||
rf + '</tbody></table></div></div>');
|
||
}
|
||
|
||
if ((scan.oneAway || []).length) {
|
||
out.push('<div class="l-sec"><h2>One direct short</h2>' +
|
||
'<div class="why">A single placement qualifies each of these. If you have anyone coming in, this is where to put them.</div>' +
|
||
'<div style="font-size:15px;line-height:1.9">' +
|
||
scan.oneAway.map(function (r) { return '<b>#' + esc(r.id) + '</b> <span style="color:var(--muted);font-size:13px">(' + esc(r.levelName) + ')</span>'; }).join(' · ') +
|
||
'</div></div>');
|
||
}
|
||
|
||
if (!out.length) {
|
||
out.push('<div class="l-sec"><h2>Nothing needs chasing</h2>' +
|
||
'<div class="why">Nobody below you is one direct short, sitting on uncatchable money, or qualified but stuck on Scintilla. That is the quiet week you spend working on your own two.</div></div>');
|
||
}
|
||
|
||
$('lSections').innerHTML = out.join('');
|
||
}
|
||
|
||
async function writePlan(btn) {
|
||
btn.disabled = true;
|
||
btn.innerHTML = '<span class="spin"></span>Reading your organisation…';
|
||
$('lErr').style.display = 'none';
|
||
try {
|
||
var r = await fetch('/api/public/suite-leader', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' });
|
||
var d = await r.json();
|
||
if (!r.ok) {
|
||
$('lErr').textContent = d.error || 'Could not write the plan just now.';
|
||
$('lErr').style.display = 'block';
|
||
} else {
|
||
$('lDigest').textContent = d.text || '';
|
||
$('lDigest').style.display = 'block';
|
||
if (d.scan) renderScan(d.scan);
|
||
}
|
||
} catch (e) {
|
||
$('lErr').textContent = 'Connection hiccup — try again.';
|
||
$('lErr').style.display = 'block';
|
||
}
|
||
btn.disabled = false;
|
||
btn.innerHTML = '🧭 Write this week\'s coaching plan';
|
||
}
|
||
|
||
async function boot() {
|
||
try {
|
||
var r = await fetch('/api/public/suite-leader');
|
||
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) {
|
||
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();
|
||
renderScan(d.scan);
|
||
if (d.writerReady === false) {
|
||
$('lWrite').disabled = true;
|
||
$('lWrite').textContent = 'The writer is warming up';
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
boot();
|
||
$('lWrite').addEventListener('click', function () { writePlan(this); });
|
||
});
|
||
})();
|