From bdbcb337ca16e66e8e10c312be83c50996901015 Mon Sep 17 00:00:00 2001 From: efiten Date: Thu, 21 May 2026 05:56:41 +0200 Subject: [PATCH] fix(home): re-render after config loads to fix null homeCfg on direct load (#1194) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - On direct page load to `#/home` (or a full refresh), `renderHome()` runs before the async `/api/config/theme` fetch resolves, so `window.SITE_CONFIG` is `undefined` and `homeCfg` is `null` — showing SF defaults instead of the site's customisations. - When navigating from another page the fetch has already completed, which is why it works in that case. - Fix: subscribe to `theme-refresh` (the event fired ~300 ms after the config is fetched and applied) and re-render; clean up the listener in `destroy()`. This matches the existing pattern used by `analytics.js` and `map.js`. Fixes #1193 ## Test plan - [x] Hard-refresh directly to `#/home` — customised `heroTitle`, `heroSubtitle`, steps, footer links must render correctly - [x] Navigate from another page to Home — still renders correctly (no regression) - [x] Site with no custom config — defaults render, no JS errors - [x] Theme customiser changes while on Home page — page re-renders (theme-refresh re-render still works) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.6 --- public/home.js | 11 +++++++++-- test-issue-1146-path-link-contrast-e2e.js | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/public/home.js b/public/home.js index 72ffc2b9..2e589845 100644 --- a/public/home.js +++ b/public/home.js @@ -5,6 +5,7 @@ let searchTimeout = null; let miniMap = null; let searchAbort = null; // AbortController for document-level listeners + let _themeRefreshHandler = null; const PREF_KEY = 'meshcore-user-level'; const MY_NODES_KEY = 'meshcore-my-nodes'; // [{pubkey, name, addedAt}] @@ -31,9 +32,14 @@ function init(container) { if (!localStorage.getItem(PREF_KEY)) { showChooser(container); - return; + } else { + renderHome(container); } - renderHome(container); + _themeRefreshHandler = function() { + if (!localStorage.getItem(PREF_KEY)) showChooser(container); + else renderHome(container); + }; + window.addEventListener('theme-refresh', _themeRefreshHandler); } function showChooser(container) { @@ -237,6 +243,7 @@ clearTimeout(searchTimeout); if (searchAbort) { searchAbort.abort(); searchAbort = null; } if (miniMap) { miniMap.remove(); miniMap = null; } + if (_themeRefreshHandler) { window.removeEventListener('theme-refresh', _themeRefreshHandler); _themeRefreshHandler = null; } } // ==================== MY NODES DASHBOARD ==================== diff --git a/test-issue-1146-path-link-contrast-e2e.js b/test-issue-1146-path-link-contrast-e2e.js index 484efbf4..77a465fa 100644 --- a/test-issue-1146-path-link-contrast-e2e.js +++ b/test-issue-1146-path-link-contrast-e2e.js @@ -126,7 +126,14 @@ async function effectiveBgFor(page, selector) { }); await step('Path link contrast (#pathsContent a) ≥ 4.5:1 in dark mode', async () => { - const linkColor = await page.$eval('#pathsContent a[href^="#/nodes/"]', (el) => getComputedStyle(el).color); + // Use page.evaluate (single CDP call) so querySelector and getComputedStyle + // are atomic — page.$eval splits them across two calls, leaving a window + // where a concurrent re-render can detach the element before getComputedStyle + // runs, causing Chromium to return '' for color. + const linkColor = await page.evaluate(() => { + const el = document.querySelector('#pathsContent a[href^="#/nodes/"]'); + return el ? getComputedStyle(el).color : ''; + }); const bgColor = await effectiveBgFor(page, '#pathsContent a[href^="#/nodes/"]'); const fg = parseRgb(linkColor); const bg = parseRgb(bgColor);