perf: parallelize DB queries across routes, clean up fitness UI

Parallelize sequential DB queries in 11 API routes and page loaders
using Promise.all — measurements/latest, stats/overview, goal streak,
exercises, sessions, task stats, monthly expenses, icon page, offline-db.

Move calorie tracking out of /fitness/measure (now under /fitness/nutrition
only). Remove fade-in entrance animations from nutrition page.

Progressive streak computation: scan 3 months first, widen only if needed.

Bump versions to 1.1.1 / 0.2.1.
This commit is contained in:
2026-04-06 13:12:02 +02:00
parent fac140b793
commit 98c67070f6
18 changed files with 147 additions and 254 deletions
@@ -6,15 +6,13 @@ import { dbConnect } from '$utils/db';
export const GET: RequestHandler = async () => {
await dbConnect();
// Fetch brief recipes (for lists/filtering)
// Include images for thumbnail caching during offline sync
const briefRecipes = await Recipe.find(
{},
'name short_name tags category icon description season dateModified images translations'
).lean() as unknown as BriefRecipeType[];
// Fetch full recipes with populated base recipe references
const fullRecipes = await Recipe.find({})
// Fetch brief and full recipes in parallel
const [briefRecipes, fullRecipes] = await Promise.all([
Recipe.find(
{},
'name short_name tags category icon description season dateModified images translations'
).lean() as unknown as Promise<BriefRecipeType[]>,
Recipe.find({})
.populate({
path: 'ingredients.baseRecipeRef',
select: 'short_name name ingredients translations',
@@ -39,7 +37,8 @@ export const GET: RequestHandler = async () => {
}
}
})
.lean() as unknown as RecipeModelType[];
.lean() as unknown as Promise<RecipeModelType[]>
]);
// Map populated refs to resolvedRecipe field (same as individual item endpoint)
function mapBaseRecipeRefs(items: any[]): any[] {
@@ -20,20 +20,12 @@ export const GET: RequestHandler = async ({ url, locals }) => {
const startDate = new Date();
startDate.setMonth(startDate.getMonth() - monthsBack);
const totalPayments = await Payment.countDocuments();
const paymentsInRange = await Payment.countDocuments({
date: {
$gte: startDate,
$lte: endDate
}
});
const expensePayments = await Payment.countDocuments({
date: {
$gte: startDate,
$lte: endDate
},
category: { $ne: 'settlement' }
});
const dateRange = { $gte: startDate, $lte: endDate };
const [totalPayments, paymentsInRange, expensePayments] = await Promise.all([
Payment.countDocuments(),
Payment.countDocuments({ date: dateRange }),
Payment.countDocuments({ date: dateRange, category: { $ne: 'settlement' } })
]);
// Aggregate payments by month and category
const pipeline = [
+6 -7
View File
@@ -46,13 +46,12 @@ export const GET: RequestHandler = async ({ url, locals }) => {
exerciseQuery = exerciseQuery.sort({ name: 1 });
}
const exercises = await exerciseQuery
.limit(limit)
.skip(offset)
.select('exerciseId name gifUrl bodyPart equipment target difficulty');
const total = await Exercise.countDocuments(query);
const [exercises, total] = await Promise.all([
exerciseQuery.limit(limit).skip(offset)
.select('exerciseId name gifUrl bodyPart equipment target difficulty'),
Exercise.countDocuments(query)
]);
return json({ exercises, total, limit, offset });
} catch (error) {
console.error('Error fetching exercises:', error);
+35 -49
View File
@@ -73,62 +73,48 @@ export const PUT: RequestHandler = async ({ request, locals }) => {
};
async function computeStreak(username: string, weeklyGoal: number): Promise<number> {
// Get weekly workout counts going back up to 2 years
const cutoff = new Date();
cutoff.setFullYear(cutoff.getFullYear() - 2);
const weeklyAgg = await WorkoutSession.aggregate([
{
$match: {
createdBy: username,
startTime: { $gte: cutoff }
}
},
{
$group: {
_id: {
year: { $isoWeekYear: '$startTime' },
week: { $isoWeek: '$startTime' }
},
count: { $sum: 1 }
}
},
{
$sort: { '_id.year': -1, '_id.week': -1 }
}
]);
// Build a set of weeks that met the goal
const metGoal = new Set<string>();
for (const item of weeklyAgg) {
if (item.count >= weeklyGoal) {
metGoal.add(`${item._id.year}-${item._id.week}`);
}
}
// Walk backwards week-by-week counting consecutive weeks that met the goal.
// Current (incomplete) week counts if it already meets the goal, otherwise skip it.
// Get weekly workout counts — only scan back enough to find the streak break.
// Start with 13 weeks; if the streak fills the entire window, widen the search.
const now = new Date();
let streak = 0;
const currentKey = isoWeekKey(now);
const currentWeekMet = metGoal.has(currentKey);
for (let months = 3; months <= 24; months += 6) {
const cutoff = new Date(now);
cutoff.setMonth(cutoff.getMonth() - months);
// If current week already met: count it, then check previous weeks.
// If not: start checking from last week (current week still in progress).
if (currentWeekMet) streak = 1;
const weeklyAgg = await WorkoutSession.aggregate([
{ $match: { createdBy: username, startTime: { $gte: cutoff } } },
{ $group: { _id: { year: { $isoWeekYear: '$startTime' }, week: { $isoWeek: '$startTime' } }, count: { $sum: 1 } } },
{ $sort: { '_id.year': -1, '_id.week': -1 } }
]);
for (let i = 1; i <= 104; i++) {
const weekDate = new Date(now);
weekDate.setDate(weekDate.getDate() - i * 7);
if (metGoal.has(isoWeekKey(weekDate))) {
streak++;
} else {
break;
const metGoal = new Set<string>();
for (const item of weeklyAgg) {
if (item.count >= weeklyGoal) metGoal.add(`${item._id.year}-${item._id.week}`);
}
let streak = 0;
const currentKey = isoWeekKey(now);
if (metGoal.has(currentKey)) streak = 1;
const maxWeeks = Math.ceil(months * 4.35);
let foundBreak = false;
for (let i = 1; i <= maxWeeks; i++) {
const weekDate = new Date(now);
weekDate.setDate(weekDate.getDate() - i * 7);
if (metGoal.has(isoWeekKey(weekDate))) {
streak++;
} else {
foundBreak = true;
break;
}
}
// If we found where the streak broke, we're done
if (foundBreak || streak < maxWeeks) return streak;
// Otherwise widen the window and try again
}
return streak;
return 104; // Max 2-year streak
}
function isoWeekKey(date: Date): string {
@@ -22,13 +22,10 @@ export const GET: RequestHandler = async ({ url, locals }) => {
query.date = dateFilter;
}
const measurements = await BodyMeasurement.find(query)
.sort({ date: -1 })
.skip(offset)
.limit(limit)
.lean();
const total = await BodyMeasurement.countDocuments(query);
const [measurements, total] = await Promise.all([
BodyMeasurement.find(query).sort({ date: -1 }).skip(offset).limit(limit).lean(),
BodyMeasurement.countDocuments(query)
]);
return json({ measurements, total, limit, offset });
};
@@ -8,38 +8,16 @@ export const GET: RequestHandler = async ({ locals }) => {
const user = await requireAuth(locals);
await dbConnect();
// Get latest measurement that has each field
const latestWithWeight = await BodyMeasurement.findOne({
createdBy: user.nickname,
weight: { $exists: true, $ne: null }
})
.sort({ date: -1 })
.select('weight date')
.lean();
const latestWithBodyFat = await BodyMeasurement.findOne({
createdBy: user.nickname,
bodyFatPercent: { $exists: true, $ne: null }
})
.sort({ date: -1 })
.select('bodyFatPercent date')
.lean();
const latestWithCalories = await BodyMeasurement.findOne({
createdBy: user.nickname,
caloricIntake: { $exists: true, $ne: null }
})
.sort({ date: -1 })
.select('caloricIntake date')
.lean();
const latestWithMeasurements = await BodyMeasurement.findOne({
createdBy: user.nickname,
measurements: { $exists: true, $ne: null }
})
.sort({ date: -1 })
.select('measurements date')
.lean();
// Get latest measurement that has each field (parallel)
const base = { createdBy: user.nickname };
const [latestWithWeight, latestWithBodyFat, latestWithMeasurements] = await Promise.all([
BodyMeasurement.findOne({ ...base, weight: { $exists: true, $ne: null } })
.sort({ date: -1 }).select('weight date').lean(),
BodyMeasurement.findOne({ ...base, bodyFatPercent: { $exists: true, $ne: null } })
.sort({ date: -1 }).select('bodyFatPercent date').lean(),
BodyMeasurement.findOne({ ...base, measurements: { $exists: true, $ne: null } })
.sort({ date: -1 }).select('measurements date').lean(),
]);
return json({
weight: latestWithWeight
@@ -48,9 +26,6 @@ export const GET: RequestHandler = async ({ locals }) => {
bodyFatPercent: latestWithBodyFat
? { value: latestWithBodyFat.bodyFatPercent, date: latestWithBodyFat.date }
: null,
caloricIntake: latestWithCalories
? { value: latestWithCalories.caloricIntake, date: latestWithCalories.date }
: null,
measurements: latestWithMeasurements
? {
value: latestWithMeasurements.measurements,
+7 -8
View File
@@ -28,14 +28,13 @@ export const GET: RequestHandler = async ({ url, locals }) => {
const limit = parseInt(url.searchParams.get('limit') || '20');
const offset = parseInt(url.searchParams.get('offset') || '0');
const sessions = await WorkoutSession.find({ createdBy: session.user.nickname })
.select('-exercises.gpsTrack -gpsTrack')
.sort({ startTime: -1 })
.limit(limit)
.skip(offset);
const total = await WorkoutSession.countDocuments({ createdBy: session.user.nickname });
const query = { createdBy: session.user.nickname };
const [sessions, total] = await Promise.all([
WorkoutSession.find(query).select('-exercises.gpsTrack -gpsTrack')
.sort({ startTime: -1 }).limit(limit).skip(offset),
WorkoutSession.countDocuments(query)
]);
return json({ sessions, total, limit, offset });
} catch (error) {
console.error('Error fetching workout sessions:', error);
@@ -16,31 +16,13 @@ export const GET: RequestHandler = async ({ locals }) => {
const tenWeeksAgo = new Date();
tenWeeksAgo.setDate(tenWeeksAgo.getDate() - 70);
const totalWorkouts = await WorkoutSession.countDocuments({ createdBy: user.nickname });
const weeklyAgg = await WorkoutSession.aggregate([
{
$match: {
createdBy: user.nickname,
startTime: { $gte: tenWeeksAgo }
}
},
{
$group: {
_id: {
year: { $isoWeekYear: '$startTime' },
week: { $isoWeek: '$startTime' }
},
count: { $sum: 1 }
}
},
{
$sort: { '_id.year': 1, '_id.week': 1 }
}
]);
// Fetch user demographics for kcal estimation
const [goal, latestMeasurement] = await Promise.all([
const [totalWorkouts, weeklyAgg, goal, latestMeasurement] = await Promise.all([
WorkoutSession.countDocuments({ createdBy: user.nickname }),
WorkoutSession.aggregate([
{ $match: { createdBy: user.nickname, startTime: { $gte: tenWeeksAgo } } },
{ $group: { _id: { year: { $isoWeekYear: '$startTime' }, week: { $isoWeek: '$startTime' } }, count: { $sum: 1 } } },
{ $sort: { '_id.year': 1, '_id.week': 1 } }
]),
FitnessGoal.findOne({ username: user.nickname }).lean() as any,
BodyMeasurement.findOne(
{ createdBy: user.nickname, weight: { $ne: null } },
@@ -57,10 +39,19 @@ export const GET: RequestHandler = async ({ locals }) => {
// Lifetime totals: tonnage lifted + cardio km + kcal estimate
// Use stored kcalEstimate when available; fall back to on-the-fly for legacy sessions
const allSessions = await WorkoutSession.find(
{ createdBy: user.nickname },
{ 'exercises.exerciseId': 1, 'exercises.sets': 1, 'exercises.totalDistance': 1, kcalEstimate: 1 }
).lean();
const DISPLAY_LIMIT = 30;
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
const [allSessions, weightMeasurements] = await Promise.all([
WorkoutSession.find(
{ createdBy: user.nickname },
{ 'exercises.exerciseId': 1, 'exercises.sets': 1, 'exercises.totalDistance': 1, kcalEstimate: 1 }
).lean(),
BodyMeasurement.find(
{ createdBy: user.nickname, weight: { $ne: null } },
{ date: 1, weight: 1, _id: 0 }
).sort({ date: -1 }).limit(DISPLAY_LIMIT + SMA_LOOKBACK).lean()
]);
weightMeasurements.reverse(); // back to chronological order
let totalTonnage = 0;
let totalCardioKm = 0;
@@ -146,18 +137,6 @@ export const GET: RequestHandler = async ({ locals }) => {
upper: totalKcal + combinedMargin,
};
// Fetch extra measurements beyond the display limit to fill the SMA lookback window
const DISPLAY_LIMIT = 30;
const SMA_LOOKBACK = 6; // w - 1 where w = 7 max
const weightMeasurements = await BodyMeasurement.find(
{ createdBy: user.nickname, weight: { $ne: null } },
{ date: 1, weight: 1, _id: 0 }
)
.sort({ date: -1 })
.limit(DISPLAY_LIMIT + SMA_LOOKBACK)
.lean();
weightMeasurements.reverse(); // back to chronological order
// Split into lookback-only (not displayed) and display portions
const displayStart = Math.max(0, weightMeasurements.length - DISPLAY_LIMIT);
+11 -17
View File
@@ -9,25 +9,19 @@ export const GET: RequestHandler = async ({ locals }) => {
await dbConnect();
// Completions per user
const userStats = await TaskCompletion.aggregate([
{ $group: { _id: '$completedBy', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
const [userStats, userStickers, recentCompletions] = await Promise.all([
TaskCompletion.aggregate([
{ $group: { _id: '$completedBy', count: { $sum: 1 } } },
{ $sort: { count: -1 } }
]),
TaskCompletion.aggregate([
{ $match: { stickerId: { $exists: true, $ne: null } } },
{ $group: { _id: { user: '$completedBy', sticker: '$stickerId' }, count: { $sum: 1 } } },
{ $sort: { '_id.user': 1, count: -1 } }
]),
TaskCompletion.find().sort({ completedAt: -1 }).limit(500).lean()
]);
// Stickers per user
const userStickers = await TaskCompletion.aggregate([
{ $match: { stickerId: { $exists: true, $ne: null } } },
{ $group: { _id: { user: '$completedBy', sticker: '$stickerId' }, count: { $sum: 1 } } },
{ $sort: { '_id.user': 1, count: -1 } }
]);
// Recent completions (enough for ~3 months of calendar)
const recentCompletions = await TaskCompletion.find()
.sort({ completedAt: -1 })
.limit(500)
.lean();
return json({ userStats, userStickers, recentCompletions });
};