Fix OpenBao login overlay runaway DOM loop and slow loads

Replace the MutationObserver feedback loop with bounded, idempotent apply
retries so Firefox no longer hangs on the auth page. Route static UI assets
and API calls around HTML sub_filter injection to keep bundles compressed.
This commit is contained in:
2026-06-19 20:58:44 +02:00
parent 6ddf4e56b4
commit a6a87ae282
2 changed files with 77 additions and 15 deletions

View File

@@ -26,6 +26,16 @@ http {
add_header Cache-Control "public, max-age=300"; add_header Cache-Control "public, max-age=300";
} }
# Static UI bundles and API calls bypass HTML injection and stay compressed.
location ~ ^/(v1|ui/assets|ui/engines-dist|ui/favicon\.svg) {
proxy_pass http://openbao_upstream;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / { location / {
proxy_pass http://openbao_upstream; proxy_pass http://openbao_upstream;
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -33,7 +43,7 @@ http {
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
# Disable upstream compression so sub_filter can rewrite HTML. # Disable upstream compression only for HTML shell injection.
proxy_set_header Accept-Encoding ""; proxy_set_header Accept-Encoding "";
proxy_buffering on; proxy_buffering on;

View File

@@ -2,6 +2,8 @@
"use strict"; "use strict";
const PRESETS_URL = "/ui/platform-overlay/presets.json"; const PRESETS_URL = "/ui/platform-overlay/presets.json";
const MAX_APPLY_ATTEMPTS = 40;
const APPLY_INTERVAL_MS = 250;
const DEFAULT_PRESETS = { const DEFAULT_PRESETS = {
namespace: "", namespace: "",
method: "oidc", method: "oidc",
@@ -14,6 +16,9 @@
}; };
let presets = { ...DEFAULT_PRESETS }; let presets = { ...DEFAULT_PRESETS };
let applyAttempts = 0;
let applyTimer = null;
let overlayApplied = false;
function isAuthPage() { function isAuthPage() {
const path = window.location.pathname; const path = window.location.pathname;
@@ -24,19 +29,23 @@
} }
function hideNode(node) { function hideNode(node) {
if (!node) return; if (!node || node.dataset.keycapeOverlayHidden === "true") return;
const field = const field =
node.closest(".field.is-horizontal") || node.closest(".field.is-horizontal") ||
node.closest(".field") || node.closest(".field") ||
node.closest(".box") || node.closest(".box") ||
node; node;
if (field.dataset.keycapeOverlayHidden === "true") return;
field.style.display = "none"; field.style.display = "none";
field.setAttribute("aria-hidden", "true"); field.setAttribute("aria-hidden", "true");
field.dataset.keycapeOverlayHidden = "true";
} }
function setInputValue(input, value) { function setInputValue(input, value) {
if (!input || input.value === value) return; if (!input || input.dataset.keycapeOverlayPreset === value) return;
input.value = value; input.value = value;
input.dataset.keycapeOverlayPreset = value;
// Fire once so Ember picks up the preset without a mutation feedback loop.
input.dispatchEvent(new Event("input", { bubbles: true })); input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true })); input.dispatchEvent(new Event("change", { bubbles: true }));
} }
@@ -62,8 +71,16 @@
} }
} }
function loginShellReady() {
return Boolean(
document.querySelector(".login-form") ||
document.querySelector(".auth-form") ||
document.querySelector(".toolbar-namespace-picker")
);
}
function applyDom() { function applyDom() {
if (!isAuthPage()) return; if (!isAuthPage() || overlayApplied) return false;
hideNode(document.querySelector(".toolbar-namespace-picker")); hideNode(document.querySelector(".toolbar-namespace-picker"));
document document
@@ -85,8 +102,10 @@
.forEach(hideNode); .forEach(hideNode);
document.querySelectorAll("nav.tabs").forEach((el) => { document.querySelectorAll("nav.tabs").forEach((el) => {
if (el.dataset.keycapeOverlayHidden === "true") return;
el.style.display = "none"; el.style.display = "none";
el.setAttribute("aria-hidden", "true"); el.setAttribute("aria-hidden", "true");
el.dataset.keycapeOverlayHidden = "true";
}); });
document document
@@ -94,7 +113,10 @@
.forEach(hideNode); .forEach(hideNode);
document.querySelectorAll("h1.title.is-3").forEach((heading) => { document.querySelectorAll("h1.title.is-3").forEach((heading) => {
if (/Sign in to OpenBao|Authenticate/.test(heading.textContent)) { if (
/Sign in to OpenBao|Authenticate/.test(heading.textContent) &&
heading.textContent !== presets.title
) {
heading.textContent = presets.title; heading.textContent = presets.title;
} }
}); });
@@ -102,7 +124,9 @@
document document
.querySelectorAll('#auth-submit, button[data-test="auth-submit"]') .querySelectorAll('#auth-submit, button[data-test="auth-submit"]')
.forEach((button) => { .forEach((button) => {
button.textContent = presets.signInLabel; if (button.textContent !== presets.signInLabel) {
button.textContent = presets.signInLabel;
}
}); });
document document
@@ -111,7 +135,9 @@
document document
.querySelectorAll('#role, input[name="role"]') .querySelectorAll('#role, input[name="role"]')
.forEach((input) => setInputValue(input, presets.role || "platform-admin")); .forEach((input) =>
setInputValue(input, presets.role || "platform-admin")
);
if (!document.getElementById("keycape-overlay-banner")) { if (!document.getElementById("keycape-overlay-banner")) {
const banner = document.createElement("div"); const banner = document.createElement("div");
@@ -125,6 +151,36 @@
} }
document.documentElement.classList.add("keycape-overlay-active"); document.documentElement.classList.add("keycape-overlay-active");
if (loginShellReady()) {
overlayApplied = true;
return true;
}
return false;
}
function stopApplyLoop() {
if (applyTimer !== null) {
window.clearInterval(applyTimer);
applyTimer = null;
}
}
function scheduleApply() {
stopApplyLoop();
applyAttempts = 0;
const tick = () => {
applyAttempts += 1;
if (applyDom() || applyAttempts >= MAX_APPLY_ATTEMPTS) {
stopApplyLoop();
return;
}
};
tick();
applyTimer = window.setInterval(tick, APPLY_INTERVAL_MS);
} }
async function loadPresets() { async function loadPresets() {
@@ -138,12 +194,6 @@
} }
} }
function observe() {
const observer = new MutationObserver(() => applyDom());
observer.observe(document.body, { childList: true, subtree: true });
applyDom();
}
async function init() { async function init() {
await loadPresets(); await loadPresets();
if (!isAuthPage()) return; if (!isAuthPage()) return;
@@ -151,9 +201,11 @@
ensureAuthMountSelected(); ensureAuthMountSelected();
if (document.readyState === "loading") { if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observe); document.addEventListener("DOMContentLoaded", scheduleApply, {
once: true,
});
} else { } else {
observe(); scheduleApply();
} }
} }