From f3effe93900bafab6d4cc526636aacce12d7736d Mon Sep 17 00:00:00 2001 From: Till JS Date: Thu, 9 Apr 2026 19:41:31 +0200 Subject: [PATCH] fix(mana/web/news): instant onboarding handoff to feed branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Fertig" button needed two clicks before the wizard would disappear. Cause: the wizard branch is gated on `prefs.onboardingCompleted` which comes out of a Dexie liveQuery. liveQuery debounces and emits the post-write value ~50-100ms after the table.update() returns, so the first click writes the row but the page re-renders the same wizard step until the next liveQuery tick. Users instinctively click again before noticing. Fix: a local `onboardingJustFinished` $state override that flips to true synchronously inside `finishOnboarding()`. The wizard branch is now hidden by `!(prefs.onboardingCompleted || onboardingJustFinished)`, so the feed appears the instant the write resolves. The liveQuery catches up a moment later but its update is a no-op because the override and the queried value agree. Also: - `onboardingSubmitting` $state guard so a panicked double-click gets ignored, and the button shows "Speichere…" while the write is in flight (visual feedback that something is happening) - Eagerly call `feedCacheStore.refresh()` from finishOnboarding so the feed isn't empty for the moment the layout's $effect needs to notice the prefs change. The store's inFlight guard makes the redundant layout-effect refresh a no-op. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../web/src/routes/(app)/news/+page.svelte | 68 +++++++++++++++---- 1 file changed, 56 insertions(+), 12 deletions(-) diff --git a/apps/mana/apps/web/src/routes/(app)/news/+page.svelte b/apps/mana/apps/web/src/routes/(app)/news/+page.svelte index eed0da150..a2cedd06a 100644 --- a/apps/mana/apps/web/src/routes/(app)/news/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/news/+page.svelte @@ -39,6 +39,14 @@ let pickedLanguages = $state(['de', 'en']); let pickedBlocked = $state([]); let onboardingStep = $state<1 | 2 | 3>(1); + // Local "just finished" override so the wizard hides immediately on + // click instead of waiting for Dexie's liveQuery to debounce + emit + // the new prefs.onboardingCompleted = true. Without this, the user + // clicks "Fertig", the write goes through, but the UI re-renders + // the same wizard step until the next liveQuery tick (~50-100ms), + // so people instinctively click again before noticing the change. + let onboardingJustFinished = $state(false); + let onboardingSubmitting = $state(false); function toggleTopic(t: Topic) { pickedTopics = pickedTopics.includes(t) @@ -57,20 +65,49 @@ } async function finishOnboarding() { - // $state.snapshot strips the Svelte 5 reactive proxies — without it - // the arrays travel into Dexie hooks as proxies and trip - // DataCloneError on the structured-clone into _pendingChanges. - await preferencesStore.completeOnboarding({ - topics: $state.snapshot(pickedTopics) as Topic[], - languages: $state.snapshot(pickedLanguages) as Language[], - blockedSources: $state.snapshot(pickedBlocked) as string[], - }); - // The +layout effect will pick up the new prefs and refresh. + if (onboardingSubmitting) return; + onboardingSubmitting = true; + try { + // $state.snapshot strips the Svelte 5 reactive proxies — without it + // the arrays travel into Dexie hooks as proxies and trip + // DataCloneError on the structured-clone into _pendingChanges. + const topicsSnap = $state.snapshot(pickedTopics) as Topic[]; + const langsSnap = $state.snapshot(pickedLanguages) as Language[]; + const blockedSnap = $state.snapshot(pickedBlocked) as string[]; + + await preferencesStore.completeOnboarding({ + topics: topicsSnap, + languages: langsSnap, + blockedSources: blockedSnap, + }); + + // Flip to the feed branch immediately. Without this we'd be at + // the mercy of Dexie's liveQuery debounce — the prefs read + // behind `prefs.onboardingCompleted` only updates a few ticks + // after the write, so the wizard would re-render the same + // step for ~50-100ms and the user would click "Fertig" twice. + onboardingJustFinished = true; + + // Eagerly trigger the first feed pull instead of waiting for + // the layout's $effect to notice the prefs change. The layout + // effect WILL also fire shortly after, but its refresh is a + // no-op via the store's inFlight guard. + void feedCacheStore.refresh({ + topics: topicsSnap, + lang: langsSnap.length === 1 ? langsSnap[0] : 'all', + }); + } finally { + onboardingSubmitting = false; + } } // ─── Feed branch ────────────────────────────────────────── const reactedIds = $derived(buildReactedIds(reactions)); - const ranked = $derived(prefs.onboardingCompleted ? rankFeed(pool, { prefs, reactedIds }) : []); + // Treat the local "just finished" override as fully onboarded so the + // feed renders immediately after the user clicks Fertig, before the + // liveQuery has had a chance to refresh prefs. + const isOnboarded = $derived(prefs.onboardingCompleted || onboardingJustFinished); + const ranked = $derived(isOnboarded ? rankFeed(pool, { prefs, reactedIds }) : []); async function react( article: LocalCachedArticle, @@ -107,7 +144,7 @@
- {#if !prefs.onboardingCompleted} + {#if !isOnboarded}

Willkommen beim News Hub

@@ -216,7 +253,14 @@ - +
{/if}