Page Builder fixes from Marty's test: our own CSP frame-src blocked framing our member pages (preview showed broken) — add 'self'; /p/<id> now sends explicit permissive framing headers so member pages always render embedded; button label tracks whether a page exists; live progress line with step narration + elapsed seconds; stale preview hidden during rebuild and cache-busted after
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -60,6 +60,7 @@
|
|||||||
<button id="pgGo" class="btn btn-primary">🧱 Build my page</button>
|
<button id="pgGo" class="btn btn-primary">🧱 Build my page</button>
|
||||||
<span id="pgMeter"></span>
|
<span id="pgMeter"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="pgProg" style="display:none;margin-top:10px;font-size:13.5px;color:var(--teal);font-weight:600"></div>
|
||||||
<div id="pgErr"></div>
|
<div id="pgErr"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+40
-4
@@ -24,15 +24,46 @@
|
|||||||
$('pgMeter').innerHTML = '<b>' + m.remaining + '</b> of ' + m.limit + ' page builds left this month';
|
$('pgMeter').innerHTML = '<b>' + m.remaining + '</b> of ' + m.limit + ' page builds left this month';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var built = false;
|
||||||
|
|
||||||
function showLive(url) {
|
function showLive(url) {
|
||||||
|
built = true;
|
||||||
$('pgUrl').textContent = url.replace(/^https:\/\//, '');
|
$('pgUrl').textContent = url.replace(/^https:\/\//, '');
|
||||||
$('pgOpen').href = url;
|
$('pgOpen').href = url;
|
||||||
$('pgLive').style.display = 'block';
|
$('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';
|
$('pgPrev').style.display = 'block';
|
||||||
$('pgGo').textContent = '🔁 Rebuild my page';
|
$('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 = '<span class="spin"></span>' + steps[i] + ' <span style="opacity:.65">(' + s + 's)</span>';
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
progTimer = setInterval(tick, 1000);
|
||||||
|
}
|
||||||
|
function stopProgress() {
|
||||||
|
if (progTimer) clearInterval(progTimer);
|
||||||
|
progTimer = null;
|
||||||
|
$('pgProg').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
function gate(msg) {
|
function gate(msg) {
|
||||||
var g = $('pgGate');
|
var g = $('pgGate');
|
||||||
g.style.display = 'block';
|
g.style.display = 'block';
|
||||||
@@ -74,8 +105,12 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
$('pgErr').style.display = 'none';
|
$('pgErr').style.display = 'none';
|
||||||
$('pgGo').disabled = true;
|
$('pgGo').disabled = true;
|
||||||
var label = $('pgGo').textContent;
|
// Hide any previous preview while the new one is being written, so a stale
|
||||||
$('pgGo').innerHTML = '<span class="spin"></span>Writing your page… (up to a minute)';
|
// or half-loaded frame can't look like a broken result.
|
||||||
|
$('pgPrev').style.display = 'none';
|
||||||
|
$('pgFrame').removeAttribute('src');
|
||||||
|
$('pgGo').innerHTML = '<span class="spin"></span>Writing your page…';
|
||||||
|
startProgress();
|
||||||
try {
|
try {
|
||||||
var r = await fetch('/api/public/suite-page', {
|
var r = await fetch('/api/public/suite-page', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -100,9 +135,10 @@
|
|||||||
$('pgErr').textContent = 'Connection hiccup — try again.';
|
$('pgErr').textContent = 'Connection hiccup — try again.';
|
||||||
$('pgErr').style.display = 'block';
|
$('pgErr').style.display = 'block';
|
||||||
}
|
}
|
||||||
|
stopProgress();
|
||||||
busy = false;
|
busy = false;
|
||||||
$('pgGo').disabled = 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 () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|||||||
@@ -464,7 +464,9 @@ async function handleTranslate(req,res){
|
|||||||
return json(res,200,{t:out});
|
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/<id> 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={}) {
|
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.
|
// Public pages must render inside safelist / traffic-exchange iframes, so framing stays open here; admin.html re-locks it via ADMIN_FRAME_HEADERS.
|
||||||
return {
|
return {
|
||||||
@@ -670,7 +672,10 @@ async function handleApi(req,res,pathname){
|
|||||||
const rec=suitePages.load(pid);
|
const rec=suitePages.load(pid);
|
||||||
if(!rec||!rec.copy)return json(res,404,{error:'No page here yet.'});
|
if(!rec||!rec.copy)return json(res,404,{error:'No page here yet.'});
|
||||||
const html=suitePages.render(rec);
|
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);
|
return res.end(html);
|
||||||
}
|
}
|
||||||
if(req.method==='GET'&&pathname==='/api/public/suite-page'){
|
if(req.method==='GET'&&pathname==='/api/public/suite-page'){
|
||||||
|
|||||||
Reference in New Issue
Block a user