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>
This commit is contained in:
martbost
2026-08-28 10:49:36 -05:00
parent 8380696574
commit 3f8d23d66c
17 changed files with 1525 additions and 22 deletions
+120 -3
View File
@@ -25,6 +25,9 @@ const suiteEmail = require('./suite-email');
const suiteVideo = require('./suite-video'); suiteVideo.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR });
const suiteTraffic = require('./suite-traffic'); suiteTraffic.init({ dataDir: DATA_DIR });
const suiteTextAds = require('./suite-textads');
const suiteVoice = require('./suite-voice'); suiteVoice.init({ dataDir: DATA_DIR });
const suiteSplit = require('./suite-split'); suiteSplit.init({ dataDir: DATA_DIR });
const suiteLeader = require('./suite-leader');
const tgbot = require('./tgbot');
tgbot.init({ dataDir: DATA_DIR, chain, getConfig, messages, baseUrl: 'https://rmcircle.team' });
const SESSION_TTL = 8 * 60 * 60 * 1000;
@@ -669,6 +672,21 @@ async function handleApi(req,res,pathname){
}
// ── Page Builder ─────────────────────────────────────────────────────────
// Level 6 extra pages: /p/<id>/<slug>. Falls back to the member's main page
// rather than 404ing, for the same reason /p/<id> falls back to /join/<id>.
if(req.method==='GET'&&/^\/p\/\d{1,15}\/[a-z0-9-]{1,24}$/.test(pathname)){
const parts=pathname.split('/');
const pid=parts[2], slug=parts[3];
const rec=suitePages.load(pid,slug);
if(!rec||!rec.copy){
res.writeHead(302,securityHeaders({'Location':'/p/'+pid,'Cache-Control':'no-store'}));
return res.end();
}
const html=suitePages.render(rec);
res.writeHead(200,securityHeaders({'Content-Type':'text/html; charset=utf-8','Cache-Control':'public, max-age=120'}));
return res.end(html);
}
if(req.method==='GET'&&/^\/p\/\d{1,15}$/.test(pathname)){
const pid=pathname.split('/')[2];
const rec=suitePages.load(pid);
@@ -736,7 +754,7 @@ async function handleApi(req,res,pathname){
}
try{
const link='https://rmcircle.team/join/'+e.d.id;
const emails=await suiteEmail.generate(kind,brief,{link:link,id:e.d.id});
const emails=await suiteEmail.generate(kind,brief,{link:link,id:e.d.id,voice:suiteVoice.promptBlock(e.d.id)});
suiteMeter.record(e.d.id,'email',emails.length||1);
return json(res,200,{emails:emails,meter:suiteMeter.check(e.d.id,e.d.level,'email')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
@@ -825,6 +843,105 @@ async function handleApi(req,res,pathname){
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
// -- Voice Profile (level 4) ----------------------------------------------
if(pathname==='/api/public/suite-voice'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<4)return json(res,403,{error:'The Voice Profile unlocks at Culmen (level 4).',minLevel:4});
if(req.method==='GET'){
return json(res,200,{profile:suiteVoice.load(e.d.id)||suiteVoice.blank(),fields:suiteVoice.FIELDS,level:e.d.level,id:e.d.id});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.clear){ suiteVoice.clear(e.d.id); return json(res,200,{profile:suiteVoice.blank(),cleared:true}); }
return json(res,200,{profile:suiteVoice.save(e.d.id,b),saved:true});
}
}
// -- Split Tester (level 5) -----------------------------------------------
if(pathname==='/api/public/suite-split'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<suiteSplit.MIN_LEVEL)return json(res,403,{error:'The Split Tester unlocks at Apex (level 5).',minLevel:suiteSplit.MIN_LEVEL});
if(req.method==='GET'){
let r=[]; try{ r=await suiteSplit.results(e.d.id); }catch(err){}
const pg=suitePages.load(e.d.id);
return json(res,200,{tests:r,level:e.d.level,id:e.d.id,
traffic:suiteTraffic.status(e.d.id,e.d.level),hasPage:!!(pg&&pg.copy),
minPerArm:suiteSplit.MIN_PER_ARM,maxArms:suiteSplit.MAX_ARMS,
textAngles:suiteTextAds.angles(),
textMeter:suiteMeter.check(e.d.id,e.d.level,'textad')});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.stop){
try{ return json(res,200,{stopped:await suiteSplit.stop(e.d.id,String(b.stop))}); }
catch(err){ return json(res,400,{error:String(err.message||err)}); }
}
const pg=suitePages.load(e.d.id);
try{
const t=await suiteSplit.launch({id:e.d.id,level:e.d.level,arms:b.arms,
impressions:b.impressions,target:b.target,angle:b.angle,hasPage:!!(pg&&pg.copy)});
return json(res,200,{test:t,traffic:suiteTraffic.status(e.d.id,e.d.level)});
}catch(err){ return json(res,400,{error:String(err.message||err)}); }
}
}
// -- Funnel Factory: extra pages (level 6) --------------------------------
if(pathname==='/api/public/suite-funnel'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<6)return json(res,403,{error:'The Funnel Factory unlocks at Fastigium (level 6).',minLevel:6});
if(req.method==='GET'){
return json(res,200,{pages:suitePages.list(e.d.id),angles:suitePages.ANGLES,level:e.d.level,id:e.d.id,
meter:suiteMeter.check(e.d.id,e.d.level,'page')});
}
if(req.method==='POST'){
const b=await bodyJson(req)||{};
if(b.remove){
const gone=suitePages.remove(e.d.id,String(b.remove));
return json(res,gone?200:404,gone?{removed:b.remove,pages:suitePages.list(e.d.id)}:{error:'That page is not there.'});
}
const slug=suitePages.slugify(b.slug||b.name||'');
if(!slug)return json(res,400,{error:'Give the page a short name.'});
const gate=suiteMeter.check(e.d.id,e.d.level,'page');
if(!gate.allowed)return json(res,429,{error:'You have used all '+gate.limit+' page builds this month. It resets on the 1st.',meter:gate});
const input={id:e.d.id,name:String(b.name||'').slice(0,80),audience:String(b.audience||'').slice(0,400),
story:String(b.story||'').slice(0,900),angle:suitePages.ANGLES[b.angle]?b.angle:'overview'};
try{
const copy=await suitePages.generate(input);
suitePages.save(e.d.id,{id:e.d.id,slug:slug,name:input.name,angle:input.angle,audience:input.audience,
story:input.story,copy:copy,updatedAt:new Date().toISOString()},slug);
suiteMeter.record(e.d.id,'page',1);
return json(res,200,{pages:suitePages.list(e.d.id),url:'https://rmcircle.team/p/'+e.d.id+'/'+slug,
meter:suiteMeter.check(e.d.id,e.d.level,'page')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
}
// -- Leader Ops (level 7) -------------------------------------------------
if(pathname==='/api/public/suite-leader'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup - try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
if(!e.inOrg||!e.allowed)return json(res,403,{error:'The Circle Suite is not open for this position yet.'});
if(Number(e.d.level)<suiteLeader.MIN_LEVEL)return json(res,403,{error:'Leader Ops unlocks at Vertex (level 7).',minLevel:suiteLeader.MIN_LEVEL});
if(req.method==='GET'){
return json(res,200,{scan:suiteLeader.scan(e.d.id,12),level:e.d.level,id:e.d.id,writerReady:suiteAI.configured()});
}
if(req.method==='POST'){
const gate=suiteMeter.check(e.d.id,e.d.level,'copy');
if(!gate.allowed)return json(res,429,{error:'That is this month of generation allowance used up.',meter:gate});
try{
const r=await suiteLeader.digest(e.d.id,{maxItems:12});
suiteMeter.record(e.d.id,'copy',1);
return json(res,200,{scan:r.scan,text:r.text,meter:suiteMeter.check(e.d.id,e.d.level,'copy')});
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
}
}
if(req.method==='POST'&&pathname==='/api/public/suite-traffic-stop'){
const e=await suiteEntitlement(req).catch(()=>({error:'Chain read hiccup — try again.',code:500}));
if(e.error)return json(res,e.code||500,{error:e.error});
@@ -861,7 +978,7 @@ async function handleApi(req,res,pathname){
}
const link='https://rmcircle.team/join/'+e.d.id;
try{
const text=await suiteAI.generate(kind,brief,{link:link,id:e.d.id});
const text=await suiteAI.generate(kind,brief,{link:link,id:e.d.id,voice:suiteVoice.promptBlock(e.d.id)});
suiteMeter.record(e.d.id,'copy',1);
return json(res,200,{text:text,meter:suiteMeter.check(e.d.id,e.d.level,'copy')});
}catch(err){
@@ -1169,7 +1286,7 @@ const server=http.createServer(async(req,res)=>{
if((mj=pathname.match(/^\/join\/(\d{1,15})$/)))return serveMemberPage(req,res,path.join(PUBLIC_DIR,'join.html'),'join',mj[1]);
}
let file;
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.html');else if(pathname==='/app'||pathname==='/app/')file=path.join(PUBLIC_DIR,'app.html');else if(pathname==='/start'||pathname==='/start/')file=path.join(PUBLIC_DIR,'start.html');else if(pathname==='/training'||pathname==='/training/')file=path.join(PUBLIC_DIR,'training.html');else if(pathname==='/admin'||pathname==='/admin/')file=path.join(PUBLIC_DIR,'admin.html');else if(pathname==='/my'||pathname==='/my/'||/^\/my\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'my.html');else if(pathname==='/contract'||pathname==='/contract/')file=path.join(PUBLIC_DIR,'contract.html');else if(pathname==='/disclaimer'||pathname==='/disclaimer/')file=path.join(PUBLIC_DIR,'disclaimer.html');else if(pathname==='/privacy'||pathname==='/privacy/')file=path.join(PUBLIC_DIR,'privacy.html');else if(pathname==='/refunds'||pathname==='/refunds/')file=path.join(PUBLIC_DIR,'refunds.html');else if(pathname==='/how-pay-works'||pathname==='/how-pay-works/')file=path.join(PUBLIC_DIR,'how-pay-works.html');else if(pathname==='/tools'||pathname==='/tools/')file=path.join(PUBLIC_DIR,'tools.html');else if(pathname==='/fast-start'||pathname==='/fast-start/')file=path.join(PUBLIC_DIR,'fast-start.html');else if(pathname==='/flyers'||pathname==='/flyers/')file=path.join(PUBLIC_DIR,'flyers.html');else if(pathname==='/weekly-rhythm'||pathname==='/weekly-rhythm/')file=path.join(PUBLIC_DIR,'weekly-rhythm.html');else if(pathname==='/generation-pay'||pathname==='/generation-pay/')file=path.join(PUBLIC_DIR,'generation-pay.html');else if(pathname==='/suite'||pathname==='/suite/')file=path.join(PUBLIC_DIR,'suite.html');else if(pathname==='/suite/copy'||pathname==='/suite/copy/')file=path.join(PUBLIC_DIR,'suite-copy.html');else if(pathname==='/suite/page'||pathname==='/suite/page/')file=path.join(PUBLIC_DIR,'suite-page.html');else if(pathname==='/suite/email'||pathname==='/suite/email/')file=path.join(PUBLIC_DIR,'suite-email.html');else if(pathname==='/suite/video'||pathname==='/suite/video/')file=path.join(PUBLIC_DIR,'suite-video.html');else if(pathname==='/suite/traffic'||pathname==='/suite/traffic/')file=path.join(PUBLIC_DIR,'suite-traffic.html');else if(pathname==='/suite/voice'||pathname==='/suite/voice/')file=path.join(PUBLIC_DIR,'suite-voice.html');else if(pathname==='/suite/split'||pathname==='/suite/split/')file=path.join(PUBLIC_DIR,'suite-split.html');else if(pathname==='/suite/funnel'||pathname==='/suite/funnel/')file=path.join(PUBLIC_DIR,'suite-funnel.html');else if(pathname==='/suite/leader'||pathname==='/suite/leader/')file=path.join(PUBLIC_DIR,'suite-leader.html');else if(pathname==='/direct-join'||pathname==='/direct-join/'){if(!getSession(req)){res.writeHead(302,{Location:'/admin'});return res.end();}file=path.join(ROOT,'private','direct-join.html');}else if(pathname==='/join-now'||pathname==='/join-now/'){if(!getConfig().dappFallbackPublic){res.writeHead(302,{Location:'/start'});return res.end();}file=path.join(ROOT,'private','join-now.html');}else if(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/join-now'});return res.end();}else{
const safe=path.normalize(pathname).replace(/^([.][.][/\\])+/, '').replace(/^[/\\]+/,'');file=path.join(PUBLIC_DIR,safe);if(!file.startsWith(PUBLIC_DIR))file='';
}
if(file&&staticFile(req,res,file))return;return staticFile(req,res,path.join(PUBLIC_DIR,'404.html'),404);