perf: add projection + O(1) bucket math to muscle-heatmap endpoint

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).
This commit is contained in:
2026-04-23 15:31:53 +02:00
parent 0da3b130e4
commit 4112e38306
3 changed files with 14 additions and 17 deletions
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "homepage",
"version": "1.46.20",
"version": "1.46.21",
"private": true,
"type": "module",
"scripts": {
@@ -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<string, MuscleData> }[] = [];
// 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;