Files
homepage/src/routes/api/fitness/stats/muscle-heatmap/+server.ts
Alexander Bocken 593f211252
All checks were successful
CI / update (push) Successful in 3m31s
feat: ExerciseDB integration with muscle heatmap, SVG body filter, and enriched exercises
Integrate ExerciseDB v2 data layer (muscleMap.ts, exercisedb.ts) to enrich
the 77 static exercises with detailed muscle targeting, similar exercises,
and expand the catalog to 254 exercises. Add interactive SVG muscle body
diagrams for both the stats page heatmap and exercise list filtering, with
split front/back views flanking the exercise list on desktop. Replace body
part dropdown with unified muscle group multi-select with pill tags.
2026-04-06 20:57:49 +02:00

117 lines
3.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { requireAuth } from '$lib/server/middleware/auth';
import { dbConnect } from '$utils/db';
import { WorkoutSession } from '$models/WorkoutSession';
import { getEnrichedExerciseById } from '$lib/data/exercisedb';
import { edbMuscleToSimple, MUSCLE_GROUPS } from '$lib/data/muscleMap';
/**
* GET /api/fitness/stats/muscle-heatmap?weeks=8
*
* Returns weekly muscle usage data from workout history.
* Primary muscles get 1× set count, secondary get 0.5×.
*/
export const GET: RequestHandler = async ({ url, locals }) => {
const user = await requireAuth(locals);
const weeks = Math.min(parseInt(url.searchParams.get('weeks') || '8'), 26);
await dbConnect();
const since = new Date();
since.setDate(since.getDate() - weeks * 7);
const sessions = await WorkoutSession.find({
createdBy: user.nickname,
startTime: { $gte: since }
}).lean();
// Build weekly buckets
type MuscleData = { primary: number; secondary: number };
const weeklyData: { weekStart: string; muscles: Record<string, MuscleData> }[] = [];
// Initialize week buckets
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);
const weekStart = d.toISOString().slice(0, 10);
const muscles: Record<string, MuscleData> = {};
for (const g of MUSCLE_GROUPS) {
muscles[g] = { primary: 0, secondary: 0 };
}
weeklyData.push({ weekStart, muscles });
}
// 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 bucket = weeklyData[weekIdx].muscles;
for (const ex of session.exercises) {
const enriched = getEnrichedExerciseById(ex.exerciseId);
if (!enriched) continue;
const setCount = ex.sets?.filter((s: any) => s.completed !== false).length ?? 0;
if (setCount === 0) continue;
// Primary muscles
const primaryGroups = new Set<string>();
if (enriched.targetMusclesDetailed?.length) {
for (const m of enriched.targetMusclesDetailed) {
const group = edbMuscleToSimple(m);
primaryGroups.add(group);
if (bucket[group]) bucket[group].primary += setCount;
}
} else if (enriched.target) {
primaryGroups.add(enriched.target);
if (bucket[enriched.target]) bucket[enriched.target].primary += setCount;
}
// Secondary muscles
if (enriched.secondaryMusclesDetailed?.length) {
for (const m of enriched.secondaryMusclesDetailed) {
const group = edbMuscleToSimple(m);
if (!primaryGroups.has(group) && bucket[group]) {
bucket[group].secondary += setCount;
}
}
} else if (enriched.secondaryMuscles) {
for (const m of enriched.secondaryMuscles) {
if (!primaryGroups.has(m) && bucket[m]) {
bucket[m].secondary += setCount;
}
}
}
}
}
// Compute totals
const totals: Record<string, { primary: number; secondary: number; total: number; weeklyAvg: number }> = {};
for (const g of MUSCLE_GROUPS) {
let primary = 0, secondary = 0;
for (const w of weeklyData) {
primary += w.muscles[g].primary;
secondary += w.muscles[g].secondary;
}
const total = primary + secondary * 0.5;
totals[g] = { primary, secondary, total, weeklyAvg: total / weeks };
}
return json({ weeks: weeklyData, totals, muscleGroups: [...MUSCLE_GROUPS] });
};