diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 3fa1797..579f7fb 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -220,7 +220,13 @@ async function loadToday(fresh) { const q = P.param(prefs.data); const ex = Array.from(dismissed).join(','); - const fetched = await getJSON(`/api/brief?limit=7${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`); + let fetched; + try { + fetched = await getJSON(`/api/brief?limit=7${q ? '&' + q : ''}${ex ? '&exclude=' + ex : ''}`); + } catch (e) { + 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 view = P.loadJSON(BRIEF_VIEW_KEY, null); const sameServerBrief = view && view.generated_at && fetched.generated_at && view.generated_at === fetched.generated_at; @@ -429,6 +435,18 @@ dismissed = new Set(P.loadJSON(DISMISSED_KEY, [])); refreshAuth(); trackVisit(); + // Instant paint: render the last saved Today brief immediately and refresh + // it behind the scenes, so the first view never blocks on a (personalized, + // origin-bound) /api/brief request. "Gathering the good news…" then only + // 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) { + brief = cached; + heroIdx = 0; + loading = false; + } + } // Critical path: moods + categories (needed to resolve a mood/topic view) // load in PARALLEL, then the requested view. Every getJSON times out, and // loading ALWAYS clears in finally, so the page can never get stuck on @@ -441,7 +459,7 @@ moods = m; topics = c.topics; await loadView(selected); } catch (e) { - error = 'Could not reach Upbeat Bytes.'; + if (!brief) error = 'Could not reach Upbeat Bytes.'; // keep painted content if a refresh failed } finally { loading = false; }