From f71e117cdd3750d03608f054ba0954e41831d23c Mon Sep 17 00:00:00 2001 From: efiten Date: Thu, 2 Apr 2026 03:45:15 +0200 Subject: [PATCH] fix: reset restores home steps after SITE_CONFIG contamination (#460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem Fixes #325. Removing all home steps and clicking "Reset my theme" did not restore them. ## Root cause Two-part bug: **1. `SITE_CONFIG.home` permanently mutated at page load** `app.js` calls `mergeUserHomeConfig(SITE_CONFIG, userTheme)` which does `SITE_CONFIG.home = Object.assign({}, serverHome, userTheme.home)`. If the user had `steps: []` saved in localStorage, this sets `SITE_CONFIG.home.steps = []` globally โ€” permanently for the lifetime of the page. **2. `initState()` reads the contaminated config** When the customizer opens (or Reset is clicked), `initState()` reads `cfg = window.SITE_CONFIG`. Since `SITE_CONFIG.home.steps` is already `[]`, `state.home.steps` stays `[]` even after `localStorage.removeItem`. `autoSave()` then re-saves `steps: []` straight back. **Secondary issue:** `data-rm-step` / add / move handlers didn't call `autoSave()`, making step persistence non-deterministic (only saved if a text field edit happened to be pending). ## Fix - **`app.js`**: snapshot `SITE_CONFIG.home` before `mergeUserHomeConfig` โ†’ `window._SITE_CONFIG_ORIGINAL_HOME` - **`customize.js`**: `initState()` uses `_SITE_CONFIG_ORIGINAL_HOME` instead of the contaminated `cfg.home` - **`customize.js`**: add `autoSave()` to rm/move/add handlers for steps, checklist, and footer links ## Tests 2 new unit tests covering the snapshot bypass and DEFAULTS fallback. 231 tests pass. ## Checklist - [x] Branches from `upstream/master` - [x] No Matomo or local-only commits - [x] Cache busters bumped - [x] 231 tests pass, 0 fail ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) --- public/app.js | 1 + public/customize.js | 17 ++++++------ public/index.html | 56 ++++++++++++++++++++-------------------- test-frontend-helpers.js | 37 ++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 36 deletions(-) diff --git a/public/app.js b/public/app.js index 6e5749fa..bd54467b 100644 --- a/public/app.js +++ b/public/app.js @@ -807,6 +807,7 @@ window.addEventListener('DOMContentLoaded', () => { // User's localStorage preferences take priority over server config const userTheme = (() => { try { return JSON.parse(localStorage.getItem('meshcore-user-theme') || '{}'); } catch { return {}; } })(); + window._SITE_CONFIG_ORIGINAL_HOME = JSON.parse(JSON.stringify(window.SITE_CONFIG.home || {})); mergeUserHomeConfig(window.SITE_CONFIG, userTheme); // Apply CSS variable overrides from theme config (skipped if user has local overrides) diff --git a/public/customize.js b/public/customize.js index c879ebe2..f5cf61ec 100644 --- a/public/customize.js +++ b/public/customize.js @@ -450,7 +450,8 @@ function mergeSection(key) { return Object.assign({}, DEFAULTS[key], cfg[key] || {}, local[key] || {}); } - var mergedHome = mergeSection('home'); + var serverHome = window._SITE_CONFIG_ORIGINAL_HOME || cfg.home || {}; + var mergedHome = Object.assign({}, DEFAULTS.home, serverHome, local.home || {}); var localTsMode = localStorage.getItem('meshcore-timestamp-mode'); var localTsTimezone = localStorage.getItem('meshcore-timestamp-timezone'); var localTsFormat = localStorage.getItem('meshcore-timestamp-format'); @@ -1202,19 +1203,19 @@ var tmp = state.home.steps[i]; state.home.steps[i] = state.home.steps[j]; state.home.steps[j] = tmp; - render(container); + render(container); autoSave(); }); }); container.querySelectorAll('[data-rm-step]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.steps.splice(parseInt(btn.dataset.rmStep), 1); - render(container); + render(container); autoSave(); }); }); var addStepBtn = document.getElementById('addStep'); if (addStepBtn) addStepBtn.addEventListener('click', function () { state.home.steps.push({ emoji: '๐Ÿ“Œ', title: '', description: '' }); - render(container); + render(container); autoSave(); }); // Checklist @@ -1227,13 +1228,13 @@ container.querySelectorAll('[data-rm-check]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.checklist.splice(parseInt(btn.dataset.rmCheck), 1); - render(container); + render(container); autoSave(); }); }); var addCheckBtn = document.getElementById('addCheck'); if (addCheckBtn) addCheckBtn.addEventListener('click', function () { state.home.checklist.push({ question: '', answer: '' }); - render(container); + render(container); autoSave(); }); // Footer links @@ -1246,13 +1247,13 @@ container.querySelectorAll('[data-rm-link]').forEach(function (btn) { btn.addEventListener('click', function () { state.home.footerLinks.splice(parseInt(btn.dataset.rmLink), 1); - render(container); + render(container); autoSave(); }); }); var addLinkBtn = document.getElementById('addLink'); if (addLinkBtn) addLinkBtn.addEventListener('click', function () { state.home.footerLinks.push({ label: '', url: '' }); - render(container); + render(container); autoSave(); }); // Export copy diff --git a/public/index.html b/public/index.html index 55f66d9b..3bc60197 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -85,30 +85,30 @@
- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index 6fb128d3..acaa0a6e 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -1960,6 +1960,43 @@ console.log('\n=== customize.js: initState merge behavior ==='); assert.strictEqual(state.theme.accent, '#abcdef'); assert.strictEqual(state.theme.navBg, '#fedcba'); }); + + test('initState uses _SITE_CONFIG_ORIGINAL_HOME to bypass contaminated SITE_CONFIG.home', () => { + // Simulates: app.js called mergeUserHomeConfig which mutated SITE_CONFIG.home.steps = [] + // The original server steps must still be recoverable via _SITE_CONFIG_ORIGINAL_HOME + const ctx = makeSandbox(); + ctx.setTimeout = function (fn) { fn(); return 1; }; + ctx.clearTimeout = function () {}; + // SITE_CONFIG.home is contaminated โ€” steps wiped by mergeUserHomeConfig at page load + ctx.window.SITE_CONFIG = { + home: { + heroTitle: 'Server Hero', + steps: [] // contaminated โ€” user had steps:[] in localStorage at page load + } + }; + // app.js snapshots original before mutation + ctx.window._SITE_CONFIG_ORIGINAL_HOME = { + heroTitle: 'Server Hero', + steps: [{ emoji: '๐Ÿงช', title: 'Original Step', description: 'from server' }] + }; + const ex = loadCustomizeExports(ctx); + ex.initState(); + const state = ex.getState(); + assert.strictEqual(state.home.steps.length, 1, 'should restore from snapshot, not contaminated SITE_CONFIG'); + assert.strictEqual(state.home.steps[0].title, 'Original Step'); + }); + + test('initState uses DEFAULTS.home when no SITE_CONFIG and no snapshot', () => { + const ctx = makeSandbox(); + ctx.setTimeout = function (fn) { fn(); return 1; }; + ctx.clearTimeout = function () {}; + // No SITE_CONFIG at all โ€” pure DEFAULTS + const ex = loadCustomizeExports(ctx); + ex.initState(); + const state = ex.getState(); + assert.ok(state.home.steps.length > 0, 'should use DEFAULTS.home.steps when no server config'); + assert.strictEqual(state.home.steps[0].title, 'Join the Bay Area MeshCore Discord'); + }); } // ===== APP.JS: home rehydration merge =====