Page Builder: members can hold several pages, scaled by level
The quota and the capability used to contradict each other. An Ascensus member was allowed 3 page builds a month, but every build overwrote the same file — so they could build three times and never have more than one page. The slug plumbing to do better already existed and was only exposed at level 6, which made the Funnel Factory's differentiator a COUNT rather than a KIND, the same flaw the upper tiers had. Now: hold caps by level (2 at Ascensus rising to 50 at Corona), separate from the monthly build quota. Rebuilding spends a build but needs no hold room; adding a new page needs both. The Page Builder is a list of your pages with view / copy link / rebuild / delete, plus a form that ADDS one instead of silently replacing what you had. Two things kept deliberately fixed. The first page a member builds stays at /p/<id> forever and cannot be deleted — that address ends up on banners, printed flyers and in people's DMs, so it must never move. Extra pages take a slug from the headline the writer produced, de-duplicated, so the address describes the page without anyone having to invent one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -736,13 +736,28 @@ async function handleApi(req,res,pathname){
|
||||
if(e.error)return json(res,e.code||500,{error:e.error});
|
||||
if(!e.inOrg||!e.allowed)return json(res,403,{error:'Not available for this position yet.'});
|
||||
const gate=suiteMeter.check(e.d.id,e.d.level,'page');
|
||||
return json(res,200,{page:suitePages.load(e.d.id),meter:gate,angles:suitePages.ANGLES,level:e.d.level,id:e.d.id});
|
||||
const pages=suitePages.list(e.d.id);
|
||||
const cap=suitePages.pageLimit(e.d.level);
|
||||
return json(res,200,{pages:pages,page:suitePages.load(e.d.id),meter:gate,
|
||||
angles:suitePages.ANGLES,level:e.d.level,id:e.d.id,
|
||||
cap:cap,held:pages.length,canAddMore:pages.length<cap});
|
||||
}
|
||||
if(req.method==='POST'&&pathname==='/api/public/suite-page'){
|
||||
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.'});
|
||||
const b=await bodyJson(req)||{};
|
||||
|
||||
// Deleting one of their own pages.
|
||||
if(b.remove!==undefined){
|
||||
const slug=String(b.remove||'');
|
||||
if(!slug)return json(res,400,{error:'The main page cannot be deleted — rebuild it instead.'});
|
||||
const gone=suitePages.remove(e.d.id,slug);
|
||||
return json(res,gone?200:404,gone
|
||||
?{removed:slug,pages:suitePages.list(e.d.id),held:suitePages.list(e.d.id).length,cap:suitePages.pageLimit(e.d.level)}
|
||||
:{error:'That page is not there.'});
|
||||
}
|
||||
|
||||
const input={
|
||||
name:String(b.name||'').trim().slice(0,60),
|
||||
audience:String(b.audience||'').trim().slice(0,300),
|
||||
@@ -750,6 +765,21 @@ async function handleApi(req,res,pathname){
|
||||
angle:suitePages.ANGLES[b.angle]?b.angle:'overview'
|
||||
};
|
||||
if(!suiteAI.configured())return json(res,503,{error:'The Page Builder is warming up — try again shortly.'});
|
||||
|
||||
// Two different limits, and they used to contradict each other: the monthly
|
||||
// BUILD quota is how often the writer may run, the HOLD cap is how many
|
||||
// pages the level lets them keep. Rebuilding an existing page spends a
|
||||
// build but does not need hold room; adding a new one needs both.
|
||||
const existing=suitePages.list(e.d.id);
|
||||
const cap=suitePages.pageLimit(e.d.level);
|
||||
const replacing=b.replace!==undefined?String(b.replace||''):null;
|
||||
const isNew=replacing===null;
|
||||
if(isNew&&existing.length>=cap){
|
||||
return json(res,429,{error:cap<=1
|
||||
? 'Your level allows one page. Rebuild the one you have, or upgrade to hold more.'
|
||||
: 'You are holding all '+cap+' pages your level allows. Delete one, or upgrade to hold more.',
|
||||
cap:cap,held:existing.length});
|
||||
}
|
||||
const gate=suiteMeter.check(e.d.id,e.d.level,'page');
|
||||
if(!gate.allowed){
|
||||
return json(res,429,{error:gate.reason==='locked'
|
||||
@@ -758,9 +788,23 @@ async function handleApi(req,res,pathname){
|
||||
}
|
||||
try{
|
||||
const copy=await suitePages.generate(input);
|
||||
const rec=suitePages.save(e.d.id,{id:e.d.id,name:input.name,angle:input.angle,audience:input.audience,story:input.story,copy:copy,updatedAt:new Date().toISOString()});
|
||||
// The first page a member builds stays at /p/<id> — that address is on
|
||||
// banners, flyers and in people's DMs, so it must never move. Extra pages
|
||||
// get a slug from the headline the writer produced, so the address
|
||||
// describes the page without asking anyone to invent one.
|
||||
const hasMain=existing.some(function(x){return !x.slug});
|
||||
let slug;
|
||||
if(replacing!==null) slug=replacing;
|
||||
else if(!hasMain) slug='';
|
||||
else slug=suitePages.slugFromTitle(e.d.id,copy.headline,input.angle);
|
||||
|
||||
const rec=suitePages.save(e.d.id,{id:e.d.id,slug:slug||undefined,name:input.name,angle:input.angle,
|
||||
audience:input.audience,story:input.story,copy:copy,updatedAt:new Date().toISOString()},slug||undefined);
|
||||
suiteMeter.record(e.d.id,'page',1);
|
||||
return json(res,200,{page:rec,url:'https://rmcircle.team/p/'+e.d.id,meter:suiteMeter.check(e.d.id,e.d.level,'page')});
|
||||
const url='https://rmcircle.team/p/'+e.d.id+(slug?'/'+slug:'');
|
||||
const after=suitePages.list(e.d.id);
|
||||
return json(res,200,{page:rec,url:url,slug:slug||'',pages:after,held:after.length,cap:cap,
|
||||
canAddMore:after.length<cap,meter:suiteMeter.check(e.d.id,e.d.level,'page')});
|
||||
}catch(err){ return json(res,502,{error:String(err.message||err)}); }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user