diff --git a/public/suite-copy.html b/public/suite-copy.html index bf3c88e..a006edc 100644 --- a/public/suite-copy.html +++ b/public/suite-copy.html @@ -58,5 +58,6 @@

Teach it forward: when this saves you fifteen minutes, show your two how to use it — that's how the whole team gets faster.
Everything the engine writes follows the team's rules: no income promises, no invented numbers, honest about risk. It's still your name on the post, so read before you send. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-email.html b/public/suite-email.html index 4a52205..2d463c0 100644 --- a/public/suite-email.html +++ b/public/suite-email.html @@ -58,5 +58,6 @@

Teach it forward: a member with a follow-up sequence running never loses a warm prospect to silence — show your two how to set theirs up.
Read every email before you send it; your name is on it. Generated copy follows the team's rules — no income promises, no invented numbers, honest about risk. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-founder.html b/public/suite-founder.html index cacf539..4af222d 100644 --- a/public/suite-founder.html +++ b/public/suite-founder.html @@ -73,5 +73,6 @@ curl -H "Authorization: Bearer YOUR_KEY" \

Everything here obeys the same rules as the rest of the Suite — no income claims, no invented figures, POL quantities only, Premium only. Generating a week does not post anything anywhere; it hands you the words and you decide what goes out.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-funnel.html b/public/suite-funnel.html index 9fc4de7..5aa5c2a 100644 --- a/public/suite-funnel.html +++ b/public/suite-funnel.html @@ -72,5 +72,6 @@

Every page here is public and framable, so you can drop one into an ad, a QR code, or someone's inbox and it will just work. They all carry the same honest disclaimers as the rest of the site.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-grants.html b/public/suite-grants.html index 80b6472..89f0e9e 100644 --- a/public/suite-grants.html +++ b/public/suite-grants.html @@ -64,5 +64,6 @@

Grants come out of a separate monthly pool that your level gives you — giving to your team never eats into your own allowances, because nobody should have to choose between equipping their people and using their own tools. Pools reset on the 1st along with everything else, so a grant is for unblocking somebody now rather than building a balance.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-intel.html b/public/suite-intel.html index 918ac52..ba14890 100644 --- a/public/suite-intel.html +++ b/public/suite-intel.html @@ -46,5 +46,6 @@

Nothing here identifies anybody. No positions, no names, no per-person numbers — this is a picture of what works, not a leaderboard of who is working. Groupings with too little traffic behind them are held back and counted rather than shown, because a click rate built on a few hundred impressions is noise dressed up as insight.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-leader.html b/public/suite-leader.html index 8fc064d..693351a 100644 --- a/public/suite-leader.html +++ b/public/suite-leader.html @@ -52,5 +52,6 @@

Every number here comes off the contract, not an estimate — positions, levels, and the POL forming below each person. Nothing on this page is a projection or a promise of what anyone will earn.

Coach people, don't chase them. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-page.html b/public/suite-page.html index d4d671d..48b0696 100644 --- a/public/suite-page.html +++ b/public/suite-page.html @@ -72,5 +72,6 @@

Teach it forward: when your two join, walk them through building theirs — a personal page beats a bare link every time.
Rebuilding replaces your page at the same link, so anything you've already shared keeps working. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-split.html b/public/suite-split.html index 2529b1d..a660b43 100644 --- a/public/suite-split.html +++ b/public/suite-split.html @@ -83,5 +83,6 @@

Why this is deliberately slow to declare a winner: click rates on display advertising are small, so a two- or three-click gap on a few hundred impressions is noise, not a result. This tool waits until one version is clearly ahead before it calls anything — and when the versions genuinely tie, it says so, because that is useful information too: it means the wording isn't what's holding the ad back.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-traffic.html b/public/suite-traffic.html index 0aafd5c..123e63a 100644 --- a/public/suite-traffic.html +++ b/public/suite-traffic.html @@ -98,5 +98,6 @@

What this is, honestly: display advertising is awareness volume — it puts your link in front of people browsing the network. It is not a lead list, and click rates on display inventory are low by nature. The conversations you have with people you know are still what actually builds a team; this runs quietly in the background while you do that.

All banners use approved team creative, so every ad on the network stays on-brand. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-video.html b/public/suite-video.html index faab356..8bf6066 100644 --- a/public/suite-video.html +++ b/public/suite-video.html @@ -56,5 +56,6 @@

