Personal invite pages (/join/<id>) + auto-count queue directs from the chain

Prospects who clicked a member's shared /my link landed on a bare stats
dashboard with no explanation of the program. New /join/<id> page carries the
full bridge-page story (video, strategy, matrix, live on-chain payout proof)
personalized to one sponsor: their live position stats as proof, their ID
pinned on the dApp join button, and the submit-your-ID form locked to them
(source invite-<id>). Dashboard share buttons + QR codes now hand out the
invite page; /my stays the member dashboard. Invalid/unregistered invite ids
fall back to the /start rotation. Tracked as new 'join' event with an admin
traffic column.

Also: the chain indexer now auto-increments a rotation sponsor's directs when
a registration names them as referrer (same op as the admin ⊕), announcing it
on Telegram. At 2/2 it alerts to Qualify instead of auto-rotating — moving
the team focus stays a human decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-14 08:18:30 -05:00
parent 532cb18b68
commit 29e0d6e791
7 changed files with 148 additions and 11 deletions
+19 -2
View File
@@ -225,7 +225,7 @@ function getConfig() { return readJson(CONFIG_FILE); }
function activeSponsor(sponsors) { return sponsors.find(s=>s.status==='active') || sponsors.find(s=>s.status==='waiting') || null; }
function getAnalytics() { try { return readJson(ANALYTICS_FILE); } catch (e) { return { sources: {} }; } }
function recordEvent(event, source) {
if (!['bridge','start','click','training','postback','purchase'].includes(event)) return;
if (!['bridge','start','click','training','postback','purchase','join'].includes(event)) return;
const s = String(source||'').toLowerCase().trim().replace(/[^a-z0-9.()\-_:/ ]/g,'').slice(0,80) || '(direct)';
const a = getAnalytics(); if (!a.sources) a.sources = {};
if (!a.sources[s]) { if (Object.keys(a.sources).length >= 500) return; a.sources[s] = { bridge:0, start:0, click:0 }; }
@@ -382,7 +382,7 @@ const server=http.createServer(async(req,res)=>{
if(pathname==='/health'||pathname.startsWith('/api/'))return await handleApi(req,res,pathname);
if(req.method!=='GET'&&req.method!=='HEAD')return send(res,405,'Method Not Allowed',{'Content-Type':'text/plain; charset=utf-8'});
let file;
if(pathname==='/')file=path.join(PUBLIC_DIR,'index.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==='/')file=path.join(PUBLIC_DIR,'index.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(/^\/join\/\d{1,15}$/.test(pathname))file=path.join(PUBLIC_DIR,'join.html');else if(pathname==='/join'||pathname==='/join/'){res.writeHead(302,{Location:'/start'});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);
@@ -420,4 +420,21 @@ chain.startIndexer(evt=>{
if(sp&&sp.email)sendPaidEmail(sp.email,sp.name,evt);
}
}catch(e){console.error('paid email error',e.message)}
// Auto-count directs (Marty 2026-08-14, "prevent manual effort"): a new
// registration whose referrer sits in the rotation queue increments that
// sponsor's directs automatically — same operation as the admin ⊕ button.
// Deliberately does NOT auto-qualify at 2/2: rotating the team focus stays
// a human call, so it alerts instead.
try{
if(evt.type==='registered'&&evt.referrerId!=null){
const sponsors=getSponsors();
const idx=sponsors.findIndex(x=>String(x.id)===String(evt.referrerId));
if(idx>=0&&sponsors[idx].status!=='qualified'&&(Number(sponsors[idx].directs)||0)<2){
sponsors[idx].directs=Math.min(2,(Number(sponsors[idx].directs)||0)+1);
saveSponsors(sponsors);
const s=sponsors[idx];
sendTelegram(`🤖 AUTO-COUNT: #${evt.id} is a DIRECT for queue sponsor #${s.id}${s.name?` (${s.name})`:''} — now ${s.directs}/2.${s.directs>=2?'\n🎯 2/2 reached — open /admin and hit Qualify when you want the rotation to move.':''}`);
}
}
}catch(e){console.error('auto-direct error',e.message)}
});