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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions app/api/sportySync.js
Original file line number Diff line number Diff line change
@@ -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';

Expand Down
5 changes: 4 additions & 1 deletion app/public/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion app/public/locales/fa/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,10 @@
"language": "زبان",
"languageNorwegian": "Norsk",
"languageEnglish": "English",
"languagePersian": "فارسی"
"languagePersian": "فارسی",
"myGym": "باشگاه من",
"myGymMembership": "Sporty Thon Senter Ski",
"myGymFutureHint": "به زودی: انتخاب باشگاه و مشاهده تاریخچه مشترک با سایر مربیان."
},
"report": {
"heroMuscles_one": "{{count}} عضله",
Expand Down
5 changes: 4 additions & 1 deletion app/public/locales/nb/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
7 changes: 6 additions & 1 deletion app/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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();
}, []);
Expand Down
19 changes: 18 additions & 1 deletion app/src/components/Settings.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -79,6 +79,23 @@ export default function Settings() {
/>
</div>

<SectionLabel>{t("settings.myGym")}</SectionLabel>
<div style={{ padding: "0 16px 24px" }}>
<div style={cardStyle}>
<Tag type="green" size="md" style={{ marginBottom: 8 }}>
{t("settings.myGymMembership")}
</Tag>
<p style={{
color: "var(--cds-text-secondary)",
fontFamily: "var(--cds-font-sans)",
fontSize: 13,
margin: 0,
}}>
{t("settings.myGymFutureHint")}
</p>
</div>
</div>

<SectionLabel>{t("settings.contact")}</SectionLabel>
<div style={{ padding: "0 16px 24px" }}>
<div style={cardStyle}>
Expand Down
28 changes: 28 additions & 0 deletions app/src/lib/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Loading