diff --git a/supabase/migrations/20260525122151_app_feedback.sql b/supabase/migrations/20260525122151_app_feedback.sql new file mode 100644 index 0000000..e1d1199 --- /dev/null +++ b/supabase/migrations/20260525122151_app_feedback.sql @@ -0,0 +1,18 @@ +-- app_feedback: user feedback submissions (bugs, features, other) with RLS +create table if not exists public.app_feedback ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references auth.users on delete cascade, + feedback_type text not null check (feedback_type in ('bug', 'feature', 'other')), + message text not null, + created_at timestamptz default now() not null +); + +alter table public.app_feedback enable row level security; + +drop policy if exists "users_insert_own_feedback" on public.app_feedback; +create policy "users_insert_own_feedback" on public.app_feedback + for insert with check (auth.uid() = user_id); + +drop policy if exists "users_select_own_feedback" on public.app_feedback; +create policy "users_select_own_feedback" on public.app_feedback + for select using (auth.uid() = user_id); diff --git a/supabase/migrations/20260525125310_polar_config.sql b/supabase/migrations/20260525125310_polar_config.sql new file mode 100644 index 0000000..c7242bf --- /dev/null +++ b/supabase/migrations/20260525125310_polar_config.sql @@ -0,0 +1,19 @@ +-- Support for Polar Flow connection. +-- Update check constraint on provider_connections to support 'polar'. +alter table public.provider_connections + drop constraint if exists provider_connections_provider_check; + +alter table public.provider_connections + add constraint provider_connections_provider_check + check (provider in ('strava', 'terra', 'garmin', 'polar')); + +-- Single-row configuration table for Polar App credentials. +create table if not exists public.polar_config ( + id int primary key default 1 check (id = 1), + client_id text not null default '', + client_secret text not null default '', + updated_at timestamptz default now() not null +); + +-- Seed default empty row for Polar configuration. +insert into public.polar_config (id) values (1) on conflict (id) do nothing; diff --git a/web/app/(app)/connections/actions.ts b/web/app/(app)/connections/actions.ts index df1ac67..1ae4bac 100644 --- a/web/app/(app)/connections/actions.ts +++ b/web/app/(app)/connections/actions.ts @@ -119,3 +119,42 @@ export async function disconnectStrava(): Promise<{ success?: boolean; error?: s revalidatePath("/connections") return { success: true } } + +export async function disconnectPolar(): Promise<{ success?: boolean; error?: string }> { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) return { error: "Non authentifié" } + + const { error } = await supabase + .from("provider_connections") + .update({ is_active: false }) + .eq("user_id", user.id) + .eq("provider", "polar") + + if (error) return { error: error.message } + + revalidatePath("/connections") + return { success: true } +} + +export async function syncPolarHistory(days = 30): Promise<{ synced?: number; error?: string }> { + const supabase = await createClient() + const { + data: { user }, + } = await supabase.auth.getUser() + + if (!user) return { error: "Non authentifié" } + + try { + const { syncPolarMetrics } = await import("@/lib/server/polar/sync") + const synced = await syncPolarMetrics(user.id, days) + revalidatePath("/connections") + revalidatePath("/dashboard") + return { synced } + } catch (e) { + return { error: e instanceof Error ? e.message : "Synchronisation Polar échouée" } + } +} diff --git a/web/app/(app)/connections/connections-client.tsx b/web/app/(app)/connections/connections-client.tsx index bfc9241..51b6145 100644 --- a/web/app/(app)/connections/connections-client.tsx +++ b/web/app/(app)/connections/connections-client.tsx @@ -11,7 +11,9 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { disconnectStrava, disconnectTerra, + disconnectPolar, syncGarminHistory, + syncPolarHistory, syncAllStravaHistory, syncStrava, syncStravaHistory, @@ -189,7 +191,7 @@ export function TerraCard({ connected, providerUserId, lastSyncAt }: TerraCardPr ))} Connecter mon appareil @@ -372,7 +374,7 @@ export function StravaCard({ Connectez votre compte Strava pour importer automatiquement vos activités.
Connecter Strava @@ -383,3 +385,110 @@ export function StravaCard({ ) } + +export function PolarCard({ connected, providerUserId, lastSyncAt }: TerraCardProps) { + const router = useRouter() + const [syncing, setSyncing] = useState(false) + const [disconnecting, setDisconnecting] = useState(false) + + const lastSyncLabel = lastSyncAt + ? new Intl.DateTimeFormat("fr-FR", { + dateStyle: "short", + timeStyle: "short", + }).format(new Date(lastSyncAt)) + : "Jamais" + + async function handleDisconnect() { + setDisconnecting(true) + const result = await disconnectPolar() + setDisconnecting(false) + if (result.error) { + toast.error(result.error) + } else { + toast.success("Polar déconnecté") + router.refresh() + } + } + + return ( +ID Utilisateur Polar
+{providerUserId ?? "connecté"}
+Dernière actualisation
+{lastSyncLabel}
+Données synchronisées
+FC repos · HRV nocturne · Score sommeil · Durée de sommeil
++ Connectez votre compte Polar Flow pour importer les données de récupération et de sommeil de votre montre Polar. +
+ + Connecter Polar + +{dateStr}
++ + {type === "hr" ? "FC repos : " : "VFC (HRV) : "} + {value} + + {type === "hr" ? " bpm" : " ms"} + +
+{content}
+{formatZone(injury.body_zone)}
++ {startFmt} + {endFmt ? ` → ${endFmt}` : " → en cours"} +
+{injury.description}
+ )} + {injury.treatment && ( ++ Traitement : {injury.treatment} +
+ )} ++ Durée : {formatSleepDuration(sleepDuration)} +
++ Reflète la récupération globale +
++ Stress moyen : {stressScore !== null ? `${Math.round(stressScore)}/100` : "–"} +
++ {spo2 !== null ? `${spo2.toFixed(1)}%` : "–"} +
++ {respiration !== null ? `${respiration.toFixed(1)} cpm` : "–"} +
++ {vo2max !== null ? `${vo2max.toFixed(0)}` : "–"} +
+Aucune blessure active signalée.
+ ) : ( +{formatZone(injury.body_zone)}
-- {startFmt} - {endFmt ? ` → ${endFmt}` : " → en cours"} -
-{injury.description}
- )} - {injury.treatment && ( -- Traitement : {injury.treatment} -
- )} -ACWR
-{acwr.acwr.toFixed(2)}
-Charge 7j
-{acwr.acute_load_7d}
-Charge 28j
-{acwr.chronic_load_28d}
-Aucune blessure active.
- ) : ( - active.map((i) =>- Configurez les intégrations utilisées par SportTrack. + Configurez vos intégrations et options SportTrack.
- Lance le flux OAuth Strava pour connecter votre compte utilisateur. Cette action ne
- recrée ni webhook ni identifiants d'application.
+
+ Lance le flux OAuth Strava pour connecter votre compte utilisateur. Cette action ne
+ recrée ni webhook ni identifiants d'application.
+
+ Configurez l'intégration d'API Terra pour Polar / Fitbit et autres appareils.
Connexion Strava (Développeur)
+ Clés API Terra
+
+ Configurez l'intégration Polar Flow en direct par OAuth. +
+- Garmin Connect est utilisé en priorité via une intégration non officielle. Terra reste - disponible si un accès API est configuré plus tard. + Saisissez vos identifiants Garmin Connect pour synchroniser vos métriques (sommeil, récupération, FC repos).