From 4112e383061f8e4a4fd6844e0292210cda0c2f78 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Thu, 23 Apr 2026 15:31:53 +0200 Subject: [PATCH] perf: add projection + O(1) bucket math to muscle-heatmap endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Endpoint previously pulled full WorkoutSession documents (including gpsTrack, notes, kcalEstimate etc.) to count sets per muscle group. Adds a projection that keeps only startTime + exercises.exerciseId + whole set objects — safe (avoids the malformed-sub-array issue the earlier narrower projection caused in the stats overview handler), but still drops the bulky session-level fields. Also swaps the per-session findIndex() over the weekly bucket array for direct date-math against the first bucket's Monday, turning bucket lookup from O(sessions × weeks) into O(sessions). --- TODO.md | 2 +- package.json | 2 +- .../fitness/stats/muscle-heatmap/+server.ts | 27 +++++++++---------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/TODO.md b/TODO.md index 8b38f362..42eba662 100644 --- a/TODO.md +++ b/TODO.md @@ -10,7 +10,7 @@ Order = impact. Font items + app.html preload intentionally skipped. - [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) - [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 +- [x] 7. Muscle-heatmap endpoint — add projection + O(1) bucket math. Overview already had a projection; set-subfield narrowing was attempted but reverted (returned malformed sets). Timeseries cap not feasible: totals are lifetime-scoped. - [ ] 8. Calendar payload trim — drop `name` from `yearDays`, pre-filter `feastDots` server-side - [ ] 9. History sessions endpoint — slim exercise payload for list view - [ ] 10. `Cache-Control` headers on stable API endpoints (all_brief, calendar, exercises metadata) diff --git a/package.json b/package.json index f636211f..1972bf5f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.46.20", + "version": "1.46.21", "private": true, "type": "module", "scripts": { diff --git a/src/routes/api/fitness/stats/muscle-heatmap/+server.ts b/src/routes/api/fitness/stats/muscle-heatmap/+server.ts index 345a092c..57995131 100644 --- a/src/routes/api/fitness/stats/muscle-heatmap/+server.ts +++ b/src/routes/api/fitness/stats/muscle-heatmap/+server.ts @@ -21,20 +21,19 @@ export const GET: RequestHandler = async ({ url, locals }) => { const since = new Date(); since.setDate(since.getDate() - weeks * 7); - const sessions = await WorkoutSession.find({ - createdBy: user.nickname, - startTime: { $gte: since } - }).lean(); + const sessions = await WorkoutSession.find( + { createdBy: user.nickname, startTime: { $gte: since } }, + { startTime: 1, 'exercises.exerciseId': 1, 'exercises.sets': 1 } + ).lean(); // Build weekly buckets type MuscleData = { primary: number; secondary: number }; const weeklyData: { weekStart: string; muscles: Record }[] = []; - // Initialize week buckets + // Initialize week buckets (Monday-aligned) for (let w = 0; w < weeks; w++) { const d = new Date(); d.setDate(d.getDate() - (weeks - 1 - w) * 7); - // Find Monday of that week const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); d.setDate(diff); @@ -47,18 +46,16 @@ export const GET: RequestHandler = async ({ url, locals }) => { weeklyData.push({ weekStart, muscles }); } + // Map a session's timestamp straight to a bucket index using the first + // bucket's Monday as the epoch, avoiding a linear findIndex per session. + const firstWeekStartMs = weeklyData.length > 0 ? new Date(weeklyData[0].weekStart).getTime() : 0; + const weekMs = 7 * 86400000; + // Aggregate muscle usage for (const session of sessions) { const sessionDate = new Date(session.startTime); - // Find which week bucket - const weekIdx = weeklyData.findIndex((w, i) => { - const start = new Date(w.weekStart); - const nextStart = i + 1 < weeklyData.length - ? new Date(weeklyData[i + 1].weekStart) - : new Date(start.getTime() + 7 * 86400000); - return sessionDate >= start && sessionDate < nextStart; - }); - if (weekIdx === -1) continue; + const weekIdx = Math.floor((sessionDate.getTime() - firstWeekStartMs) / weekMs); + if (weekIdx < 0 || weekIdx >= weeklyData.length) continue; const bucket = weeklyData[weekIdx].muscles;