From 456b1a0547c97a6e7a33f29caaca33417ba0fdb3 Mon Sep 17 00:00:00 2001 From: jay Date: Fri, 12 Jun 2026 09:52:01 -0400 Subject: [PATCH] Home: make instant-paint boundary-aware (Codex) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex caught a trust bug: instant-painting a brief saved under OLD boundaries could briefly flash content the reader's current settings should hide — and boundaries are trust-critical for this product. Add a filter signature (prefs param + sorted dismissals) saved alongside the brief; instant-paint and the merge-fallback only reuse a saved brief when the signature still matches the current settings. A mismatch falls through to "Gathering…" + a fresh fetch. Also closes the same latent leak in the merge's `?? it` fallback. Briefs saved before this change lack a sig → won't instant-paint until re-saved (fails safe). Co-Authored-By: Claude Opus 4.8 --- frontend/src/routes/+page.svelte | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 579f7fb..1367754 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -217,6 +217,13 @@ return P.param(P.merge(prefs.data, viewFilter())); } + // Signature of the filters that shaped a brief — boundaries/prefs + dismissals. + // Instant-paint and the merge only reuse a saved brief when this still matches, + // so a boundary change can never briefly resurface content it should now hide. + function briefSig() { + return P.param(prefs.data) + '|' + Array.from(dismissed).sort().join(','); + } + async function loadToday(fresh) { const q = P.param(prefs.data); const ex = Array.from(dismissed).join(','); @@ -227,17 +234,22 @@ if (brief) return; // already showing a saved brief — a failed background refresh stays invisible throw e; // true first load with nothing painted → let the caller surface the error } + const sig = briefSig(); const view = P.loadJSON(BRIEF_VIEW_KEY, null); + // Reuse the saved arrangement only when BOTH the server brief and the reader's + // filter signature are unchanged — otherwise the merge's `?? it` fallback could + // re-add a story the current boundaries now hide. const sameServerBrief = - view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at; + view && view.generated_at && fetched.generated_at && + view.generated_at === fetched.generated_at && view.sig === sig; if (!fresh && sameServerBrief && Array.isArray(view.items) && view.items.length) { const freshById = new Map(fetched.items.map((a) => [a.id, a])); const items = view.items.map((it) => freshById.get(it.id) ?? it); brief = { ...fetched, items }; - P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items }); + P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items, sig }); } else { brief = fetched; - P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items }); + P.saveJSON(BRIEF_VIEW_KEY, { generated_at: fetched.generated_at, items: fetched.items, sig }); } heroIdx = 0; markDisplayed(brief.items); @@ -441,7 +453,10 @@ // shows on a genuine first visit with nothing cached yet. if (selected === 'today') { const cached = P.loadJSON(BRIEF_VIEW_KEY, null); - if (cached && Array.isArray(cached.items) && cached.items.length) { + // Only paint instantly if the saved brief was shaped by the SAME + // boundaries/prefs/dismissals — never flash content the current settings + // should hide. A mismatch falls through to "Gathering…" + a fresh fetch. + if (cached && Array.isArray(cached.items) && cached.items.length && cached.sig === briefSig()) { brief = cached; heroIdx = 0; loading = false;