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>
|
||||
<span id="pgMeter"></span>
|
||||
</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>
|
||||
|
||||
|
||||
+40
-4
@@ -24,15 +24,46 @@
|
||||
$('pgMeter').innerHTML = '<b>' + m.remaining + '</b> 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 = '<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) {
|
||||
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 = '<span class="spin"></span>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 = '<span class="spin"></span>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 () {
|
||||
|
||||
@@ -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/<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={}) {
|
||||
// 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'){
|
||||
|
||||
Reference in New Issue
Block a user