Files

154 lines
5.2 KiB
TypeScript
Raw Permalink Normal View History

2026-06-16 09:43:48 +02:00
"use server";
import { revalidatePath } from "next/cache";
import type { GarminConnect } from "garmin-connect";
import {
GarminLoginRequiredError,
2026-06-18 11:02:31 +02:00
GarminCredentialsMissingError,
2026-06-16 09:43:48 +02:00
beginGarminLogin,
completeGarminMfaLogin,
fetchActivityRoutePoints,
fetchRunningActivities,
getAuthorizedClient,
} from "@/lib/garmin/client";
import {
getLastSyncAt,
getRunningActivity,
2026-06-18 11:24:56 +02:00
listRunningActivities,
2026-06-16 09:43:48 +02:00
setLastSyncAt,
2026-06-18 11:24:56 +02:00
setRunningActivityMetrics,
2026-06-16 09:43:48 +02:00
setRunningActivityRoutePoints,
upsertRunningActivity,
} from "@/lib/models/running";
2026-06-18 11:24:56 +02:00
import { fetchActivityRunMetrics } from "@/lib/garmin/client";
2026-06-16 09:43:48 +02:00
import {
clearPendingMfaState,
getPendingMfaState,
saveOauth1Token,
savePendingMfaState,
} from "@/lib/models/garmin-auth";
2026-06-18 11:02:31 +02:00
import { getCurrentUserId } from "@/lib/session";
2026-06-16 09:43:48 +02:00
export type SyncGarminState = { error: string } | { success: string } | { mfaRequired: true } | null;
2026-06-18 11:24:56 +02:00
// How many activities without data to enrich per sync (limits API call volume)
const ENRICH_PER_SYNC = 10;
2026-06-18 11:02:31 +02:00
async function syncWithClient(userId: string, client: GarminConnect): Promise<SyncGarminState> {
const since = await getLastSyncAt(userId);
2026-06-16 09:43:48 +02:00
const activities = await fetchRunningActivities(client);
const newCount = activities.filter((activity) => !since || activity.startTime > since).length;
for (const activity of activities) {
2026-06-18 11:02:31 +02:00
await upsertRunningActivity(userId, activity);
2026-06-16 09:43:48 +02:00
}
2026-06-18 11:02:31 +02:00
await setLastSyncAt(userId, new Date());
2026-06-16 09:43:48 +02:00
2026-06-18 11:24:56 +02:00
// 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
}
}
2026-06-16 09:43:48 +02:00
revalidatePath("/running");
revalidatePath("/settings");
revalidatePath("/");
return { success: `Zsynchronizowano ${newCount} nowych aktywności (zaktualizowano ${activities.length}).` };
}
export async function syncGarminActivities(): Promise<SyncGarminState> {
2026-06-18 11:02:31 +02:00
const userId = await getCurrentUserId();
2026-06-16 09:43:48 +02:00
try {
2026-06-18 11:02:31 +02:00
const client = await getAuthorizedClient(userId);
return await syncWithClient(userId, client);
2026-06-16 09:43:48 +02:00
} catch (error) {
2026-06-18 11:02:31 +02:00
if (error instanceof GarminCredentialsMissingError) {
return { error: error.message };
}
2026-06-16 09:43:48 +02:00
if (!(error instanceof GarminLoginRequiredError)) {
return { error: error instanceof Error ? error.message : "Synchronizacja z Garmin nie powiodła się." };
}
}
try {
2026-06-18 11:02:31 +02:00
const result = await beginGarminLogin(userId);
2026-06-16 09:43:48 +02:00
if ("mfaRequired" in result) {
2026-06-18 11:02:31 +02:00
await savePendingMfaState(userId, result.pendingState);
2026-06-16 09:43:48 +02:00
return { mfaRequired: true };
}
2026-06-18 11:02:31 +02:00
await saveOauth1Token(userId, result.oauth1Token);
return await syncWithClient(userId, result.client);
2026-06-16 09:43:48 +02:00
} catch (error) {
return { error: error instanceof Error ? error.message : "Logowanie do Garmin nie powiodło się." };
}
}
export async function submitGarminMfaCode(code: string): Promise<SyncGarminState> {
2026-06-18 11:02:31 +02:00
const userId = await getCurrentUserId();
const pending = await getPendingMfaState(userId);
2026-06-16 09:43:48 +02:00
if (!pending) {
return { error: "Sesja logowania do Garmin wygasła. Kliknij \"Synchronizuj z Garmin\" ponownie." };
}
try {
const result = await completeGarminMfaLogin(pending, code);
2026-06-18 11:02:31 +02:00
await saveOauth1Token(userId, result.oauth1Token);
await clearPendingMfaState(userId);
return await syncWithClient(userId, result.client);
2026-06-16 09:43:48 +02:00
} 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<LoadRouteState> {
2026-06-18 11:02:31 +02:00
const userId = await getCurrentUserId();
const activity = await getRunningActivity(userId, activityMongoId);
2026-06-16 09:43:48 +02:00
if (!activity) return { error: "Nie znaleziono aktywności." };
let client: GarminConnect;
try {
2026-06-18 11:02:31 +02:00
client = await getAuthorizedClient(userId);
2026-06-16 09:43:48 +02:00
} catch {
return { error: "Brak połączenia z Garmin Connect. Wykonaj synchronizację." };
}
try {
2026-06-18 09:43:25 +02:00
const result = await fetchActivityRoutePoints(client, activity.garminActivityId);
if (!result) return { error: "Brak danych GPS dla tej aktywności." };
2026-06-18 11:02:31 +02:00
await setRunningActivityRoutePoints(userId, activity.garminActivityId, result.points, result.elevationProfile);
2026-06-16 09:43:48 +02:00
revalidatePath(`/running/${activityMongoId}`);
return { success: true };
} catch (error) {
return { error: error instanceof Error ? error.message : "Nie udało się pobrać mapy trasy." };
}
}