Play hub: sync game status at the hub, not only on game-open

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 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-12 14:59:01 -04:00
parent 065ab98598
commit de59cf49d8
+26
View File
@@ -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());