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:
@@ -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 = [
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user