From 201847400e492c39e8d9d36081d82d6604dab6ed Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 6 Apr 2026 13:12:02 +0200 Subject: [PATCH] perf: parallelize DB queries across routes, clean up fitness UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- .../icon/[icon]/+page.server.ts | 10 +-- .../offline-db/+server.ts | 19 ++--- .../api/cospend/monthly-expenses/+server.ts | 20 ++--- src/routes/api/fitness/exercises/+server.ts | 13 ++- src/routes/api/fitness/goal/+server.ts | 84 ++++++++----------- .../api/fitness/measurements/+server.ts | 11 +-- .../fitness/measurements/latest/+server.ts | 45 +++------- src/routes/api/fitness/sessions/+server.ts | 15 ++-- .../api/fitness/stats/overview/+server.ts | 61 +++++--------- src/routes/api/tasks/stats/+server.ts | 28 +++---- .../[measure=fitnessMeasure]/+page.svelte | 7 +- .../[measure=fitnessMeasure]/add/+page.svelte | 8 -- .../edit/[id]/+page.svelte | 9 -- .../+page.server.ts | 42 +++++----- .../[nutrition=fitnessNutrition]/+page.svelte | 23 ++--- 18 files changed, 147 insertions(+), 254 deletions(-) diff --git a/package.json b/package.json index d8fff0d..9c5b7f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.1.0", + "version": "1.1.1", "private": true, "type": "module", "scripts": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5a2c066..26fd542 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bocken" -version = "0.2.0" +version = "0.2.1" edition = "2021" [lib] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index faffa30..db23691 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "Bocken", "identifier": "org.bocken.app", - "version": "0.2.0", + "version": "0.2.1", "build": { "devUrl": "http://192.168.1.4:5173", "frontendDist": "https://bocken.org" diff --git a/src/routes/[recipeLang=recipeLang]/icon/[icon]/+page.server.ts b/src/routes/[recipeLang=recipeLang]/icon/[icon]/+page.server.ts index be76bb0..e0effe9 100644 --- a/src/routes/[recipeLang=recipeLang]/icon/[icon]/+page.server.ts +++ b/src/routes/[recipeLang=recipeLang]/icon/[icon]/+page.server.ts @@ -4,13 +4,9 @@ import { getUserFavorites, addFavoriteStatusToRecipes } from "$lib/server/favori export const load: PageServerLoad = async ({ fetch, locals, params }) => { const apiBase = `/api/${params.recipeLang}`; - const res_season = await fetch(`${apiBase}/items/icon/` + params.icon); - const res_icons = await fetch(`/api/rezepte/items/icon`); // Icons are shared across languages - const icons = await res_icons.json(); - const item_season = await res_season.json(); - - // Get user favorites and session - const [userFavorites, session] = await Promise.all([ + const [item_season, icons, userFavorites, session] = await Promise.all([ + fetch(`${apiBase}/items/icon/` + params.icon).then(r => r.json()), + fetch(`/api/rezepte/items/icon`).then(r => r.json()), getUserFavorites(fetch, locals), locals.auth() ]); diff --git a/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts b/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts index b1f0e1b..60a3e7b 100644 --- a/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts +++ b/src/routes/api/[recipeLang=recipeLang]/offline-db/+server.ts @@ -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, + 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 + ]); // Map populated refs to resolvedRecipe field (same as individual item endpoint) function mapBaseRecipeRefs(items: any[]): any[] { diff --git a/src/routes/api/cospend/monthly-expenses/+server.ts b/src/routes/api/cospend/monthly-expenses/+server.ts index c6c76f0..092dab9 100644 --- a/src/routes/api/cospend/monthly-expenses/+server.ts +++ b/src/routes/api/cospend/monthly-expenses/+server.ts @@ -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 = [ diff --git a/src/routes/api/fitness/exercises/+server.ts b/src/routes/api/fitness/exercises/+server.ts index 29d675c..bcd2b6a 100644 --- a/src/routes/api/fitness/exercises/+server.ts +++ b/src/routes/api/fitness/exercises/+server.ts @@ -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); diff --git a/src/routes/api/fitness/goal/+server.ts b/src/routes/api/fitness/goal/+server.ts index 05167f2..f443c87 100644 --- a/src/routes/api/fitness/goal/+server.ts +++ b/src/routes/api/fitness/goal/+server.ts @@ -73,62 +73,48 @@ export const PUT: RequestHandler = async ({ request, locals }) => { }; async function computeStreak(username: string, weeklyGoal: number): Promise { - // 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(); - 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(); + 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 { diff --git a/src/routes/api/fitness/measurements/+server.ts b/src/routes/api/fitness/measurements/+server.ts index f2dc960..f3978a4 100644 --- a/src/routes/api/fitness/measurements/+server.ts +++ b/src/routes/api/fitness/measurements/+server.ts @@ -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 }); }; diff --git a/src/routes/api/fitness/measurements/latest/+server.ts b/src/routes/api/fitness/measurements/latest/+server.ts index 2421398..386bdb8 100644 --- a/src/routes/api/fitness/measurements/latest/+server.ts +++ b/src/routes/api/fitness/measurements/latest/+server.ts @@ -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, diff --git a/src/routes/api/fitness/sessions/+server.ts b/src/routes/api/fitness/sessions/+server.ts index 20ac4ed..296edb5 100644 --- a/src/routes/api/fitness/sessions/+server.ts +++ b/src/routes/api/fitness/sessions/+server.ts @@ -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); diff --git a/src/routes/api/fitness/stats/overview/+server.ts b/src/routes/api/fitness/stats/overview/+server.ts index e268a28..e58c9c0 100644 --- a/src/routes/api/fitness/stats/overview/+server.ts +++ b/src/routes/api/fitness/stats/overview/+server.ts @@ -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); diff --git a/src/routes/api/tasks/stats/+server.ts b/src/routes/api/tasks/stats/+server.ts index 500da47..ebd7c2a 100644 --- a/src/routes/api/tasks/stats/+server.ts +++ b/src/routes/api/tasks/stats/+server.ts @@ -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 }); }; diff --git a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte index 432017c..1bf2d3d 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/+page.svelte @@ -99,7 +99,6 @@ const parts = []; if (m.weight != null) parts.push(`${m.weight} kg`); if (m.bodyFatPercent != null) parts.push(`${m.bodyFatPercent}% bf`); - if (m.caloricIntake != null) parts.push(`${m.caloricIntake} kcal`); return parts.join(' · ') || t('body_measurements_only', lang); } @@ -151,10 +150,6 @@ {t('body_fat', lang)} {latest.bodyFatPercent?.value ?? '—'}% -
- {t('calories', lang)} - {latest.caloricIntake?.value ?? '—'} kcal -
@@ -303,7 +298,7 @@ /* Latest */ .stat-grid { display: grid; - grid-template-columns: repeat(3, 1fr); + grid-template-columns: repeat(2, 1fr); gap: 0.6rem; } .stat-card { diff --git a/src/routes/fitness/[measure=fitnessMeasure]/add/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/add/+page.svelte index 4864437..0987ca5 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/add/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/add/+page.svelte @@ -13,7 +13,6 @@ let formDate = $state(new Date().toISOString().slice(0, 10)); let formWeight = $state(''); let formBodyFat = $state(''); - let formCalories = $state(''); let formNeck = $state(''); let formShoulders = $state(''); let formChest = $state(''); @@ -35,9 +34,6 @@ else body.weight = null; if (formBodyFat) body.bodyFatPercent = Number(formBodyFat); else body.bodyFatPercent = null; - if (formCalories) body.caloricIntake = Number(formCalories); - else body.caloricIntake = null; - /** @type {any} */ const m = {}; if (formNeck) m.neck = Number(formNeck); @@ -102,10 +98,6 @@ -
- - -

{t('body_parts_cm', lang)}

diff --git a/src/routes/fitness/[measure=fitnessMeasure]/edit/[id]/+page.svelte b/src/routes/fitness/[measure=fitnessMeasure]/edit/[id]/+page.svelte index 207ae20..f3a1496 100644 --- a/src/routes/fitness/[measure=fitnessMeasure]/edit/[id]/+page.svelte +++ b/src/routes/fitness/[measure=fitnessMeasure]/edit/[id]/+page.svelte @@ -19,8 +19,6 @@ let formDate = $state(m ? new Date(m.date).toISOString().slice(0, 10) : ''); let formWeight = $state(m?.weight != null ? String(m.weight) : ''); let formBodyFat = $state(m?.bodyFatPercent != null ? String(m.bodyFatPercent) : ''); - let formCalories = $state(m?.caloricIntake != null ? String(m.caloricIntake) : ''); - const bp = m?.measurements ?? {}; let formNeck = $state(bp.neck != null ? String(bp.neck) : ''); let formShoulders = $state(bp.shoulders != null ? String(bp.shoulders) : ''); @@ -43,9 +41,6 @@ else body.weight = null; if (formBodyFat) body.bodyFatPercent = Number(formBodyFat); else body.bodyFatPercent = null; - if (formCalories) body.caloricIntake = Number(formCalories); - else body.caloricIntake = null; - /** @type {any} */ const ms = {}; if (formNeck) ms.neck = Number(formNeck); @@ -128,10 +123,6 @@ -
- - -

{t('body_parts_cm', lang)}

diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.server.ts b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.server.ts index 1c9998f..7a1882d 100644 --- a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.server.ts +++ b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.server.ts @@ -8,29 +8,33 @@ import mongoose from 'mongoose'; export const load: PageServerLoad = async ({ fetch, url, locals }) => { const dateParam = url.searchParams.get('date') || new Date().toISOString().slice(0, 10); - const [foodRes, goalRes, weightRes] = await Promise.all([ + // Run all independent work in parallel: 3 API calls + workout kcal DB query + const dayStart = new Date(dateParam + 'T00:00:00.000Z'); + const dayEnd = new Date(dateParam + 'T23:59:59.999Z'); + + const exercisePromise = (async () => { + try { + const user = await requireAuth(locals); + await dbConnect(); + const sessions = await WorkoutSession.find({ + createdBy: user.nickname, + startTime: { $gte: dayStart, $lte: dayEnd } + }).select('kcalEstimate').lean(); + let kcal = 0; + for (const s of sessions) { + if (s.kcalEstimate?.kcal) kcal += s.kcalEstimate.kcal; + } + return kcal; + } catch { return 0; } + })(); + + const [foodRes, goalRes, weightRes, exerciseKcal] = await Promise.all([ fetch(`/api/fitness/food-log?date=${dateParam}`), fetch('/api/fitness/goal'), - fetch('/api/fitness/measurements/latest') + fetch('/api/fitness/measurements/latest'), + exercisePromise ]); - // Fetch today's workout kcal burned - let exerciseKcal = 0; - try { - const user = await requireAuth(locals); - await dbConnect(); - const dayStart = new Date(dateParam + 'T00:00:00.000Z'); - const dayEnd = new Date(dateParam + 'T23:59:59.999Z'); - const sessions = await WorkoutSession.find({ - createdBy: user.nickname, - startTime: { $gte: dayStart, $lte: dayEnd } - }).select('kcalEstimate').lean(); - - for (const s of sessions) { - if (s.kcalEstimate?.kcal) exerciseKcal += s.kcalEstimate.kcal; - } - } catch {} - const foodLog = foodRes.ok ? await foodRes.json() : { entries: [] }; // Resolve recipe images for entries with source=recipe diff --git a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte index 68322dc..6dd8dba 100644 --- a/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte +++ b/src/routes/fitness/[nutrition=fitnessNutrition]/+page.svelte @@ -731,7 +731,7 @@ {@const mealCal = mealEntries.reduce((s, e) => s + entryCalories(e), 0)} {@const meta = mealMeta[meal]} {@const MealSectionIcon = meta.icon} -
+
@@ -862,11 +862,6 @@ gap: 0.75rem; } - /* ── Entrance animations ── */ - @keyframes fade-up { - from { opacity: 0; transform: translateY(8px); } - to { opacity: 1; transform: translateY(0); } - } /* ── Date Navigator ── */ .date-nav { @@ -874,7 +869,7 @@ align-items: center; justify-content: center; gap: 0.25rem; - animation: fade-up 0.3s ease both; + } .date-btn { background: none; @@ -927,8 +922,8 @@ padding: 1.25rem; box-shadow: var(--shadow-sm); position: relative; - animation: fade-up 0.35s ease both; - animation-delay: 50ms; + + } .daily-summary::before { content: ''; @@ -1148,7 +1143,7 @@ display: grid; grid-template-columns: 1fr; gap: 1rem; - animation: fade-up 0.25s ease both; + } .micro-section h4 { margin: 0 0 0.4rem; @@ -1205,7 +1200,7 @@ background: var(--color-surface); border-radius: 12px; box-shadow: var(--shadow-sm); - animation: fade-up 0.35s ease both; + } .no-goal-icon { display: flex; @@ -1247,7 +1242,7 @@ border-radius: 12px; padding: 1.25rem; box-shadow: var(--shadow-sm); - animation: fade-up 0.25s ease both; + } .preset-section { margin-bottom: 1rem; @@ -1370,7 +1365,7 @@ /* ── Meal Sections ── */ .meal-section { - animation: fade-up 0.35s ease both; + } .meal-header { display: flex; @@ -1568,7 +1563,7 @@ border-radius: 10px; padding: 0.85rem; box-shadow: var(--shadow-sm); - animation: fade-up 0.2s ease both; + } /* Search/food selection handled by FoodSearch component */