diff --git a/CLAUDE.md b/CLAUDE.md index 5c17524..7f523d3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -197,7 +197,8 @@ 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. +- **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`). `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. +- **Roles (temporal):** `roles` table stores instruktør tenure — `user_id`, `sporty_business_unit_id`, `name` (default `'instruktor'`), `title`, `valid_from` (date), `valid_to` (nullable date). Active roles = `valid_from <= today AND (valid_to IS NULL OR valid_to >= today)`. `fetchActiveRoles(buId)` in `db.js` returns all active roles for the current user at the given gym. Existing placeholder rows were migrated from `user_gyms.role` (issue #140). RLS: users can only read/write their own rows. ## 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/src/lib/db.js b/app/src/lib/db.js index 1dd8f15..1502143 100644 --- a/app/src/lib/db.js +++ b/app/src/lib/db.js @@ -383,12 +383,25 @@ 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") + .select("id, sporty_business_unit_id, created_at") .order("created_at", { ascending: true }); if (error) throw error; return data || []; } +export async function fetchActiveRoles(buId = DEFAULT_SPORTY_BUSINESS_UNIT_ID) { + const today = new Date().toISOString().slice(0, 10); + const { data, error } = await supabase + .from("roles") + .select("id, name, title, valid_from, valid_to, sporty_business_unit_id") + .eq("sporty_business_unit_id", buId) + .lte("valid_from", today) + .or(`valid_to.is.null,valid_to.gte.${today}`) + .order("valid_from", { 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;