From 6b6684a1199ca78095b25c3a5d8d650ca7a3de6d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 4 Jun 2026 10:02:14 +0100 Subject: [PATCH] Always apply the server full-width mode after auth (#1130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Back-port v5 fix 6c3a2cd70 to v6. Adds layoutStore.applyServerMode, which applies the server's canonical fullWidthMode authoritatively (even when a local preference exists), and the post-auth path now calls it with the already-fetched system settings instead of loadFromServer() — which made a redundant API call and short-circuited on any stale localStorage entry, so a user's server-side full-width setting was not honored after login/reload. Exports createLayoutStore and adds a regression test. --- frontend-modern/src/useAppRuntimeState.ts | 5 +- .../src/utils/__tests__/layout.test.ts | 51 +++++++++++++++++++ frontend-modern/src/utils/layout.ts | 27 +++++++--- 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 frontend-modern/src/utils/__tests__/layout.test.ts diff --git a/frontend-modern/src/useAppRuntimeState.ts b/frontend-modern/src/useAppRuntimeState.ts index 2ce7e94db..e9589102d 100644 --- a/frontend-modern/src/useAppRuntimeState.ts +++ b/frontend-modern/src/useAppRuntimeState.ts @@ -279,7 +279,10 @@ export const useAppRuntimeState = () => { updateSystemSettingsFromResponse(systemSettings); applyServerThemeIfAllowed(systemSettings.theme); setHasLoadedServerTheme(true); - layoutStore.loadFromServer(); + // Apply the server's canonical full-width mode using the settings we just + // fetched, so it is honored after auth even if a stale localStorage + // preference exists (#1130) — loadFromServer() would short-circuit on it. + layoutStore.applyServerMode(systemSettings.fullWidthMode); } catch (error) { logger.error('Failed to load system settings from server', error); markSystemSettingsLoadedWithDefaults(); diff --git a/frontend-modern/src/utils/__tests__/layout.test.ts b/frontend-modern/src/utils/__tests__/layout.test.ts new file mode 100644 index 000000000..eb551b6ac --- /dev/null +++ b/frontend-modern/src/utils/__tests__/layout.test.ts @@ -0,0 +1,51 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +vi.mock('@/api/settings', () => ({ + SettingsAPI: { getSystemSettings: vi.fn() }, +})); + +vi.mock('@/utils/logger', () => ({ + logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }, +})); + +import { createLayoutStore } from '@/utils/layout'; +import { STORAGE_KEYS } from '@/utils/localStorage'; + +describe('layout store applyServerMode (#1130)', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('applies the server full-width value even when a stale local preference exists', () => { + // Stale local preference that loadFromServer() would otherwise honor. + localStorage.setItem(STORAGE_KEYS.FULL_WIDTH_MODE, 'default'); + + const store = createLayoutStore(); + expect(store.isFullWidth()).toBe(false); + + store.applyServerMode(true); + + expect(store.isFullWidth()).toBe(true); + expect(localStorage.getItem(STORAGE_KEYS.FULL_WIDTH_MODE)).toBe('full-width'); + }); + + it('applies a server default that overrides a local full-width preference', () => { + localStorage.setItem(STORAGE_KEYS.FULL_WIDTH_MODE, 'full-width'); + + const store = createLayoutStore(); + expect(store.isFullWidth()).toBe(true); + + store.applyServerMode(false); + + expect(store.isFullWidth()).toBe(false); + }); + + it('leaves the current mode unchanged when the server value is undefined', () => { + localStorage.setItem(STORAGE_KEYS.FULL_WIDTH_MODE, 'full-width'); + + const store = createLayoutStore(); + store.applyServerMode(undefined); + + expect(store.isFullWidth()).toBe(true); + }); +}); diff --git a/frontend-modern/src/utils/layout.ts b/frontend-modern/src/utils/layout.ts index f9560f7ae..55d0044ad 100644 --- a/frontend-modern/src/utils/layout.ts +++ b/frontend-modern/src/utils/layout.ts @@ -12,7 +12,7 @@ export type LayoutMode = 'default' | 'full-width'; * Creates a reactive store for layout mode preference * Syncs with both localStorage (for immediate access) and server (for persistence across updates) */ -function createLayoutStore() { +export function createLayoutStore() { const stored = localStorage.getItem(STORAGE_KEYS.FULL_WIDTH_MODE); const initialMode: LayoutMode = stored === 'full-width' ? 'full-width' : 'default'; @@ -38,6 +38,22 @@ function createLayoutStore() { const isFullWidth = () => mode() === 'full-width'; + /** + * Apply the server's canonical full-width preference (called after auth with + * the already-fetched system settings). Unlike loadFromServer this is + * authoritative: it applies the server value even when a local preference + * exists, so the server setting is honored after login/reload (#1130). + */ + const applyServerMode = (serverFullWidthMode: boolean | undefined) => { + if (serverFullWidthMode !== undefined) { + const serverMode: LayoutMode = serverFullWidthMode ? 'full-width' : 'default'; + localStorage.setItem(STORAGE_KEYS.FULL_WIDTH_MODE, serverMode); + setModeInternal(serverMode); + logger.debug('Applied full-width mode from server', { mode: serverMode }); + } + setHasLoadedFromServer(true); + }; + /** * Load full-width preference from server (called after auth) * Only uses server preference if no local preference exists @@ -50,13 +66,7 @@ function createLayoutStore() { try { const settings = await SettingsAPI.getSystemSettings(); - if (settings.fullWidthMode !== undefined) { - const serverMode: LayoutMode = settings.fullWidthMode ? 'full-width' : 'default'; - localStorage.setItem(STORAGE_KEYS.FULL_WIDTH_MODE, serverMode); - setModeInternal(serverMode); - logger.debug('Loaded full-width mode from server', { mode: serverMode }); - } - setHasLoadedFromServer(true); + applyServerMode(settings.fullWidthMode); } catch (error) { logger.warn('Failed to load full-width mode from server', error); } @@ -67,6 +77,7 @@ function createLayoutStore() { setMode, toggle, isFullWidth, + applyServerMode, loadFromServer, }; }