Teach it forward: the video does the explaining so your two don't have to — show them how to make their own the day they join.
The video itself is the team's produced master; only the ending is yours, so every share stays on-brand and on-message. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/suite-voice.html b/public/suite-voice.html index 4e2f166..00c1db1 100644 --- a/public/suite-voice.html +++ b/public/suite-voice.html @@ -42,5 +42,6 @@

One thing this will not do: it will never loosen the compliance rules to sound more like you. If your own writing makes income claims, the engine still will not repeat them — the honesty rules sit above your voice profile on purpose, because they are what keeps every member safe.

Independent team resource · No income is guaranteed · Cryptocurrency involves risk.

+ diff --git a/public/tool-video.js b/public/tool-video.js new file mode 100644 index 0000000..22e8b91 --- /dev/null +++ b/public/tool-video.js @@ -0,0 +1,112 @@ +// Collapsible walkthrough player for Suite tool pages. +// +// Placement reasoning (Marty's call): the video belongs at the TOP of the tool, +// because a video in a separate library never gets watched — but it must not be +// an embedded player, because after the first week most visits are returning +// members who came to USE the tool, and a player pushes the tool below the fold +// on mobile every single time. +// +// So: a one-line bar that expands inline, auto-opened on the member's FIRST +// visit to that specific tool and collapsed on every visit after. The state is +// per-tool, per-browser. localStorage can throw outright in some contexts +// (private windows, blocked site data), so every access is guarded and the bar +// falls back to "collapsed but present" rather than breaking the page. +(function () { + 'use strict'; + + var el = document.currentScript; + var SRC = el && el.getAttribute('data-video'); + var TITLE = (el && el.getAttribute('data-title')) || 'Watch the walkthrough'; + var KEY = 'rmc.tv.' + ((el && el.getAttribute('data-key')) || (location.pathname.replace(/\W+/g, '') || 'tool')); + var SECS = (el && el.getAttribute('data-secs')) || ''; + if (!SRC) return; + + function seen() { try { return localStorage.getItem(KEY) === '1'; } catch (e) { return true; } } + function markSeen() { try { localStorage.setItem(KEY, '1'); } catch (e) {} } + + function build() { + var host = document.querySelector('main') || document.body; + var wrap = document.createElement('div'); + wrap.className = 'tv-wrap'; + + var bar = document.createElement('button'); + bar.type = 'button'; + bar.className = 'tv-bar'; + bar.setAttribute('aria-expanded', 'false'); + bar.innerHTML = '▶' + TITLE + + (SECS ? ' ' + SECS + '' : '') + '⌄'; + + var body = document.createElement('div'); + body.className = 'tv-body'; + body.hidden = true; + + var vid = document.createElement('video'); + vid.controls = true; + vid.preload = 'none'; + vid.playsInline = true; + vid.src = SRC; + body.appendChild(vid); + + function open(autoplay) { + body.hidden = false; + wrap.classList.add('on'); + bar.setAttribute('aria-expanded', 'true'); + if (autoplay) { + // Muted so a first-visit autoplay is never a surprise noise; the member + // unmutes if they want it. Browsers block unmuted autoplay anyway. + vid.muted = true; + vid.play().catch(function () {}); + } + } + function close() { + body.hidden = true; + wrap.classList.remove('on'); + bar.setAttribute('aria-expanded', 'false'); + vid.pause(); + } + + bar.addEventListener('click', function () { + if (body.hidden) { open(false); markSeen(); } + else close(); + }); + + wrap.appendChild(bar); + wrap.appendChild(body); + + // Sit directly under the page heading if there is one, otherwise at the top. + var h1 = host.querySelector('h1'); + var lede = h1 && h1.nextElementSibling && /lede|sub/.test(h1.nextElementSibling.className || '') + ? h1.nextElementSibling : h1; + if (lede && lede.parentNode) lede.parentNode.insertBefore(wrap, lede.nextSibling); + else host.insertBefore(wrap, host.firstChild); + + if (!seen()) { open(true); markSeen(); } + } + + function style() { + if (document.getElementById('tv-style')) return; + var s = document.createElement('style'); + s.id = 'tv-style'; + s.textContent = [ + '.tv-wrap{margin:0 0 18px}', + '.tv-bar{display:flex;align-items:center;gap:10px;width:100%;text-align:left;cursor:pointer;', + ' background:rgba(212,175,55,.08);border:1px solid var(--gold,#d4af37);border-radius:12px;', + ' color:var(--text,#e8eef4);padding:11px 15px;font:inherit;font-size:14.5px;font-weight:700}', + '.tv-bar:hover{background:rgba(212,175,55,.14)}', + '.tv-bar:focus-visible{outline:2px solid var(--teal,#4ed6cb);outline-offset:2px}', + '.tv-play{color:var(--gold,#d4af37);font-size:12px}', + '.tv-t{flex:1}', + '.tv-secs{color:var(--muted,#8fa6bb);font-weight:600;font-size:12.5px}', + '.tv-chev{color:var(--muted,#8fa6bb);transition:transform .18s}', + '.tv-wrap.on .tv-chev{transform:rotate(180deg)}', + '.tv-body{margin-top:10px;border:1px solid var(--line,#24425d);border-radius:12px;overflow:hidden;line-height:0}', + '.tv-body video{width:100%;height:auto;display:block;background:#000}', + '@media (prefers-reduced-motion:reduce){.tv-chev{transition:none}}' + ].join(''); + document.head.appendChild(s); + } + + function go() { style(); build(); } + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', go); + else go(); +})(); diff --git a/server.js b/server.js index 31afef5..92356fb 100644 --- a/server.js +++ b/server.js @@ -1062,6 +1062,35 @@ async function handleApi(req,res,pathname){ }catch(err){ return json(res,502,{error:String(err.message||err)}); } } + // Tool walkthrough videos, served from the data volume rather than the image. + // 45MB of MP4 in the repo would ride along on every single deploy, and this + // way a video can be re-cut and dropped in without a rebuild. + // + // Range support is not optional here: without it the browser cannot seek, so + // the scrub bar looks present but does nothing. + if(req.method==='GET'&&/^\/tv\/[a-z0-9-]{1,60}\.mp4$/.test(pathname)){ + const slug=pathname.slice(4).replace(/\.mp4$/,''); + const vfile=path.join(DATA_DIR,'tool-videos',slug+'.mp4'); + let st=null; + try{ st=fs.statSync(vfile); }catch(err){ return json(res,404,{error:'No walkthrough for that tool yet.'}); } + const range=req.headers.range; + const base={'Content-Type':'video/mp4','Accept-Ranges':'bytes','Cache-Control':'public, max-age=3600'}; + if(range){ + const m=/bytes=(\d*)-(\d*)/.exec(range)||[]; + const start=m[1]?parseInt(m[1],10):0; + const end=m[2]?parseInt(m[2],10):st.size-1; + if(start>=st.size||end>=st.size||start>end){ + res.writeHead(416,securityHeaders(Object.assign({},base,{'Content-Range':'bytes */'+st.size}))); + return res.end(); + } + res.writeHead(206,securityHeaders(Object.assign({},base,{ + 'Content-Range':'bytes '+start+'-'+end+'/'+st.size,'Content-Length':(end-start+1)}))); + return fs.createReadStream(vfile,{start,end}).pipe(res); + } + res.writeHead(200,securityHeaders(Object.assign({},base,{'Content-Length':st.size}))); + return fs.createReadStream(vfile).pipe(res); + } + 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}); @@ -1366,7 +1395,7 @@ const server=http.createServer(async(req,res)=>{ const u=new URL(req.url,`http://${req.headers.host||'localhost'}`),pathname=decodeURIComponent(u.pathname); // /p/ = member Page Builder pages: dynamic HTML, so it must reach the // API handler rather than the static-file path (which 404s it). - if(pathname==='/health'||pathname.startsWith('/api/')||/^\/p\/\d{1,15}(\/[a-z0-9-]{1,24})?$/.test(pathname))return await handleApi(req,res,pathname); + if(pathname==='/health'||pathname.startsWith('/api/')||pathname.startsWith('/tv/')||/^\/p\/\d{1,15}(\/[a-z0-9-]{1,24})?$/.test(pathname))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'}); // Members-area gate: the Circle Method lessons 2-10 + the e-gift cash-out // walkthrough are the PRODUCT — their video FILES require a wallet-verified