"use server"; import { revalidatePath } from "next/cache"; import type { GarminConnect } from "garmin-connect"; import { GarminLoginRequiredError, GarminCredentialsMissingError, beginGarminLogin, completeGarminMfaLogin, fetchActivityRoutePoints, fetchRunningActivities, getAuthorizedClient, } from "@/lib/garmin/client"; import { getLastSyncAt, getRunningActivity, listRunningActivities, setLastSyncAt, setRunningActivityMetrics, setRunningActivityRoutePoints, upsertRunningActivity, } from "@/lib/models/running"; import { fetchActivityRunMetrics } from "@/lib/garmin/client"; import { clearPendingMfaState, getPendingMfaState, saveOauth1Token, savePendingMfaState, } from "@/lib/models/garmin-auth"; import { getCurrentUserId } from "@/lib/session"; export type SyncGarminState = { error: string } | { success: string } | { mfaRequired: true } | null; // How many activities without data to enrich per sync (limits API call volume) const ENRICH_PER_SYNC = 10; async function syncWithClient(userId: string, client: GarminConnect): Promise { const since = await getLastSyncAt(userId); const activities = await fetchRunningActivities(client); const newCount = activities.filter((activity) => !since || activity.startTime > since).length; for (const activity of activities) { await upsertRunningActivity(userId, activity); } await setLastSyncAt(userId, new Date()); // Enrich activities missing route/metrics — fetched during sync so page loads are instant const all = await listRunningActivities(userId); const needsEnrich = all .filter((a) => a.hasRoute && (!a.routePoints?.length || !a.runMetrics?.paceSec)) .slice(0, ENRICH_PER_SYNC); for (const activity of needsEnrich) { try { if (!activity.routePoints?.length) { const route = await fetchActivityRoutePoints(client, activity.garminActivityId); if (route) { await setRunningActivityRoutePoints( userId, activity.garminActivityId, route.points, route.elevationProfile ); } } if (!activity.runMetrics?.paceSec) { const metrics = await fetchActivityRunMetrics(client, activity.garminActivityId); if (metrics) { await setRunningActivityMetrics(userId, activity.garminActivityId, metrics); } } } catch { // Rate limited or activity has no GPS — skip silently } } revalidatePath("/running"); revalidatePath("/settings"); revalidatePath("/"); return { success: `Zsynchronizowano ${newCount} nowych aktywności (zaktualizowano ${activities.length}).` }; } export async function syncGarminActivities(): Promise { const userId = await getCurrentUserId(); try { const client = await getAuthorizedClient(userId); return await syncWithClient(userId, client); } catch (error) { if (error instanceof GarminCredentialsMissingError) { return { error: error.message }; } if (!(error instanceof GarminLoginRequiredError)) { return { error: error instanceof Error ? error.message : "Synchronizacja z Garmin nie powiodła się." }; } } try { const result = await beginGarminLogin(userId); if ("mfaRequired" in result) { await savePendingMfaState(userId, result.pendingState); return { mfaRequired: true }; } await saveOauth1Token(userId, result.oauth1Token); return await syncWithClient(userId, result.client); } catch (error) { return { error: error instanceof Error ? error.message : "Logowanie do Garmin nie powiodło się." }; } } export async function submitGarminMfaCode(code: string): Promise { const userId = await getCurrentUserId(); const pending = await getPendingMfaState(userId); if (!pending) { return { error: "Sesja logowania do Garmin wygasła. Kliknij \"Synchronizuj z Garmin\" ponownie." }; } try { const result = await completeGarminMfaLogin(pending, code); await saveOauth1Token(userId, result.oauth1Token); await clearPendingMfaState(userId); return await syncWithClient(userId, result.client); } catch (error) { return { error: error instanceof Error ? error.message : "Weryfikacja kodu MFA nie powiodła się." }; } } export type LoadRouteState = { error: string } | { success: true } | null; export async function loadActivityRoute(activityMongoId: string): Promise { const userId = await getCurrentUserId(); const activity = await getRunningActivity(userId, activityMongoId); if (!activity) return { error: "Nie znaleziono aktywności." }; let client: GarminConnect; try { client = await getAuthorizedClient(userId); } catch { return { error: "Brak połączenia z Garmin Connect. Wykonaj synchronizację." }; } try { const result = await fetchActivityRoutePoints(client, activity.garminActivityId); if (!result) return { error: "Brak danych GPS dla tej aktywności." }; await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile); revalidatePath(`/running/${activityMongoId}`); return { success: true }; } catch (error) { return { error: error instanceof Error ? error.message : "Nie udało się pobrać mapy trasy." }; } }