From e4265f69afe8d9c40d550cf0f4ec1a5aa746f4e6 Mon Sep 17 00:00:00 2001 From: Scott Idem Date: Tue, 14 Apr 2026 21:51:18 -0400 Subject: [PATCH] fix(badges): fix stale-Dexie race in font size initialization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old guard locked on badge ID after the first liveQuery tick. If Dexie had a cached badge without cfg_json.font_sizes, the guard fired with no sizes to apply, then blocked the SWR background refresh that delivered the real saved sizes. Result: font sizes appeared unsaved on any browser that had visited the badge before sizes were set. Fix: track the cfg_json string last applied (_font_sizes_applied_cfg) instead of just the badge ID. Re-applies whenever cfg_json changes on a background refresh, but skips if local sizes have drifted from the last apply (user is mid-adjustment — auto-save will sync shortly). Co-Authored-By: Claude Sonnet 4.6 --- .../badges/[badge_id]/print/+page.svelte | 68 +++++++++++++++---- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte index 0e83aa75..c49a171b 100644 --- a/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte +++ b/src/routes/events/[event_id]/(badges)/badges/[badge_id]/print/+page.svelte @@ -103,36 +103,76 @@ function send_review_email() { // the values can be forwarded to both the controls and the badge render. // // Initialization order: badge cfg_json.font_sizes takes precedence (persisted per-badge), -// falling back to null (auto) on first load. The controls panel's "Lock Sizes" button -// saves current sizes back to cfg_json so they survive page reloads and cross-kiosk. +// falling back to null (auto) on first load. The auto-save in the controls panel writes +// adjusted sizes back to cfg_json so they survive page reloads and are shared cross-kiosk. let font_size_name: number | null = $state(null); let font_size_title: number | null = $state(null); let font_size_affiliations: number | null = $state(null); let font_size_location: number | null = $state(null); -// Track whether we've applied the saved sizes for this badge yet. -// Prevents re-applying on every liveQuery tick after the user adjusts sizes. +// Initialization tracking: +// _font_sizes_loaded_for — badge ID we last initialized for; reset on navigation. +// _font_sizes_applied_cfg — the cfg_json string we last read font_sizes from. +// Comparing this (not just the badge ID) lets us re-apply when a SWR background +// refresh delivers newly-saved sizes — fixing the stale-Dexie race where the first +// liveQuery tick has a cached badge without font_sizes, locking the badge-ID guard +// before the API refresh arrives with the real saved values. let _font_sizes_loaded_for: string | null = null; +let _font_sizes_applied_cfg: string = '$unset'; // sentinel — never equals a real cfg_json value $effect(() => { const badge = $lq__event_badge_obj; if (!badge?.event_badge_id) return; - // Only apply once per badge ID — don't clobber user adjustments on background refreshes - if (_font_sizes_loaded_for === badge.event_badge_id) return; - _font_sizes_loaded_for = badge.event_badge_id; + + const current_cfg: string | null = badge.cfg_json ?? null; + + // New badge — reset everything and fall through to apply. + if (_font_sizes_loaded_for !== badge.event_badge_id) { + _font_sizes_loaded_for = badge.event_badge_id; + _font_sizes_applied_cfg = '$unset'; + font_size_name = null; + font_size_title = null; + font_size_affiliations = null; + font_size_location = null; + } + + // cfg_json hasn't changed since we last applied — nothing to do. + if (_font_sizes_applied_cfg === current_cfg) return; + + // cfg_json changed (SWR refresh delivered new data). Before applying, check + // whether local sizes have drifted from what we last applied. If the user has + // made adjustments since our last apply, the auto-save will sync them back to + // cfg_json shortly — don't clobber their in-progress work. + if (_font_sizes_applied_cfg !== '$unset') { + try { + const prev_cfg = JSON.parse(_font_sizes_applied_cfg ?? 'null') ?? {}; + const prev_fs = prev_cfg?.font_sizes ?? null; + const user_adjusted = + font_size_name !== (prev_fs?.name ?? null) || + font_size_title !== (prev_fs?.title ?? null) || + font_size_affiliations !== (prev_fs?.affiliations ?? null) || + font_size_location !== (prev_fs?.location ?? null); + if (user_adjusted) { + // Advance the marker so we don't re-check on the next identical cfg_json. + _font_sizes_applied_cfg = current_cfg ?? '$unset'; + return; + } + } catch { /* ignore parse errors — fall through and apply */ } + } + + // Apply font sizes from the fresh cfg_json. + _font_sizes_applied_cfg = current_cfg ?? '$unset'; try { const cfg = typeof badge.cfg_json === 'string' ? JSON.parse(badge.cfg_json) : (badge.cfg_json ?? {}); const fs = cfg?.font_sizes; - if (fs) { - font_size_name = fs.name ?? null; - font_size_title = fs.title ?? null; - font_size_affiliations = fs.affiliations ?? null; - font_size_location = fs.location ?? null; - } + font_size_name = fs?.name ?? null; + font_size_title = fs?.title ?? null; + font_size_affiliations = fs?.affiliations ?? null; + font_size_location = fs?.location ?? null; } catch { - // Malformed cfg_json — stay with null (auto) for all fields + // Malformed cfg_json — stay with current values } });