From 3cc6a74be954043e6d4bb05e5737fbb84b592768 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 00:05:57 +0000 Subject: [PATCH 1/2] feat: gym-wide shared templates and exercise library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Templates and exercises are now visible and editable by all co-instructors at the same gym. Replaces user-private RLS (auth.uid() = user_id) with gym-aware policies using the same user_gyms EXISTS subquery pattern already in place for sessions. Creator attribution shown in Bibliotek. Tab label "Mine maler" → "Maler". https://claude.ai/code/session_01BX3VsjZbhAR5n85NbUmZRJ --- CHANGELOG.md | 9 +++++++++ CLAUDE.md | 14 +++++++++++++- README.md | 2 +- app/public/locales/en/translation.json | 3 ++- app/public/locales/fa/translation.json | 3 ++- app/public/locales/nb/translation.json | 3 ++- app/src/components/Bibliotek.jsx | 13 +++++++++++++ app/src/lib/db.js | 21 ++++++--------------- 8 files changed, 48 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc5ea8d..71ecdc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to Workout Lens are documented here. +## [1.2.5] — 2026-05-14 + +### Added +- **Gym-wide shared templates and exercise library** — `session_templates` and `exercise_library` are now fully shared across co-instructors at the same gym. Any instructor can create, edit, rename, and delete any template or exercise. `user_id` is retained as "created by" for attribution only. Creator name ("Av [name]") is shown on template cards and exercise rows in Bibliotek when the item was created by a colleague. Bibliotek "Mine maler" tab renamed to "Maler". +- **RLS migration (`gym_wide_templates_and_exercises`)** — replaced `auth.uid() = user_id` all-ops policies on `session_templates`, `session_template_exercises`, and `exercise_library` with gym-aware policies using the same-gym `user_gyms` EXISTS subquery pattern already used for sessions. INSERT still requires `auth.uid() = user_id`; SELECT/UPDATE/DELETE allow any co-instructor at the same gym. + +### Note +Editing an exercise's muscle mapping does **not** retroactively update historical session data. `muscle_activations` rows are permanent snapshots written at log time with no FK to `exercise_library`. + ## [1.2.4] — 2026-05-12 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index d9ff1c3..902c5e1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,7 +52,7 @@ Fully migrated to IBM Carbon Design System (issue #8, resolved 2026-04-29). - `Login.jsx` → Carbon `TextInput`, `Button`, `InlineNotification`, `Email` icon; `getDailyQuote()` renders a date-aware motivational quote below the subtitle — English only (hardcoded; language preference is unknown before login); keyed by `MM-DD` for special dates (`01-01`, `12-24`), falls back to a per-weekday quote; 13px italic `var(--cds-text-secondary)` - `MuscleMap.jsx` → Carbon `Header` + `HeaderGlobalBar` (with `RecentlyViewed` history nav, `Book` library nav, light/dark toggle), `ProgressIndicator` (horizontal stepper with step labels), `Button`, `Tag`, `InlineLoading`, `InlineNotification`; dashed-border dropzone on upload step; sticky action bar on confirm step; exercise rows delegated to `ExerciseRow` - `History.jsx` → `SectionLabel` + `PageHeading` hero (context-aware: default shows month count; filter active + date selected shows "N av total økter den dato"; filter active + no date shows month count with "med disse filtrene"); `PageHeading` has `minHeight: 72` to prevent layout shift; muscle filter chips use `flexWrap: wrap` (all always visible); `borderBottom` separator below chip section; session rows always have 3px left strip (accent when filter-matched); session title in Cond 700; custom `MonthGrid` calendar; expanded sessions are always editable — per-session edit state in a `Map` (no global `editMode` boolean); a dirty-state Save / Discard bar appears when changes are detected; "Legg til øvelse manuelt" (`Add` icon) and "Last opp nytt bilde" (`Camera` icon) rendered as sibling `Button kind="ghost"` on one row below the exercise list; session header chips capped at 2 visible with `+N` overflow span; library exercises pre-fetched on mount (not on first expand) to ensure autocomplete is ready when user adds first exercise to a session with 0 exercises; exercise rows delegated to `ExerciseRowWithAutocomplete`; all date formatting via `Intl.DateTimeFormat` driven by `i18n.language` -- `Bibliotek.jsx` → custom pill tab strip (replaces Carbon `Tabs`; keyboard ArrowLeft/ArrowRight); tabs: "Øvelser" and "Mine maler"; `PageHeading` hero; live search input on exercises tab with load-more (batches of 20 when >20 shown); "Ny øvelse" button below search input; no Snarveier carousel; search input on templates tab with load-more (batches of 12 when >12); template cards show exercise count + muscle count only (no `used_at` date); exercise rows use `AccentChip` for primary muscles + Cond 700 name + 3px accent left strip; template cards use `borderRadius: var(--r-card)`; exercise form via `ExerciseForm` +- `Bibliotek.jsx` → custom pill tab strip (replaces Carbon `Tabs`; keyboard ArrowLeft/ArrowRight); tabs: "Øvelser" and "Maler"; `PageHeading` hero; live search input on exercises tab with load-more (batches of 20 when >20 shown); "Ny øvelse" button below search input; no Snarveier carousel; search input on templates tab with load-more (batches of 12 when >12); template cards show exercise count + muscle count only (no `used_at` date); exercise rows use `AccentChip` for primary muscles + Cond 700 name + 3px accent left strip; template cards use `borderRadius: var(--r-card)`; exercise form via `ExerciseForm`; creator attribution ("Av [name]") shown on exercise rows and template cards when the item was created by a different co-instructor - `TemplatePicker.jsx` → Carbon `Button`, `InlineLoading`, `InlineNotification` - `TemplateSessionEditor.jsx` → `layer-02` + 2px accent top border container; `SectionLabel renderIcon={Edit}` header; Carbon `TextInput` for template name (inline rename); step indicator in use mode ("Steg 2 av 3"); no "Lagre mal" in use mode; body map via `BodyPanel`; exercise rows via `ExerciseRowWithAutocomplete`; library search via `LibraryPicker` - `MuscleMap.jsx` confirm step → wrapped in `layer-02` + 2px accent top border container; `SectionLabel renderIcon={Edit}` header; Carbon `DatePicker`/`DatePickerInput` for session date (defaults to today, max = today) @@ -164,6 +164,18 @@ week_plan_days `week_plan_days.template_id` nullable — an empty slot is a valid row with `template_id = null`. RLS on both tables restricts all operations to the owning user (`auth.uid() = user_id` / exists check via join). +## Gym-wide shared templates and exercise library (2026-05-14) + +`session_templates` and `exercise_library` are **gym-wide**: any co-instructor at the same gym (via `user_gyms` join) can SELECT, INSERT, UPDATE, and DELETE. `user_id` is retained on both tables as "created by" for attribution display only — it is no longer an ownership gate. + +RLS policies replaced (migration `gym_wide_templates_and_exercises`): +- Old: `auth.uid() = user_id` (ALL ops) on all three tables +- New: separate INSERT policy (`auth.uid() = user_id`) + SELECT/UPDATE/DELETE policies using the same-gym EXISTS subquery already used for sessions; `session_template_exercises` uses a JOIN via `session_templates.user_id` + +`db.js` changes: removed `.eq("user_id", user.id)` defensive filters from `updateTemplateName`, `deleteTemplate`, `touchTemplate`, `updateLibraryExercise`, `deleteLibraryExercise`; added `profiles!user_id(display_name)` join to `fetchTemplates` and `fetchLibraryExercises`. + +**Editing an exercise does NOT rewrite historical sessions.** `muscle_activations` rows are permanent snapshots written at log time with no FK to `exercise_library`. Correcting a muscle mapping in the library only affects future sessions. + `db.js` functions: | Function | Description | |---|---| diff --git a/README.md b/README.md index f28096c..7c7a3eb 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Photograph a handwritten gym whiteboard workout, and the app tells you which mus 5. **Recommendations** — ask Claude what to train next based on untrained muscle groups 6. **Save** — session is persisted to Supabase with full exercise and muscle activation data 7. **History** — custom month grid calendar with heat colors per day (darker = more exercises); click a day to see that session's muscle map and exercise list; sessions are always editable when expanded — a Save / Discard bar appears automatically when changes are detected; add exercises with library autocomplete and AI muscle inference; upload a new photo at any time to re-analyse -8. **Library** — build a named exercise library with click-to-toggle muscle selection; AI muscle inference fires when you type an exercise name and leave the field — muscles are filled in automatically and marked "Muskler satt av AI"; create session templates (e.g. "CrossFit - Anna - mandag") as reusable collections of library exercises +8. **Library** — shared gym-wide exercise library and session templates: any co-instructor can create, edit, or delete exercises and templates; AI muscle inference fires when you type an exercise name; creator attribution ("Av [name]") shown on items created by colleagues 9. **Weekly planner** — assign templates to each day of the week; an "Ikke trent denne uken" chip row lists the muscles you have not yet trained in logged sessions for the visible ISO week (History-style mono pills); a live "Projisert dekning" heatmap body map shows projected cumulative muscle coverage from the assigned templates; a Forslag card flags muscle groups with no planned coverage; plan is saved to Supabase and reloaded on next visit 10. **Language** — switch between Norsk, English and فارسی (RTL) at any time from Settings; all UI strings, date formats, and month names update instantly 11. **Settings** — language selector (top), theme toggle (dark/light) + nav hints toggle with live body map preview, contact, Om appen section (version + "Vis introduksjonsguide" replay button + changelog accordion), and account section: display name input + sign-out (bottom) diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json index a4dad6a..06c8571 100644 --- a/app/public/locales/en/translation.json +++ b/app/public/locales/en/translation.json @@ -207,7 +207,8 @@ "sectionLabel": "LIBRARY", "heading": "Your building blocks.", "tabExercises": "Exercises", - "tabTemplates": "My templates", + "tabTemplates": "Templates", + "createdBy": "By {{name}}", "newExercise": "New exercise", "searchPlaceholder": "Search exercise…", "loadingExercises": "Loading exercises…", diff --git a/app/public/locales/fa/translation.json b/app/public/locales/fa/translation.json index 01e95b0..33cf726 100644 --- a/app/public/locales/fa/translation.json +++ b/app/public/locales/fa/translation.json @@ -207,7 +207,8 @@ "sectionLabel": "کتابخانه", "heading": "بلوک‌های سازنده شما.", "tabExercises": "تمرین‌ها", - "tabTemplates": "قالب‌های من", + "tabTemplates": "قالب‌ها", + "createdBy": "توسط {{name}}", "newExercise": "تمرین جدید", "searchPlaceholder": "جستجوی تمرین…", "loadingExercises": "در حال بارگذاری تمرین‌ها…", diff --git a/app/public/locales/nb/translation.json b/app/public/locales/nb/translation.json index 4cefd5c..4182329 100644 --- a/app/public/locales/nb/translation.json +++ b/app/public/locales/nb/translation.json @@ -207,7 +207,8 @@ "sectionLabel": "BIBLIOTEK", "heading": "Dine byggeklosser.", "tabExercises": "Øvelser", - "tabTemplates": "Mine maler", + "tabTemplates": "Maler", + "createdBy": "Av {{name}}", "newExercise": "Ny øvelse", "searchPlaceholder": "Søk øvelse…", "loadingExercises": "Laster øvelser…", diff --git a/app/src/components/Bibliotek.jsx b/app/src/components/Bibliotek.jsx index 2529641..90c573c 100644 --- a/app/src/components/Bibliotek.jsx +++ b/app/src/components/Bibliotek.jsx @@ -6,6 +6,7 @@ import { import { Add, TrashCan, Edit as EditIcon, ChevronRight, Search } from "@carbon/icons-react"; import { useTranslation } from "react-i18next"; import { logDevError } from "../lib/utils"; +import { supabase } from "../lib/supabase"; import PageShell, { SectionLabel, PageHeading, AccentChip } from "./PageShell"; import { fetchLibraryExercises, saveLibraryExercise, updateLibraryExercise, deleteLibraryExercise, @@ -44,8 +45,10 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { const [exVisible, setExVisible] = useState(20); const [tplSearch, setTplSearch] = useState(""); const [tplVisible, setTplVisible] = useState(12); + const [currentUserId, setCurrentUserId] = useState(null); useEffect(() => { + supabase.auth.getUser().then(({ data: { user } }) => setCurrentUserId(user?.id ?? null)); fetchLibraryExercises() .then(setExercises) .catch(e => { logDevError("Bibliotek/fetchExercises", e); setExError(e.message); }) @@ -280,6 +283,11 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) { {t("bibliotek.noMuscles")} )} + {currentUserId && ex.user_id !== currentUserId && ex.profiles?.display_name && ( +
+ {t("bibliotek.createdBy", { name: ex.profiles.display_name })} +
+ )} {(ex.default_sets && ex.default_reps) && ( @@ -408,6 +416,11 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
{t("bibliotek.exerciseCount", { count: exCount })} · {muscleCount} MUS
+ {currentUserId && tpl.user_id !== currentUserId && tpl.profiles?.display_name && ( +
+ {t("bibliotek.createdBy", { name: tpl.profiles.display_name })} +
+ )}