diff --git a/CLAUDE.md b/CLAUDE.md index 409646d..e9f5b4a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,6 +190,7 @@ week_plan_days - **Claude API proxy:** `app/api/claude.js` verifies incoming Supabase JWTs via `GET /auth/v1/user`. Requires `ANTHROPIC_API_KEY`, `SUPABASE_URL`, and `SUPABASE_ANON_KEY` as Azure app settings. Use `SUPABASE_ANON_KEY` (no `VITE_` prefix) — the `VITE_` prefix is Vite build-time only and is invisible to the Azure Functions runtime. - **CI/CD build split:** the frontend is pre-built in the GitHub Actions runner (`npm ci && npm run build` with `VITE_*` in `env:`), then the Azure SWA action uploads `app/dist/` directly (`app_location: "app/dist"`). This bypasses Oryx for the frontend — Oryx strips `VITE_*` env vars before spawning Vite and they never reach the bundle. Oryx still handles the API (`app/api`). `vite.config.js` has a build-time assertion that fails immediately if the required vars are missing. - **Supabase client explicit apikey header:** `createClient` is called with `global: { headers: { apikey: supabaseKey } }` in `app/src/lib/supabase.js`. The Supabase JS v2 fetch interceptor should add this automatically, but it was not reaching browser requests — passing it in `global.headers` puts it directly on `PostgrestClient`'s base headers, bypassing the interceptor. Do not remove this option. +- **Multi-instruktør gym membership:** `user_gyms` table links each user to a Sporty business unit (`sporty_business_unit_id`). Primary users are instruktører; sharing default is opt-out scoped to the same gym. `ensureGymMembership(buId)` in `db.js` does an idempotent upsert on sign-in (called in `App.jsx`). The `role` column is a text placeholder (`'instruktor'`) — it will be replaced by a FK to a dedicated temporal `roles` table in a later issue. `DEFAULT_SPORTY_BUSINESS_UNIT_ID = 8` mirrors the hardcoded BU in `sportySync.js`; both must move to a DB config when multi-gym support lands. Backfilled rows exist for both current users. ## Known limitations - SVG body is improved but still geometrically simplified — not anatomically precise; key muscles (traps, lats) use path shapes, rest are ellipses diff --git a/app/api/sportySync.js b/app/api/sportySync.js index 5922e24..d317892 100644 --- a/app/api/sportySync.js +++ b/app/api/sportySync.js @@ -1,5 +1,7 @@ import { app } from '@azure/functions'; +// BU 8 is hardcoded for the single-gym MVP (Sporty Thon Senter Ski). +// When multi-gym support lands, replace with a DB lookup via user_gyms.sporty_business_unit_id. const SPORTY_BASE_URL = 'https://sporty.no/api/v1/businessunits/8/groupactivities'; diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json index 69310dc..17a68aa 100644 --- a/app/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -274,7 +274,10 @@ "language": "Language", "languageNorwegian": "Norsk", "languageEnglish": "English", - "languagePersian": "فارسی" + "languagePersian": "فارسی", + "myGym": "My gym", + "myGymMembership": "Sporty Thon Senter Ski", + "myGymFutureHint": "Coming soon: choose your gym and see shared session history with other instructors." }, "report": { "heroMuscles_one": "{{count}} muscle", diff --git a/app/public/locales/fa/translation.json b/app/public/locales/fa/translation.json index 99ce9d0..5e3fea5 100644 --- a/app/public/locales/fa/translation.json +++ b/app/public/locales/fa/translation.json @@ -274,7 +274,10 @@ "language": "زبان", "languageNorwegian": "Norsk", "languageEnglish": "English", - "languagePersian": "فارسی" + "languagePersian": "فارسی", + "myGym": "باشگاه من", + "myGymMembership": "Sporty Thon Senter Ski", + "myGymFutureHint": "به زودی: انتخاب باشگاه و مشاهده تاریخچه مشترک با سایر مربیان." }, "report": { "heroMuscles_one": "{{count}} عضله", diff --git a/app/public/locales/nb/translation.json b/app/public/locales/nb/translation.json index 29e247d..1d9282c 100644 --- a/app/public/locales/nb/translation.json +++ b/app/public/locales/nb/translation.json @@ -274,7 +274,10 @@ "language": "Språk", "languageNorwegian": "Norsk", "languageEnglish": "English", - "languagePersian": "فارسی" + "languagePersian": "فارسی", + "myGym": "Min gym", + "myGymMembership": "Sporty Thon Senter Ski", + "myGymFutureHint": "Fremtidig: velg gym og se felles økthistorikk med andre instruktører." }, "report": { "heroMuscles_one": "{{count}} muskel", diff --git a/app/src/App.jsx b/app/src/App.jsx index d1af07b..04977d6 100644 --- a/app/src/App.jsx +++ b/app/src/App.jsx @@ -1,5 +1,6 @@ import { useEffect, useState } from "react"; import { supabase } from "./lib/supabase"; +import { ensureGymMembership } from "./lib/db"; import { NavContext } from "./lib/NavContext"; import Login from "./components/Login"; import Home from "./components/Home"; @@ -22,9 +23,13 @@ function App() { const [reportPrefill, setReportPrefill] = useState(null); useEffect(() => { - supabase.auth.getSession().then(({ data: { session } }) => setSession(session)); + supabase.auth.getSession().then(({ data: { session } }) => { + setSession(session); + if (session) ensureGymMembership().catch(() => {}); + }); const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); + if (session) ensureGymMembership().catch(() => {}); }); return () => subscription.unsubscribe(); }, []); diff --git a/app/src/components/Settings.jsx b/app/src/components/Settings.jsx index fddaee4..3ba4be5 100644 --- a/app/src/components/Settings.jsx +++ b/app/src/components/Settings.jsx @@ -1,5 +1,5 @@ import { useEffect, useState } from "react"; -import { Toggle, Button, RadioButtonGroup, RadioButton } from "@carbon/react"; +import { Toggle, Button, RadioButtonGroup, RadioButton, Tag } from "@carbon/react"; import { useTranslation } from "react-i18next"; import PageShell, { SectionLabel, PageHeading } from "./PageShell"; import BodyPanel from "./BodyPanel"; @@ -79,6 +79,23 @@ export default function Settings() { /> + {t("settings.myGym")} +
+
+ + {t("settings.myGymMembership")} + +

+ {t("settings.myGymFutureHint")} +

+
+
+ {t("settings.contact")}
diff --git a/app/src/lib/db.js b/app/src/lib/db.js index d103a97..2cf4bf6 100644 --- a/app/src/lib/db.js +++ b/app/src/lib/db.js @@ -369,3 +369,31 @@ export async function updateSession(sessionId, exercises, gymCalendarId, { repla }); if (error) throw error; } + +// ── USER GYMS ───────────────────────────────────────────────────────── + +export const DEFAULT_SPORTY_BUSINESS_UNIT_ID = 8; + +export async function fetchMyGyms() { + const { data, error } = await supabase + .from("user_gyms") + .select("id, sporty_business_unit_id, role, created_at") + .order("created_at", { ascending: true }); + if (error) throw error; + return data || []; +} + +export async function ensureGymMembership(buId = DEFAULT_SPORTY_BUSINESS_UNIT_ID) { + const { data: { user } } = await supabase.auth.getUser(); + if (!user) return null; + const { data, error } = await supabase + .from("user_gyms") + .upsert( + { user_id: user.id, sporty_business_unit_id: buId }, + { onConflict: "user_id,sporty_business_unit_id", ignoreDuplicates: true } + ) + .select() + .maybeSingle(); + if (error) throw error; + return data; +}