Compare commits

...

3 Commits

Author SHA1 Message Date
c46e24f820 Merge branch 'fix/750-health-banner-spa-reinject' — health-banner SPA re-inject guard (ref #750)
Some checks are pending
License Headers / check (push) Waiting to run
First-party WAF-injected health banner: re-attach trigger+banner (and re-inject
styles) when an SPA rebuilds <body>, with a documentElement childList observer +
1.5s interval fallback. Parity with the R3 banner's existing self-heal.
NB: distinct from the x.com R3 kbin-banner nonce-CSP issue (#751).
2026-06-26 19:35:51 +02:00
3e9f6e8461 fix(hub): re-sync health-banner-open class on re-attach + bump 1.4.7 (ref #750) 2026-06-26 19:11:46 +02:00
4315584f79 fix(hub): health-banner SPA re-inject guard — re-attach on body wipe (ref #750) 2026-06-26 19:08:39 +02:00

View File

@ -21,7 +21,7 @@
if (window.__SBX_HEALTH_BANNER__) return;
window.__SBX_HEALTH_BANNER__ = true;
const VERSION = '1.4.5';
const VERSION = '1.4.7';
const VISITOR_ORIGIN_API = window.SECUBOX_VISITOR_ORIGIN_API
|| '/api/v1/metrics/visitor-origin';
const LIVE_HOSTS_API = window.SECUBOX_LIVE_HOSTS_API
@ -926,6 +926,35 @@
document.body.appendChild(trigger);
document.body.appendChild(banner);
// ── SPA re-inject guard (#750) ─────────────────────────────────────
// SPA sites (x.com, Next.js news) rebuild <body> on hydration, wiping
// our appended nodes; the one-shot __SBX_HEALTH_BANNER__ guard then
// blocks any re-init, so the banner never returns. Re-attach the
// already-created nodes — and re-add the styles if <head> was cleared
// too — whenever they detach. The closure keeps the refs alive even
// after the DOM node is wiped, and re-appending the SAME nodes
// preserves their event listeners.
function ensureMounted() {
injectBannerStyles(); // id-guarded: no-op when the <style> is present
const body = document.body;
if (!body) return;
if (!trigger.isConnected) body.appendChild(trigger);
if (!banner.isConnected) {
body.appendChild(banner);
// Re-sync the layout-shift class: a body wiped while the banner
// was expanded loses 'health-banner-open' on the fresh body.
body.classList.toggle('health-banner-open', banner.classList.contains('expanded'));
}
}
try {
// childList on <html> catches a full <body> element swap (cheap, no subtree).
new MutationObserver(ensureMounted)
.observe(document.documentElement, { childList: true });
} catch (_) { /* MutationObserver unsupported → the interval below covers it */ }
// Fallback for body.innerHTML='' (children cleared, body element kept),
// which a childList-only observer on <html> does not see.
setInterval(ensureMounted, 1500);
// Toggle banner on trigger click
trigger.addEventListener('click', () => {
const isOpen = banner.classList.toggle('expanded');