diff --git a/web/app/(app)/progression/page.tsx b/web/app/(app)/progression/page.tsx index b429729..be877fa 100644 --- a/web/app/(app)/progression/page.tsx +++ b/web/app/(app)/progression/page.tsx @@ -1,5 +1,5 @@ import type { Metadata } from "next" -import { startOfWeek, subMonths, subWeeks, format } from "date-fns" +import { startOfWeek, subYears, subWeeks, format } from "date-fns" import { fr } from "date-fns/locale" import { LineChart } from "lucide-react" @@ -36,14 +36,14 @@ export default async function ProgressionPage() { if (!user) return null const now = new Date() - const sixMonthsAgo = subMonths(now, 6) + const vmaHistoryStart = subYears(now, 4) const [activitiesRes, zonesRes, prActivitiesRes] = await Promise.all([ supabase .from("activities") - .select("provider, provider_activity_id, sport_type, start_date, duration_sec, moving_time_sec, distance_m, elevation_gain_m, average_heartrate, max_heartrate, time_in_zones_json") + .select("provider, provider_activity_id, sport_type, start_date, duration_sec, moving_time_sec, distance_m, elevation_gain_m, average_heartrate, max_heartrate, max_speed, time_in_zones_json") .eq("user_id", user.id) - .gte("start_date", sixMonthsAgo.toISOString()) + .gte("start_date", vmaHistoryStart.toISOString()) .order("start_date"), supabase .from("hr_zones") diff --git a/web/lib/compute/vma-estimate.ts b/web/lib/compute/vma-estimate.ts index 965a004..6ea6fcc 100644 --- a/web/lib/compute/vma-estimate.ts +++ b/web/lib/compute/vma-estimate.ts @@ -7,6 +7,7 @@ export type VmaActivity = { elevation_gain_m: number | null average_heartrate: number | null max_heartrate: number | null + max_speed?: number | null } export type VmaZone = { diff --git a/web/lib/server/strava/vma.ts b/web/lib/server/strava/vma.ts index a4f1890..7d82703 100644 --- a/web/lib/server/strava/vma.ts +++ b/web/lib/server/strava/vma.ts @@ -30,11 +30,17 @@ function candidateActivities(activities: VmaStravaActivity[]): VmaStravaActivity return distanceKm >= 2 && durationSec >= 600 && elevationPerKm <= 35 }) .sort((a, b) => { - const speedA = (a.distance_m ?? 0) / Math.max(1, a.moving_time_sec ?? a.duration_sec ?? 1) - const speedB = (b.distance_m ?? 0) / Math.max(1, b.moving_time_sec ?? b.duration_sec ?? 1) - return speedB - speedA + const score = (activity: VmaStravaActivity) => { + const avgSpeed = ((activity.distance_m ?? 0) / Math.max(1, activity.moving_time_sec ?? activity.duration_sec ?? 1)) * 3.6 + const maxSpeed = (activity.max_speed ?? 0) * 3.6 + const hrScore = Math.max(activity.max_heartrate ?? 0, activity.average_heartrate ?? 0) / 10 + const daysAgo = Math.max(0, (Date.now() - new Date(activity.start_date).getTime()) / 86_400_000) + const recency = Math.max(0, 4 - daysAgo / 180) + return Math.max(avgSpeed, maxSpeed * 0.82) + hrScore + recency + } + return score(b) - score(a) }) - .slice(0, 6) + .slice(0, 12) } async function fetchStreams(token: string, providerActivityId: string): Promise { @@ -58,23 +64,20 @@ export async function getVmaStreamEfforts( token: string, activities: VmaStravaActivity[], ): Promise { - const efforts: VmaStreamEffort[] = [] - for (const activity of candidateActivities(activities)) { + const results = await Promise.all(candidateActivities(activities).map(async (activity) => { const payload = await fetchStreams(token, activity.provider_activity_id as string) const time = payload?.time?.data const distance = payload?.distance?.data - if (!time || !distance || time.length !== distance.length) continue + if (!time || !distance || time.length !== distance.length) return [] - efforts.push( - ...bestStreamEfforts({ - time, - distance, - velocity: payload?.velocity_smooth?.data, - heartrate: payload?.heartrate?.data, - altitude: payload?.altitude?.data, - date: activity.start_date, - }), - ) - } - return efforts + return bestStreamEfforts({ + time, + distance, + velocity: payload?.velocity_smooth?.data, + heartrate: payload?.heartrate?.data, + altitude: payload?.altitude?.data, + date: activity.start_date, + }) + })) + return results.flat() }