From de59cf49d89108a16cd678dfcacc36360f1d4842 Mon Sep 17 00:00:00 2001 From: jay Date: Fri, 12 Jun 2026 14:59:01 -0400 Subject: [PATCH] Play hub: sync game status at the hub, not only on game-open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported sync gaps: the hub showed "Play" until you opened each game, and a game synced only if you'd reopened it on that device (so a desktop win that was never reopened never reached the server). Root cause: the /play hub read card status from localStorage only and never talked to the server — sync happened exclusively inside the game components on mount. Now the hub itself reconciles every game (word 5/6 + wordsearch small/med/large) with the server on load (signed-in): pushes this device's local state and writes the merged result back to localStorage, then refreshes the cards. So statuses appear cross-device WITHOUT opening each game, and local progress uploads even for games not reopened. Word Search card status derived from the (completion- gated) ms. 237 pytest + 11 vitest green. Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/play/+page.svelte | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/frontend/src/routes/play/+page.svelte b/frontend/src/routes/play/+page.svelte index b588201..71c4ea1 100644 --- a/frontend/src/routes/play/+page.svelte +++ b/frontend/src/routes/play/+page.svelte @@ -3,6 +3,8 @@ import { goto, afterNavigate } from '$app/navigation'; import { page } from '$app/stores'; import { getJSON } from '$lib/api.js'; + import { pushGameState } from '$lib/gamesync.js'; + import { auth } from '$lib/auth.svelte.js'; import WordGame from '$lib/components/WordGame.svelte'; import WordSearchGame from '$lib/components/WordSearchGame.svelte'; @@ -50,6 +52,29 @@ return Math.floor(s / 60) + ':' + String(s % 60).padStart(2, '0'); } + // The hub itself reconciles every game with the server (signed-in), so cards + // show cross-device status WITHOUT having to open each game first, and this + // device's local progress gets uploaded even for games it hasn't reopened. + async function syncOne(g, v, key) { + let local = null; + try { local = JSON.parse(localStorage.getItem(key) || 'null'); } catch { /* ignore */ } + const merged = await pushGameState(g, v, date, local || {}); + if (!merged) return; + if (g === 'wordsearch') merged.status = merged.ms ? 'done' : 'playing'; // card reads .status + try { localStorage.setItem(key, JSON.stringify(merged)); } catch { /* ignore */ } + } + async function syncAllGames() { + if (!auth.user || !date) return; + await Promise.allSettled([ + syncOne('word', '5', `goodnews:word:5:${date}`), + syncOne('word', '6', `goodnews:word:6:${date}`), + syncOne('wordsearch', 'small', `goodnews:wordsearch:small:${date}`), + syncOne('wordsearch', 'med', `goodnews:wordsearch:med:${date}`), + syncOne('wordsearch', 'large', `goodnews:wordsearch:large:${date}`), + ]); + refreshStatus(); + } + // Hub card one-liners function wordLabel() { const a = wordStatus['5'], b = wordStatus['6']; @@ -123,6 +148,7 @@ try { date = (await getJSON('/api/puzzle/word?variant=5')).date; } catch { /* offline */ } try { wsTheme = (await getJSON('/api/puzzle/wordsearch?variant=med')).theme; } catch { /* offline */ } refreshStatus(); + syncAllGames(); // signed-in: pull cross-device status into the cards + upload local progress }); // Refresh hub/selection statuses whenever we land on a screen (incl. Back). afterNavigate(() => refreshStatus());