diff --git a/TODO.md b/TODO.md index 409986ad..8b38f362 100644 --- a/TODO.md +++ b/TODO.md @@ -9,7 +9,7 @@ Order = impact. Font items + app.html preload intentionally skipped. - [x] 3. Recipe API endpoints — drop `JSON.parse(JSON.stringify(...))` double-serialize (9 endpoints). Client-side shuffle / cache headers deferred (would require rethinking hero preload + hydration) - [x] 4. Favorites page — drop unnecessary `all_brief` fetch (verified Search uses `favoritesOnly` so `allRecipes` was redundant) - [x] 5. Replace redundant `locals.auth()` with `locals.session` across all routes (68 files, 107 sites — loaders, actions, API endpoints) -- [ ] 6. Stream fitness stats loader — return promises for slow panels +- [x] 6. Stream fitness stats loader — muscleHeatmap, nutritionStats, periods, sharedPeriods now stream via `{#await}`. `stats` still awaited (too many chart $deriveds depend on it) - [ ] 7. Overview endpoint — add `.select(...)` projection, cap timeseries window - [ ] 8. Calendar payload trim — drop `name` from `yearDays`, pre-filter `feastDots` server-side - [ ] 9. History sessions endpoint — slim exercise payload for list view diff --git a/package.json b/package.json index 99f58745..5f1e4dfc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.46.18", + "version": "1.46.19", "private": true, "type": "module", "scripts": { diff --git a/src/routes/fitness/[stats=fitnessStats]/+page.server.ts b/src/routes/fitness/[stats=fitnessStats]/+page.server.ts index e2330ca4..1def53be 100644 --- a/src/routes/fitness/[stats=fitnessStats]/+page.server.ts +++ b/src/routes/fitness/[stats=fitnessStats]/+page.server.ts @@ -2,21 +2,33 @@ import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ fetch, locals }) => { const session = locals.session ?? await locals.auth(); - const [res, goalRes, heatmapRes, nutritionRes, latestRes, periodRes, sharedRes] = await Promise.all([ + + // stats / goal / latest block the shell because the main charts, goal header, + // and body-part cards all depend on them. The heavier panels below stream. + const [res, goalRes, latestRes] = await Promise.all([ fetch('/api/fitness/stats/overview'), fetch('/api/fitness/goal'), - fetch('/api/fitness/stats/muscle-heatmap?weeks=8'), - fetch('/api/fitness/stats/nutrition'), - fetch('/api/fitness/measurements/latest'), - fetch('/api/fitness/period').catch(() => null), - fetch('/api/fitness/period/shared').catch(() => null) + fetch('/api/fitness/measurements/latest') ]); const stats = await res.json(); const goal = goalRes.ok ? await goalRes.json() : { weeklyWorkouts: null, streak: 0 }; - const muscleHeatmap = heatmapRes.ok ? await heatmapRes.json() : { weeks: [], totals: {}, muscleGroups: [] }; - const nutritionStats = nutritionRes.ok ? await nutritionRes.json() : null; const latest = latestRes.ok ? await latestRes.json() : {}; - const periods = periodRes?.ok ? (await periodRes.json()).entries : []; - const sharedPeriods = sharedRes?.ok ? (await sharedRes.json()).shared : []; - return { session, stats, goal, muscleHeatmap, nutritionStats, latest, periods, sharedPeriods }; + + // Streamed — resolved into $state-backed locals on the client so the card + // shells render immediately and fill in once the value arrives. Error + // fallbacks keep the previous empty shapes. + const muscleHeatmap = fetch('/api/fitness/stats/muscle-heatmap?weeks=8') + .then(r => r.ok ? r.json() : { weeks: [], totals: {}, muscleGroups: [] }) + .catch(() => ({ weeks: [], totals: {}, muscleGroups: [] })); + const nutritionStats = fetch('/api/fitness/stats/nutrition') + .then(r => r.ok ? r.json() : null) + .catch(() => null); + const periods = fetch('/api/fitness/period') + .then(r => r.ok ? r.json().then(j => j.entries) : []) + .catch(() => []); + const sharedPeriods = fetch('/api/fitness/period/shared') + .then(r => r.ok ? r.json().then(j => j.shared) : []) + .catch(() => []); + + return { session, stats, goal, latest, muscleHeatmap, nutritionStats, periods, sharedPeriods }; }; diff --git a/src/routes/fitness/[stats=fitnessStats]/+page.svelte b/src/routes/fitness/[stats=fitnessStats]/+page.svelte index 6611c1b6..5a3e35d1 100644 --- a/src/routes/fitness/[stats=fitnessStats]/+page.svelte +++ b/src/routes/fitness/[stats=fitnessStats]/+page.svelte @@ -120,7 +120,22 @@ }] }); - const ns = $derived(data.nutritionStats); + // Streamed panels: render empty shells on SSR/initial hydrate, then fill + // in once the server-sent promise resolves. Defaults match the previous + // error-fallback shapes so the existing `!= null` checks cascade to the + // "—" branches while the data is in flight. + /** @type {any} */ + let ns = $state({}); + /** @type {{ weeks: any[]; totals: any; muscleGroups: any[] }} */ + let muscleHeatmapData = $state({ weeks: [], totals: {}, muscleGroups: [] }); + /** @type {any[]} */ + let periodsData = $state([]); + /** @type {any[]} */ + let sharedPeriodsData = $state([]); + $effect(() => { Promise.resolve(data.nutritionStats).then(v => { ns = v ?? {}; }); }); + $effect(() => { Promise.resolve(data.muscleHeatmap).then(v => { muscleHeatmapData = v ?? { weeks: [], totals: {}, muscleGroups: [] }; }); }); + $effect(() => { Promise.resolve(data.periods).then(v => { periodsData = v ?? []; }); }); + $effect(() => { Promise.resolve(data.sharedPeriods).then(v => { sharedPeriodsData = v ?? []; }); }); const hasSma = $derived(stats.weightChart?.sma?.some((/** @type {any} */ v) => v !== null)); @@ -362,7 +377,6 @@ {/if}
- {#if ns}
{#if ns.avgProteinPerKg != null} @@ -483,11 +497,10 @@ {/each}
- {/if}

{t('muscle_balance', lang)}

- +
@@ -540,10 +553,10 @@ {/if} {#if data.goal?.sex === 'female'} - + {/if} - {#each data.sharedPeriods ?? [] as shared (shared.owner)} + {#each sharedPeriodsData as shared (shared.owner)} {/each}