Tool walkthrough videos: collapsible player + volume-served /tv/ route

Twelve narrated walkthroughs, one per Suite tool, in Marty's cloned voice.
No screen recording involved: Playwright drives and records the real UI,
ElevenLabs narrates, ffmpeg muxes. Pipeline lives outside this repo at
../video-pipeline.

Placement (Marty's call): TOP of each tool page but COLLAPSED — a one-line
"Watch the walkthrough" bar that auto-opens on a member's first visit to that
specific tool and stays shut after. A video in a separate library never gets
watched; an embedded player pushes the tool below the fold on mobile for every
returning member. First-visit autoplay is muted, so it is never a surprise
noise, and every localStorage access is guarded because it throws outright in
private windows.

Served from the data volume via /tv/<slug>.mp4, NOT from public/. That
directory is already 199MB and another 33MB of MP4 would ride along on every
deploy forever; this way a video can also be re-cut and dropped in without a
rebuild. The route implements HTTP range requests — without them the scrub bar
renders but cannot seek, which looks broken in a way people rarely report.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-08-28 14:14:21 -05:00
parent 6a7c5161ff
commit 1896b707f8
14 changed files with 154 additions and 1 deletions
+1
View File
@@ -58,5 +58,6 @@
<p class="cp-note"><strong style="color:var(--text)">Teach it forward:</strong> when this saves you fifteen minutes, show your two how to use it — that's how the whole team gets faster. <br>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.</p>
</main>
<script src="/suite-copy.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-copy-engine.mp4" data-title="Watch the Copy Engine walkthrough" data-key="copy" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -58,5 +58,6 @@
<p class="em-note"><strong style="color:var(--text)">Teach it forward:</strong> a member with a follow-up sequence running never loses a warm prospect to silence — show your two how to set theirs up. <br>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.</p>
</main>
<script src="/suite-email.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-email-engine.mp4" data-title="Watch the Email Engine walkthrough" data-key="email" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -73,5 +73,6 @@ curl -H "Authorization: Bearer YOUR_KEY" \
<p class="d-note"><strong style="color:var(--text)">Everything here obeys the same rules as the rest of the Suite</strong> — 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-founder.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-founder-desk.mp4" data-title="Watch the Founder Desk walkthrough" data-key="founder" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -72,5 +72,6 @@
<p class="f-note"><strong style="color:var(--text)">Every page here is public and framable</strong>, 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-funnel.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-funnel-factory.mp4" data-title="Watch the Funnel Factory walkthrough" data-key="funnel" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -64,5 +64,6 @@
<p class="g-note"><strong style="color:var(--text)">Grants come out of a separate monthly pool that your level gives you</strong> — 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-grants.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-team-grants.mp4" data-title="Watch the Team Grants walkthrough" data-key="grants" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -46,5 +46,6 @@
<p class="i-note"><strong style="color:var(--text)">Nothing here identifies anybody.</strong> 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-intel.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-network-intel.mp4" data-title="Watch the Network Intelligence walkthrough" data-key="intel" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -52,5 +52,6 @@
<p class="l-note"><strong style="color:var(--text)">Every number here comes off the contract</strong>, 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. <br><br>Coach people, don't chase them. Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-leader.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-leader-ops.mp4" data-title="Watch the Leader Ops walkthrough" data-key="leader" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -72,5 +72,6 @@
<p class="pg-note"><strong style="color:var(--text)">Teach it forward:</strong> when your two join, walk them through building theirs — a personal page beats a bare link every time. <br>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.</p>
</main>
<script src="/suite-page.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-page-builder.mp4" data-title="Watch the Page Builder walkthrough" data-key="page" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -83,5 +83,6 @@
<p class="s-note"><strong style="color:var(--text)">Why this is deliberately slow to declare a winner:</strong> 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-split.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-split-tester.mp4" data-title="Watch the Split Tester walkthrough" data-key="split" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -98,5 +98,6 @@
<p class="tf-note"><strong style="color:var(--text)">What this is, honestly:</strong> display advertising is <strong style="color:var(--text)">awareness volume</strong> — 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. <br><br>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.</p>
</main>
<script src="/suite-traffic.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-traffic-desk.mp4" data-title="Watch the Traffic Desk walkthrough" data-key="traffic" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -56,5 +56,6 @@
<p class="vd-note"><strong style="color:var(--text)">Teach it forward:</strong> the video does the explaining so your two don't have to — show them how to make their own the day they join. <br>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.</p>
</main>
<script src="/suite-video.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-video-maker.mp4" data-title="Watch the Video Maker walkthrough" data-key="video" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+1
View File
@@ -42,5 +42,6 @@
<p class="v-note"><strong style="color:var(--text)">One thing this will not do:</strong> 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. <br><br>Independent team resource · No income is guaranteed · Cryptocurrency involves risk.</p>
</main>
<script src="/suite-voice.js"></script>
<script src="/tool-video.js" data-video="/tv/suite-voice-profile.mp4" data-title="Watch the Voice Profile walkthrough" data-key="voice" defer></script>
<script src="/chat.js" defer></script>
<script src="/translate.js" defer></script><script src="/tg-app.js" defer></script><script src="/nav-dash.js" defer></script></body></html>
+112
View File
@@ -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 = '<span class="tv-play">▶</span><span class="tv-t">' + TITLE +
(SECS ? ' <span class="tv-secs">' + SECS + '</span>' : '') + '</span><span class="tv-chev">⌄</span>';
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();
})();
+30 -1
View File
@@ -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/<id> = 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