Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 14 additions & 1 deletion app/src/lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading