From 6c73b5d9c5a0dfe959b8324f7c73fbe1fc57dea6 Mon Sep 17 00:00:00 2001 From: martbost Date: Fri, 28 Aug 2026 08:56:32 -0500 Subject: [PATCH] =?UTF-8?q?Page=20Builder=20fixes=20from=20Marty's=20test:?= =?UTF-8?q?=20our=20own=20CSP=20frame-src=20blocked=20framing=20our=20memb?= =?UTF-8?q?er=20pages=20(preview=20showed=20broken)=20=E2=80=94=20add=20's?= =?UTF-8?q?elf';=20/p/=20now=20sends=20explicit=20permissive=20framing?= =?UTF-8?q?=20headers=20so=20member=20pages=20always=20render=20embedded;?= =?UTF-8?q?=20button=20label=20tracks=20whether=20a=20page=20exists;=20liv?= =?UTF-8?q?e=20progress=20line=20with=20step=20narration=20+=20elapsed=20s?= =?UTF-8?q?econds;=20stale=20preview=20hidden=20during=20rebuild=20and=20c?= =?UTF-8?q?ache-busted=20after?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- public/suite-page.html | 1 + public/suite-page.js | 44 ++++++++++++++++++++++++++++++++++++++---- server.js | 9 +++++++-- 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/public/suite-page.html b/public/suite-page.html index 197a048..d4d671d 100644 --- a/public/suite-page.html +++ b/public/suite-page.html @@ -60,6 +60,7 @@ +
diff --git a/public/suite-page.js b/public/suite-page.js index 8142a1c..7ce3828 100644 --- a/public/suite-page.js +++ b/public/suite-page.js @@ -24,15 +24,46 @@ $('pgMeter').innerHTML = '' + m.remaining + ' of ' + m.limit + ' page builds left this month'; } + var built = false; + function showLive(url) { + built = true; $('pgUrl').textContent = url.replace(/^https:\/\//, ''); $('pgOpen').href = url; $('pgLive').style.display = 'block'; - $('pgFrame').src = url + '?preview=1'; + // cache-bust so a rebuild always shows the NEW page, not the old one + $('pgFrame').src = url + '?preview=' + Date.now(); $('pgPrev').style.display = 'block'; $('pgGo').textContent = '🔁 Rebuild my page'; } + // Live status under the button. A silent 40-second wait reads as "frozen", + // so we narrate it and count the seconds. + var progTimer = null; + function startProgress() { + var el = $('pgProg'), t0 = Date.now(); + var steps = [ + 'Reading your notes…', + 'Writing your headline and story…', + 'Choosing your angle video…', + 'Laying out your page…', + 'Almost there — finishing up…' + ]; + el.style.display = 'block'; + var tick = function () { + var s = Math.round((Date.now() - t0) / 1000); + var i = Math.min(steps.length - 1, Math.floor(s / 9)); + el.innerHTML = '' + steps[i] + ' (' + s + 's)'; + }; + tick(); + progTimer = setInterval(tick, 1000); + } + function stopProgress() { + if (progTimer) clearInterval(progTimer); + progTimer = null; + $('pgProg').style.display = 'none'; + } + function gate(msg) { var g = $('pgGate'); g.style.display = 'block'; @@ -74,8 +105,12 @@ busy = true; $('pgErr').style.display = 'none'; $('pgGo').disabled = true; - var label = $('pgGo').textContent; - $('pgGo').innerHTML = 'Writing your page… (up to a minute)'; + // Hide any previous preview while the new one is being written, so a stale + // or half-loaded frame can't look like a broken result. + $('pgPrev').style.display = 'none'; + $('pgFrame').removeAttribute('src'); + $('pgGo').innerHTML = 'Writing your page…'; + startProgress(); try { var r = await fetch('/api/public/suite-page', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -100,9 +135,10 @@ $('pgErr').textContent = 'Connection hiccup — try again.'; $('pgErr').style.display = 'block'; } + stopProgress(); busy = false; $('pgGo').disabled = false; - $('pgGo').textContent = label.indexOf('Rebuild') !== -1 ? '🔁 Rebuild my page' : '🧱 Build my page'; + $('pgGo').textContent = built ? '🔁 Rebuild my page' : '🧱 Build my page'; } document.addEventListener('DOMContentLoaded', function () { diff --git a/server.js b/server.js index e9429c9..a6ec70d 100644 --- a/server.js +++ b/server.js @@ -464,7 +464,9 @@ async function handleTranslate(req,res){ return json(res,200,{t:out}); } -const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src https://www.youtube-nocookie.com"; +// frame-src includes 'self' so our own pages (the Page Builder preview framing +// a member's /p/ page) are not blocked by our own policy. +const CSP_BASE="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; form-action 'self'; frame-src 'self' https://www.youtube-nocookie.com"; function securityHeaders(extra={}) { // Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS. return { @@ -670,7 +672,10 @@ async function handleApi(req,res,pathname){ const rec=suitePages.load(pid); if(!rec||!rec.copy)return json(res,404,{error:'No page here yet.'}); const html=suitePages.render(rec); - res.writeHead(200,{'Content-Type':'text/html; charset=utf-8','Cache-Control':'public, max-age=120'}); + // Member pages must ALWAYS be viewable inside a frame — the builder previews + // them, and members share them into contexts that embed. Never send + // X-Frame-Options here, and keep frame-ancestors open. + 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'&&pathname==='/api/public/suite-page'){