From e4b45190618d0dafa591d0bfb44d29085fc95e0b Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 26 Mar 2026 20:06:09 +0000 Subject: [PATCH] fix: onboarding 401 redirect, glass card rendering bugs - rpc-client: don't redirect to /login on 401 during onboarding flow, which caused session expired kicks on fresh installs - style.css: add translateZ(0) + isolation:isolate to glass-card, glass-strong, path-option-card to fix Chromium compositor bug where backdrop-filter + animated fixed overlays cause black rectangles - App.vue: pause background animations when tab hidden, force compositor layer rebuild on tab return to prevent stale renders Co-Authored-By: Claude Opus 4.6 (1M context) --- neode-ui/src/App.vue | 22 ++++++++++++++++++++++ neode-ui/src/api/rpc-client.ts | 4 +++- neode-ui/src/style.css | 30 +++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/neode-ui/src/App.vue b/neode-ui/src/App.vue index 3408a82e..52775363 100644 --- a/neode-ui/src/App.vue +++ b/neode-ui/src/App.vue @@ -168,7 +168,28 @@ const isReady = ref(false) * - User has already seen the intro * - User is on a direct route (refresh/bookmark) */ +// Fix Chromium backdrop-filter rendering bug: when tab loses/regains focus, +// the compositor fails to repaint backdrop-filter layers over animated +// fixed-position overlays (body::before/after with mix-blend-mode). +// On return: strip backdrop-filter via class, wait a frame, then restore. +function onVisibilityChange() { + if (document.hidden) { + document.documentElement.classList.add('tab-hidden') + } else { + // Step 1: kill all backdrop-filters (forces compositor to drop those layers) + document.documentElement.classList.add('no-backdrop') + document.documentElement.classList.remove('tab-hidden') + // Step 2: next frame, re-enable (compositor builds fresh layers) + requestAnimationFrame(() => { + requestAnimationFrame(() => { + document.documentElement.classList.remove('no-backdrop') + }) + }) + } +} + onMounted(async () => { + document.addEventListener('visibilitychange', onVisibilityChange) window.addEventListener('keydown', onKeyDown, true) window.addEventListener('mousemove', onUserActivity) window.addEventListener('mousedown', onUserActivity) @@ -196,6 +217,7 @@ onMounted(async () => { }) onBeforeUnmount(() => { + document.removeEventListener('visibilitychange', onVisibilityChange) window.removeEventListener('keydown', onKeyDown, true) window.removeEventListener('mousemove', onUserActivity) window.removeEventListener('mousedown', onUserActivity) diff --git a/neode-ui/src/api/rpc-client.ts b/neode-ui/src/api/rpc-client.ts index f8e6eef4..84454e53 100644 --- a/neode-ui/src/api/rpc-client.ts +++ b/neode-ui/src/api/rpc-client.ts @@ -62,7 +62,9 @@ class RPCClient { // Use a single shared timeout to prevent redirect storms when // multiple parallel requests all get 401 at once if (response.status === 401 && method !== 'auth.login') { - if (!RPCClient._sessionExpiredRedirecting) { + // Don't redirect during onboarding — those endpoints are unauthenticated + const isOnboarding = window.location.pathname.startsWith('/onboarding') + if (!isOnboarding && !RPCClient._sessionExpiredRedirecting) { RPCClient._sessionExpiredRedirecting = true setTimeout(() => { window.location.href = '/login' diff --git a/neode-ui/src/style.css b/neode-ui/src/style.css index cad8afa5..26075d7b 100644 --- a/neode-ui/src/style.css +++ b/neode-ui/src/style.css @@ -115,14 +115,18 @@ input[type="radio"]:active + * { -webkit-backdrop-filter: blur(18px); border: 1px solid rgba(255, 255, 255, 0.18); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + transform: translateZ(0); + isolation: isolate; } - + .glass-strong { background-color: rgba(0, 0, 0, 0.35); backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px); border: 1px solid rgba(255, 255, 255, 0.18); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + transform: translateZ(0); + isolation: isolate; } .glass-card { @@ -134,6 +138,11 @@ input[type="radio"]:active + * { border-radius: 1rem; overflow-x: hidden; overflow-y: visible; + /* Fix Chromium compositor bug: backdrop-filter + fixed animated overlays + causes cards to render as black rectangles on scroll/tab-switch. + Own layer + isolation prevents stacking context confusion. */ + transform: translateZ(0); + isolation: isolate; } /* Mode switcher - sidebar toggle */ @@ -767,6 +776,8 @@ input[type="radio"]:active + * { cursor: pointer; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.2s ease, box-shadow 0.3s ease; border: none; + transform: translateZ(0); + isolation: isolate; } .path-option-card:active { @@ -1175,6 +1186,23 @@ body::after { animation-fill-mode: backwards; } +/* Pause background animations when tab is hidden to prevent + Chromium compositor from corrupting backdrop-filter layers on tab return */ +html.tab-hidden body::before, +html.tab-hidden body::after, +html.tab-hidden::before { + animation-play-state: paused !important; + will-change: auto !important; +} + +/* Strip all backdrop-filters to force compositor layer rebuild on tab return */ +html.no-backdrop *, +html.no-backdrop *::before, +html.no-backdrop *::after { + backdrop-filter: none !important; + -webkit-backdrop-filter: none !important; +} + /* Dashboard: full viewport width, no letterboxing, no body scroll */ body.dashboard-active { overflow: hidden;