From 3e99edb81cbe0e25d52e95acec65bee013d1c053 Mon Sep 17 00:00:00 2001
From: Christopher Rotnes
Date: Thu, 7 May 2026 01:05:20 +0200
Subject: [PATCH 01/10] =?UTF-8?q?feat:=20UI/UX=20overhaul=20=E2=80=94=20ed?=
=?UTF-8?q?it=20consistency,=20always-on=20history=20editing,=20visibility?=
=?UTF-8?q?=20removal,=20report=20restructure,=20library=20scaling=20(#147?=
=?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Sonnet 4.6
---
CHANGELOG.md | 10 +-
CLAUDE.md | 16 +-
README.md | 4 +-
app/public/locales/en/translation.json | 28 +-
app/public/locales/fa/translation.json | 28 +-
app/public/locales/nb/translation.json | 28 +-
app/src/components/Bibliotek.jsx | 208 +++++---
app/src/components/ExerciseForm.jsx | 20 +-
app/src/components/History.jsx | 482 +++++++++----------
app/src/components/MuscleMap.jsx | 7 +-
app/src/components/PageShell.jsx | 10 +-
app/src/components/Report.jsx | 249 ++++------
app/src/components/Settings.jsx | 9 +
app/src/components/TemplateSessionEditor.jsx | 114 ++---
app/src/lib/db.js | 11 +-
15 files changed, 625 insertions(+), 599 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1ee92b..78e2562 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,11 +5,17 @@ All notable changes to Workout Lens are documented here.
## [Unreleased]
### Added
-- **Joint class history (#138)** — expanding a gym-linked session in History now shows a "Kolleger i denne klassen" panel listing co-instructor sessions for the same class slot. Display name (or "Instruktør" fallback) is shown as a header per colleague, with their exercise list below. Fetched lazily on first expand and cached per `gym_calendar_id`. New RLS policy on `sessions` allows same-gym users to read each other's shared sessions. `fetchClassHistory(gymCalendarId)` added to `db.js`.
-- **Session privacy (#139)** — `visibility` column added to `sessions` (default `'shared'`). History edit mode gains a Carbon `Toggle` ("Del med andre instruktører") that persists to `visibility = 'private'` on save. Private sessions are excluded from the cross-gym RLS policy and from `fetchClassHistory`. `updateSession` accepts a `visibility` option.
+- **Joint class history (#138)** — expanding a gym-linked session in History now shows a "Kolleger i denne klassen" panel listing co-instructor sessions for the same class slot. Display name (or "Instruktør" fallback) is shown as a header per colleague, with their exercise list below. Fetched lazily on first expand and cached per `gym_calendar_id`. New RLS policy on `sessions` allows same-gym users to read each other's sessions. `fetchClassHistory(gymCalendarId)` added to `db.js`.
- **Display name (#141)** — `display_name text` column (max 50 chars) added to `profiles`. Settings → Konto section now has a `TextInput` to set/update a display name, with success/error feedback. Same-gym RLS policy on `profiles` allows co-instructors to read each other's `display_name`. `fetchDisplayName()` and `updateDisplayName()` added to `db.js`. Display name is shown next to colleague sessions in the joint class history view.
+- **GDPR transparency note** — Settings → Konto now shows an informational paragraph explaining that all logged sessions are visible to co-instructors at the same gym, in line with the app's purpose.
### Changed
+- **Session visibility removed** — the `visibility` / "Del med andre instruktører" toggle has been removed entirely. All sessions logged under a gym are now always visible to co-instructors at the same gym (the intended behaviour). The Supabase RLS policy on `sessions` was updated to remove the `visibility = 'shared'` filter; all existing private sessions were backfilled to shared. `updateSessionVisibility` removed from `db.js`.
+- **History — always-on inline editing** — sessions are always editable when expanded; the "Rediger økt" button and locked read state are gone. A sticky Save / Discard / Reupload bar appears automatically when any change is detected (dirty state). Fixes the filter+edit bug where an active muscle filter prevented entering edit mode. The muscle groups section (redundant with the body map) is removed from the expanded view. "Re-analyser" renamed to "Last opp nytt bilde".
+- **Edit panel visual consistency (#147)** — all edit/entry containers now share the same surface treatment: `var(--cds-layer-02)` background + 2px `var(--accent)` top border + `SectionLabel` with icon header. Applies to `ExerciseForm`, `TemplateSessionEditor`, and the MuscleMap confirm step. Cancel buttons changed to `kind="ghost"`, errors shown as `InlineNotification kind="error"` above the button bar. `SectionLabel` now accepts a `renderIcon` prop.
+- **Template use flow** — "Lagre mal" is no longer shown in the template use flow (Planlegger → Bruk økt). A step indicator ("Steg 2 av 3 — Tilpass øvelser") is shown instead. Template name input replaced with Carbon `TextInput`.
+- **Report — restructured layout** — the "Ikke trent" gap card is now positioned after the muscle frequency table, directly above the recommendation button, acting as a visual header for the recommendation section. The post-recommendation body map (`BodySVG`) is removed. Fallback messages added: if all primary muscles are trained the gap section shows a success message; if some are secondary-only only those are listed.
+- **Library — scaling** — the Snarveier carousel is capped at 6 items with a "Se alle →" link to the templates tab. Load-more buttons (20 exercises / 12 templates per batch) appear when lists exceed their threshold. A search input is added to the templates tab.
- **Test suite — better coverage, less noise** — replaced low-value assertions (one-line constant checks, per-model `it`s, a duplicated prompt assertion) with behavioural tests, and filled the largest gaps in `utils.js` (date helpers `toIsoDate`/`toWeekIso`/`weekIsoToMonday`/`isoWeekMonday`, `isInvalidNum`, `extractMuscles`, `getIntlLocale`, `inferMusclesFromName`) and `prompts.js` (`buildMuscleInferencePrompt`). Added a fake-timer test for `checkRateLimit` window expiry. Net: 60 → 82 tests; `utils.js` line coverage ~30% → ~80%, `prompts.js` to 100% statements.
---
diff --git a/CLAUDE.md b/CLAUDE.md
index 40f4e7f..0dd5e5c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -51,16 +51,16 @@ Fully migrated to IBM Carbon Design System (issue #8, resolved 2026-04-29).
- `app/src/theme.jsx` — `ThemeProvider` sets `data-theme="g10"` or `data-theme="g100"` on ``, persists to `localStorage`, respects `prefers-color-scheme`, defaults to g100 (dark)
- `Login.jsx` → Carbon `TextInput`, `Button`, `InlineNotification`, `Email` icon; `getDailyQuote()` renders a date-aware motivational quote below the subtitle — keyed by `MM-DD` for special dates (`01-01`, `05-05`, `05-17`, `12-24`), falls back to a per-weekday quote (mandag–søndag); 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; edit mode uses `Edit`, `Camera`, `Add`, `Renew` icons; exercise rows delegated to `ExerciseRow`; all date formatting via `Intl.DateTimeFormat` driven by `i18n.language`
-- `Bibliotek.jsx` → custom pill tab strip (replaces Carbon `Tabs`; keyboard ArrowLeft/ArrowRight); `PageHeading` hero; live search input on exercises tab; Snarvei template carousel; exercise rows use `AccentChip` for primary muscles + Cond 700 name + 3px accent left strip; template cards use `borderRadius: var(--r-card)`; "Ny øvelse" button renders above Snarveier carousel to prevent tab-switch layout shift; exercise form via `ExerciseForm`
+- `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 / Reupload bar appears when changes are detected; `Camera`, `Add`, `Renew` icons; 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); `PageHeading` hero; live search input on exercises tab with load-more (batches of 20 when >20 shown); Snarvei template carousel (capped at 6, "Se alle →" link to templates tab); search input on templates tab with load-more (batches of 12 when >12); exercise rows use `AccentChip` for primary muscles + Cond 700 name + 3px accent left strip; template cards use `borderRadius: var(--r-card)`; "Ny øvelse" button renders above Snarveier carousel to prevent tab-switch layout shift; exercise form via `ExerciseForm`
- `TemplatePicker.jsx` → Carbon `Button`, `InlineLoading`, `InlineNotification`
-- `TemplateSessionEditor.jsx` → Carbon `Button`, `Tag`, `InlineNotification`, `InlineLoading`; body map via `BodyPanel`; exercise rows via `ExerciseRow`; library search via `LibraryPicker`
-- `MuscleMap.jsx` confirm step → Carbon `DatePicker`/`DatePickerInput` for session date (defaults to today, max = today)
+- `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)
- `BodySVG` / `HeatmapBodySVG` muscle highlights: primary → `var(--heat-4)` solid green, secondary → diagonal blue hatch (`#001d6c` base + `#4589ff` lines). `HeatmapBodySVG` accepts `onHover(id|null)` and `hovered` props — when `onHover` is provided the internal floating tooltip is suppressed and the caller manages the detail card.
- `Home.jsx` → `SectionLabel` + `PageHeading` headings; last session card with gym-class identity hero; 7-day weekly strip with heat colors — clicking a day that has a session navigates to History pre-selected on that date; `fetchThisWeekSessions` in `db.js`
-- `Report.jsx` → `SectionLabel` eyebrow with period + active day filters on two separate `display:block` spans; two-line Cond 700 hero (untrained count in magenta + "aldri trent."); three separate `flexWrap: wrap` filter rows (period / weekdays / session types) with `1px solid var(--border-subtle-wl)` top borders between groups; "Nullstill filter" always rendered (opacity-toggled); gap callout card uses `var(--accent-bg-08)` with `AccentChip` per untrained muscle; recommendation rows have 3px accent left strip + round `+` button that saves the exercise inline via `saveLibraryExercise` (no navigation away); on success button becomes a disabled `Checkmark` icon (grayed out, stays that way); Postgres 23505 duplicate treated as success; save errors show an `InlineNotification kind="error"` above the recs list; `savedRecs` (Set), `savingRec`, `saveRecError` state tracks per-row state; `StickyCta` "Disse bør du legge inn i programmet →"; prefill prop applied on mount via `useRef` — supports `periodDays`, `selectedDays`, `selectedTypes`, `weekday` (pre-selects the weekday chip), and `sessionType` (pre-selects the session type chip); `KpiTile` (42px Plex Light value); `muscleLastDate` in useMemo
+- `Report.jsx` → `SectionLabel` eyebrow with period + active day filters on two separate `display:block` spans; three separate `flexWrap: wrap` filter rows (period / weekdays / session types) with `1px solid var(--border-subtle-wl)` top borders between groups; "Nullstill filter" always rendered (opacity-toggled); KPI tiles → heatmap body → hover detail → heat legend → frequency table → gap callout card (with `AccentChip` per untrained muscle) → recommendation button → recs list (no post-rec body map); when all primary muscles trained shows positive fallback message; when some muscles secondary-only shows those as blue tags; recommendation rows have 3px accent left strip + round `+` button that saves the exercise inline via `saveLibraryExercise`; `StickyCta` "Disse bør du legge inn i programmet →"; prefill prop applied on mount via `useRef` — supports `periodDays`, `selectedDays`, `selectedTypes`, `weekday`, `sessionType`; `KpiTile` (42px Plex Light value); `muscleLastDate` in useMemo
- `History.jsx` → custom `MonthGrid` (7-column CSS grid, heat fill, today/selected outlines, month nav); `sessionCountMap` useMemo; `SectionLabel` + `PageHeading` at top; removed `react-day-picker` dependency entirely
-- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`; `NavBtn` active state: 2px `var(--accent)` bottom border + `var(--cds-layer-01)` background; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Book (Bibliotek) → Settings — 6 icons, each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here
+- `PageShell.jsx` → exports: `SectionLabel` (mono 12px, 0.16em tracking, 3px `var(--accent)` left border; accepts optional `renderIcon` prop — renders the Carbon icon at 14px before the label text), `PageHeading` (Cond 700 28px), `PageTitle` (alias for SectionLabel), `AccentChip` (magenta pill: `var(--accent-bg-14)` bg, `var(--accent-soft)` text), `StickyCta` (sticky bottom bar with top border), `BackButton`; `NavBtn` active state: 2px `var(--accent)` bottom border + `var(--cds-layer-01)` background; nav icons in order: Camera → RecentlyViewed → Analytics → EventSchedule (Planlegger) → Book (Bibliotek) → Settings — 6 icons, each 48px wide; theme toggle and logout removed from header (now in Settings view); `ChangelogModal` no longer rendered here
- `carbon-tokens.css` → added `--heat-1..5` green scale (#044317 → #42be65); WL custom tokens: `--accent` (#ee2c80 magenta), `--surface-card`, `--border-subtle-wl`, `--text-muted-wl`, `--accent-bg-08/14/30`, `--accent-soft`, `--r-card` (16px), `--r-pill` (999px), `--r-tile` (10px), `--cond` (IBM Plex Sans Condensed); g10 light-mode overrides for all WL tokens
- `app.css` → global `html, body { overflow-x: hidden }` to prevent horizontal viewport bleed from chip rows; do not use `overflow: hidden` on direct parents of `flexWrap: wrap` chip containers — it clips instead of scrolling
- Removed: Bebas Neue, DM Sans, Google Fonts import, custom `C` token objects, all raw hex colors, rounded corners, `react-day-picker`, `date-fns`
@@ -200,8 +200,8 @@ week_plan_days
- **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.
- **Display name:** `profiles` has `display_name text CHECK (char_length(display_name) <= 50)`. RLS: existing "Brukere ser sin egen profil" / "Brukere oppdaterer sin egen profil" policies cover self-reads and writes; new "Same-gym users can read profiles" SELECT policy exposes `display_name` to co-instructors at the same gym. `fetchDisplayName()` / `updateDisplayName(name)` in `db.js`. Settings → Konto exposes a TextInput.
-- **Session visibility:** `sessions.visibility text NOT NULL DEFAULT 'shared' CHECK (visibility IN ('shared', 'private'))`. Cross-gym SELECT policy "Same-gym users can read sessions" requires `visibility = 'shared'`; own sessions are always accessible via the existing ALL policy. `updateSession` accepts `{ visibility }` option (default `'shared'`). History edit mode shows a Carbon `Toggle` ("Del med andre instruktører") that persists the flag on save.
-- **Joint class history:** `fetchClassHistory(gymCalendarId)` in `db.js` returns shared co-instructor sessions for a given gym class instance (excludes own, requires `visibility = 'shared'`), with joined `profiles(display_name)` and `session_exercises`. History lazy-fetches on first expand of a gym-linked session; cached in `classHistory` Map state (key: `gym_calendar_id`). Panel renders in read mode only, after the muscle-groups section, showing display name + exercise list per colleague.
+- **Session visibility (removed):** The `sessions.visibility` column exists in the DB but is no longer used. The "Same-gym users can read sessions" RLS policy was updated to remove the `visibility = 'shared'` filter — all sessions are cross-readable by co-instructors at the same gym. `updateSessionVisibility` is removed from `db.js`; the History visibility Toggle is gone. Settings → Konto shows an informational GDPR paragraph.
+- **Joint class history:** `fetchClassHistory(gymCalendarId)` in `db.js` returns co-instructor sessions for a given gym class instance (excludes own), with joined `profiles(display_name)` and `session_exercises`. History lazy-fetches on first expand of a gym-linked session; cached in `classHistory` Map state (key: `gym_calendar_id`). Panel always renders in the expanded session view, showing display name + exercise list per colleague.
## 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/README.md b/README.md
index 3cc0c45..bb91f90 100644
--- a/README.md
+++ b/README.md
@@ -10,12 +10,12 @@ Photograph a handwritten gym whiteboard workout, and the app tells you which mus
4. **Muscle map** — front and back body SVG; primary muscles glow solid green, secondary muscles show as blue diagonal stripes; hover for exercise names
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; edit or re-analyse any saved session; edit mode supports library autocomplete — type an exercise name to get suggestions from your library; AI muscle inference fires automatically when you add a new exercise with no muscles assigned (tab or click away from the name field)
+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
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) with live body map preview, contact, changelog, and account section: display name input + sign-out (bottom)
-12. **Joint class history** — when a gym-linked session is expanded in History, a "Kolleger i denne klassen" panel shows co-instructor sessions for the same class slot (display name + exercise list). Sessions default to shared; an edit-mode toggle marks them private to exclude them from the shared view
+12. **Joint class history** — when a gym-linked session is expanded in History, a "Kolleger i denne klassen" panel shows co-instructor sessions for the same class slot (display name + exercise list). All sessions are always visible to co-instructors at the same gym — this cross-instructor transparency is the core value of the shared view
## Tech stack
diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json
index 00b7e4a..53a6b29 100644
--- a/app/public/locales/en/translation.json
+++ b/app/public/locales/en/translation.json
@@ -23,7 +23,8 @@
"front": "Front",
"back_view": "Back",
"saveFailed": "Saving failed. Try again.",
- "add": "Add"
+ "add": "Add",
+ "discard": "Discard"
},
"muscles": {
"chest": "Chest",
@@ -151,7 +152,8 @@
"savingError": "Saving failed",
"progressLabel": "Progress",
"primaryTag": "Primary",
- "secondaryTag": "Secondary"
+ "secondaryTag": "Secondary",
+ "confirmLabel": "Confirm session"
},
"history": {
"sectionLabel": "HISTORY",
@@ -163,11 +165,9 @@
"primaryCount": "Primary ({{count}})",
"secondaryCount": "Secondary ({{count}})",
"reanalyze": "Re-analyze",
+ "reuploadPhoto": "Upload new photo",
"analyzing": "Analyzing…",
"editSession": "Edit session",
- "shareWithColleagues": "Share with other instructors",
- "shareOn": "Shared",
- "shareOff": "Private",
"classHistory": "Colleagues in this class",
"classHistoryLoading": "Loading colleague sessions…",
"classHistoryError": "Could not load colleague sessions",
@@ -228,7 +228,10 @@
"usedInTemplates_one": "The exercise is used in the template",
"usedInTemplates_other": "The exercise is used in the templates",
"exerciseRemovedWarning": "and will be removed from it.",
- "exerciseCount": "{{count}} EX"
+ "exerciseCount": "{{count}} EX",
+ "searchTemplates": "Search templates…",
+ "showMore": "Show {{count}} more",
+ "seeAll": "See all"
},
"planlegger": {
"heading": "Plan the week",
@@ -292,7 +295,8 @@
"languagePersian": "فارسی",
"myGym": "My gym",
"myGymMembership": "Sporty Thon Senter Ski",
- "myGymFutureHint": "Coming soon: choose your gym and see shared session history with other instructors."
+ "myGymFutureHint": "Coming soon: choose your gym and see shared session history with other instructors.",
+ "dataSharingNote": "All sessions you log are visible to other instructors at the same gym. This is necessary to give you insight into what your colleagues are training, and is part of the service's purpose."
},
"report": {
"heroMuscles_one": "{{count}} muscle",
@@ -319,6 +323,8 @@
"noSessions": "No sessions found for selected filter.",
"saveRecError": "Could not save exercise. Try again.",
"fetchRecError": "Could not fetch recommendations. Try again.",
+ "allMusclesPrimary": "All 17 muscle groups trained this period. Great work!",
+ "allMusclesSecondaryNote": "All primary muscles trained. These are secondary only:",
"toCta": "Add these to your program →",
"period": "PERIOD",
"activeDays": "ACTIVE DAYS",
@@ -356,7 +362,9 @@
"defaultSets": "Default sets",
"defaultReps": "Default reps",
"saveExercise": "Save exercise",
- "noMusclesWarning": "No muscles selected — click the figure to register"
+ "noMusclesWarning": "No muscles selected — click the figure to register",
+ "headerNew": "New exercise",
+ "headerEdit": "Edit exercise"
},
"libraryPicker": {
"searchLabel": "Search exercise library",
@@ -398,6 +406,8 @@
"manual": "Manually",
"saveChanges": "Save template changes",
"useSession": "Use session",
- "saveTemplate": "Save template"
+ "saveTemplate": "Save template",
+ "stepIndicator": "Step 2 of 3 — Adjust exercises",
+ "nameLabel": "Template name"
}
}
diff --git a/app/public/locales/fa/translation.json b/app/public/locales/fa/translation.json
index 7bc63d7..aeeb8d7 100644
--- a/app/public/locales/fa/translation.json
+++ b/app/public/locales/fa/translation.json
@@ -23,7 +23,8 @@
"front": "جلو",
"back_view": "پشت",
"saveFailed": "ذخیره ناموفق بود. دوباره امتحان کنید.",
- "add": "افزودن"
+ "add": "افزودن",
+ "discard": "لغو تغییرات"
},
"muscles": {
"chest": "سینه",
@@ -151,7 +152,8 @@
"savingError": "ذخیره ناموفق بود",
"progressLabel": "پیشرفت",
"primaryTag": "اولیه",
- "secondaryTag": "ثانویه"
+ "secondaryTag": "ثانویه",
+ "confirmLabel": "تأیید جلسه"
},
"history": {
"sectionLabel": "تاریخچه",
@@ -163,11 +165,9 @@
"primaryCount": "اولیه ({{count}})",
"secondaryCount": "ثانویه ({{count}})",
"reanalyze": "تحلیل مجدد",
+ "reuploadPhoto": "آپلود عکس جدید",
"analyzing": "در حال تحلیل…",
"editSession": "ویرایش جلسه",
- "shareWithColleagues": "اشتراک با مربیان دیگر",
- "shareOn": "اشتراکی",
- "shareOff": "خصوصی",
"classHistory": "همکاران در این کلاس",
"classHistoryLoading": "در حال بارگذاری جلسات همکاران…",
"classHistoryError": "بارگذاری جلسات همکاران ممکن نشد",
@@ -228,7 +228,10 @@
"usedInTemplates_one": "این تمرین در قالب استفاده شده",
"usedInTemplates_other": "این تمرین در قالبها استفاده شده",
"exerciseRemovedWarning": "و از آن حذف خواهد شد.",
- "exerciseCount": "{{count}} تمرین"
+ "exerciseCount": "{{count}} تمرین",
+ "searchTemplates": "جستجو در قالبها…",
+ "showMore": "نمایش {{count}} بیشتر",
+ "seeAll": "مشاهده همه"
},
"planlegger": {
"heading": "برنامهریزی هفته",
@@ -292,7 +295,8 @@
"languagePersian": "فارسی",
"myGym": "باشگاه من",
"myGymMembership": "Sporty Thon Senter Ski",
- "myGymFutureHint": "به زودی: انتخاب باشگاه و مشاهده تاریخچه مشترک با سایر مربیان."
+ "myGymFutureHint": "به زودی: انتخاب باشگاه و مشاهده تاریخچه مشترک با سایر مربیان.",
+ "dataSharingNote": "تمام جلسات ثبتشده برای سایر مربیان در همان مرکز ورزشی قابل مشاهده است. این برای ارائه بینش درباره تمرین همکاران ضروری است."
},
"report": {
"heroMuscles_one": "{{count}} عضله",
@@ -319,6 +323,8 @@
"noSessions": "جلسهای برای فیلتر انتخابشده یافت نشد.",
"saveRecError": "ذخیره تمرین ناموفق بود. دوباره امتحان کنید.",
"fetchRecError": "دریافت پیشنهادها ناموفق بود. دوباره امتحان کنید.",
+ "allMusclesPrimary": "همه ۱۷ گروه عضلانی در این دوره تمرین شده. آفرین!",
+ "allMusclesSecondaryNote": "همه عضلات اولیه تمرین شده. اینها فقط ثانویه تمرین شدهاند:",
"toCta": "اینها را به برنامهات اضافه کن →",
"period": "دوره",
"activeDays": "روزهای فعال",
@@ -356,7 +362,9 @@
"defaultSets": "ست پیشفرض",
"defaultReps": "تکرار پیشفرض",
"saveExercise": "ذخیره تمرین",
- "noMusclesWarning": "هیچ عضلهای انتخاب نشده — روی شکل کلیک کنید"
+ "noMusclesWarning": "هیچ عضلهای انتخاب نشده — روی شکل کلیک کنید",
+ "headerNew": "تمرین جدید",
+ "headerEdit": "ویرایش تمرین"
},
"libraryPicker": {
"searchLabel": "جستجو در کتابخانه تمرینها",
@@ -398,6 +406,8 @@
"manual": "دستی",
"saveChanges": "ذخیره تغییرات قالب",
"useSession": "شروع جلسه",
- "saveTemplate": "ذخیره قالب"
+ "saveTemplate": "ذخیره قالب",
+ "stepIndicator": "مرحله ۲ از ۳ — تنظیم تمرینها",
+ "nameLabel": "نام قالب"
}
}
diff --git a/app/public/locales/nb/translation.json b/app/public/locales/nb/translation.json
index ccc0546..3f5370a 100644
--- a/app/public/locales/nb/translation.json
+++ b/app/public/locales/nb/translation.json
@@ -23,7 +23,8 @@
"front": "Front",
"back_view": "Bak",
"saveFailed": "Lagring feilet. Prøv igjen.",
- "add": "Legg til"
+ "add": "Legg til",
+ "discard": "Forkast"
},
"muscles": {
"chest": "Bryst",
@@ -151,7 +152,8 @@
"savingError": "Lagring feilet",
"progressLabel": "Fremgang",
"primaryTag": "Primær",
- "secondaryTag": "Sekundær"
+ "secondaryTag": "Sekundær",
+ "confirmLabel": "Bekreft økt"
},
"history": {
"sectionLabel": "HISTORIKK",
@@ -163,11 +165,9 @@
"primaryCount": "Primær ({{count}})",
"secondaryCount": "Sekundær ({{count}})",
"reanalyze": "Re-analyser",
+ "reuploadPhoto": "Last opp nytt bilde",
"analyzing": "Analyserer…",
"editSession": "Rediger økt",
- "shareWithColleagues": "Del med andre instruktører",
- "shareOn": "Delt",
- "shareOff": "Privat",
"classHistory": "Kolleger i denne klassen",
"classHistoryLoading": "Henter kollegaøkter…",
"classHistoryError": "Kunne ikke hente kollegaøkter",
@@ -228,7 +228,10 @@
"usedInTemplates_one": "Øvelsen brukes i malen",
"usedInTemplates_other": "Øvelsen brukes i malene",
"exerciseRemovedWarning": "og vil bli fjernet derfra.",
- "exerciseCount": "{{count}} ØV"
+ "exerciseCount": "{{count}} ØV",
+ "searchTemplates": "Søk i maler…",
+ "showMore": "Vis {{count}} til",
+ "seeAll": "Se alle"
},
"planlegger": {
"heading": "Planlegg uken",
@@ -292,7 +295,8 @@
"languagePersian": "فارسی",
"myGym": "Min gym",
"myGymMembership": "Sporty Thon Senter Ski",
- "myGymFutureHint": "Fremtidig: velg gym og se felles økthistorikk med andre instruktører."
+ "myGymFutureHint": "Fremtidig: velg gym og se felles økthistorikk med andre instruktører.",
+ "dataSharingNote": "Alle økter du logger er synlige for andre instruktører ved samme treningssenter. Dette er nødvendig for å gi deg innsikt i hva kollegene dine trener, og er en del av tjenestens formål."
},
"report": {
"heroMuscles_one": "{{count}} muskel",
@@ -319,6 +323,8 @@
"noSessions": "Ingen økter funnet for valgte filter.",
"saveRecError": "Kunne ikke lagre øvelsen. Prøv igjen.",
"fetchRecError": "Kunne ikke hente anbefalinger. Prøv igjen.",
+ "allMusclesPrimary": "Alle 17 muskelgrupper er trent i perioden. Bra jobba!",
+ "allMusclesSecondaryNote": "Alle primærmuskelgrupper er trent. Disse er bare trent sekundært:",
"toCta": "Disse bør du legge inn i programmet →",
"period": "PERIODE",
"activeDays": "AKTIVE DAGER",
@@ -356,7 +362,9 @@
"defaultSets": "Standard sett",
"defaultReps": "Standard reps",
"saveExercise": "Lagre øvelse",
- "noMusclesWarning": "Ingen muskler valgt — klikk på figuren for å registrere"
+ "noMusclesWarning": "Ingen muskler valgt — klikk på figuren for å registrere",
+ "headerNew": "Ny øvelse",
+ "headerEdit": "Rediger øvelse"
},
"libraryPicker": {
"searchLabel": "Søk i øvelsesbiblioteket",
@@ -398,6 +406,8 @@
"manual": "Manuelt",
"saveChanges": "Lagre endringer i malen",
"useSession": "Bruk økt",
- "saveTemplate": "Lagre mal"
+ "saveTemplate": "Lagre mal",
+ "stepIndicator": "Steg 2 av 3 — Tilpass øvelser",
+ "nameLabel": "Navn på mal"
}
}
diff --git a/app/src/components/Bibliotek.jsx b/app/src/components/Bibliotek.jsx
index ef1373c..9686bde 100644
--- a/app/src/components/Bibliotek.jsx
+++ b/app/src/components/Bibliotek.jsx
@@ -42,6 +42,9 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
const [newTplName, setNewTplName] = useState("");
const [savingTpl, setSavingTpl] = useState(false);
const [showNewTpl, setShowNewTpl] = useState(false);
+ const [exVisible, setExVisible] = useState(20);
+ const [tplSearch, setTplSearch] = useState("");
+ const [tplVisible, setTplVisible] = useState(12);
useEffect(() => {
fetchLibraryExercises()
@@ -54,12 +57,18 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
.finally(() => setTplLoading(false));
}, []);
- const filteredExercises = useMemo(
- () => debouncedSearch.trim()
+ const filteredExercises = useMemo(() => {
+ const result = debouncedSearch.trim()
? exercises.filter(e => e.name.toLowerCase().includes(debouncedSearch.toLowerCase().trim()))
- : exercises,
- [exercises, debouncedSearch]
- );
+ : exercises;
+ setExVisible(20);
+ return result;
+ }, [exercises, debouncedSearch]);
+
+ const filteredTemplates = useMemo(() => {
+ const q = tplSearch.trim().toLowerCase();
+ return q ? templates.filter(t => t.name.toLowerCase().includes(q)) : templates;
+ }, [templates, tplSearch]);
const handleSaveNewExercise = async (fields) => {
setSavingEx(true);
@@ -194,7 +203,7 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
{t("bibliotek.shortcuts")}
+ If you did not request a password reset, you can safely ignore this email.
+
+
+
+
+ workout.umulig.org — you are receiving this email because a password reset was requested for this address.
+
+
From 5a32bc2952014fe9fc5df7cf1c7243af76ceb4c8 Mon Sep 17 00:00:00 2001
From: Christopher Rotnes
Date: Thu, 7 May 2026 01:27:09 +0200
Subject: [PATCH 05/10] feat: add reauthentication email template (#148)
Completes all 6 Supabase auth email templates with consistent branding.
Co-Authored-By: Claude Sonnet 4.6
---
supabase/config.toml | 4 ++++
supabase/templates/reauthentication.html | 25 ++++++++++++++++++++++++
2 files changed, 29 insertions(+)
create mode 100644 supabase/templates/reauthentication.html
diff --git a/supabase/config.toml b/supabase/config.toml
index 0059356..4d54b08 100644
--- a/supabase/config.toml
+++ b/supabase/config.toml
@@ -46,3 +46,7 @@ content_path = "./supabase/templates/recovery.html"
[auth.email.template.email_change]
subject = "Confirm your new Workout Lens email"
content_path = "./supabase/templates/email_change.html"
+
+[auth.email.template.reauthentication]
+subject = "Confirm your identity on Workout Lens"
+content_path = "./supabase/templates/reauthentication.html"
diff --git a/supabase/templates/reauthentication.html b/supabase/templates/reauthentication.html
new file mode 100644
index 0000000..bae7442
--- /dev/null
+++ b/supabase/templates/reauthentication.html
@@ -0,0 +1,25 @@
+
+
+ Workout Lens
+ workout.umulig.org
+
+
+
+
Confirm your identity
+
+ Enter the code below to confirm your identity on Workout Lens. The code expires in 10 minutes.
+
+
+
+ {{ .Token }}
+
+
+
+ If you did not request this, you can safely ignore this email.
+
+
+
+
+ workout.umulig.org — you are receiving this email because a sensitive action was requested on your account.
+
+
From f759ba4846de8dfc691a8be0d0d00a81addc3586 Mon Sep 17 00:00:00 2001
From: Christopher Rotnes
Date: Thu, 7 May 2026 02:23:42 +0200
Subject: [PATCH 06/10] =?UTF-8?q?fix:=20UI=20polish=20from=20PR=20#147=20r?=
=?UTF-8?q?eview=20=E2=80=94=2010=20UX=20fixes?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- History: remove username from edit box; unify ghost buttons; fix chip
overflow (+N); pre-fetch library on mount for autocomplete; clarify
gym conflict warning
- Bibliotek: rename "Maler" → "Mine maler"; remove Snarveier carousel;
remove used_at date from template cards
- TemplatePicker: remove "Sist brukt" date
- Planlegger: auto-save on add/remove; remove Lagre/Fjern uke buttons
- MuscleMap: remove "Neste steg" CTA and "Tips" callout
- Login: English-only quotes (language unknown before sign-in)
- app.css: strengthen Carbon Select fix (default + hover background)
Co-Authored-By: Claude Sonnet 4.6
---
CHANGELOG.md | 10 ++
CLAUDE.md | 8 +-
app/public/locales/en/translation.json | 10 +-
app/public/locales/fa/translation.json | 10 +-
app/public/locales/nb/translation.json | 10 +-
app/src/components/Bibliotek.jsx | 71 ++------------
app/src/components/History.jsx | 51 +++++-----
app/src/components/Login.jsx | 24 ++---
app/src/components/MuscleMap.jsx | 47 ---------
app/src/components/Planlegger.jsx | 127 +++++--------------------
app/src/components/TemplatePicker.jsx | 6 +-
app/src/styles/app.css | 4 +
12 files changed, 95 insertions(+), 283 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e44daa7..002456d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,16 @@ All notable changes to Workout Lens are documented here.
## [Unreleased]
+### Changed
+- **UI polish — post-#147 review (#147)** — ten UX fixes across History, Bibliotek, Planlegger, MuscleMap, TemplatePicker, and Login:
+ - **History** — removed username display from exercise edit box; "Legg til øvelse manuelt" and "Last opp nytt bilde" unified as sibling ghost buttons below the exercise list; session header chips capped at 2 visible + `+N` overflow to prevent title overflow; library pre-fetched on mount so autocomplete is always ready; gym-class conflict warning wording clarified
+ - **Bibliotek** — "Maler" tab renamed to "Mine maler"; Snarveier carousel removed (caused horizontal overflow); `used_at` date removed from template cards
+ - **TemplatePicker** — "Sist brukt" date removed from template cards
+ - **Planlegger** — "Lagre plan" and "Fjern uke" buttons removed; plan now auto-saves on every add/remove and auto-deletes when all slots are cleared
+ - **MuscleMap** — "NESTE STEG / Analyser perioden" CTA card removed from result step; "TIPS" callout removed from upload step
+ - **Login** — daily quotes hardcoded to English (language is unknown before sign-in)
+ - **Carbon Select** — global CSS fix strengthened to also force `background-color: var(--cds-field-01)` in default state, preventing white-on-white in all layer contexts
+
### Added
- **Email templates (#148)** — Supabase auth emails (magic link, invite, email confirmation) are now version-controlled in `supabase/templates/`. Branded with Workout Lens name, `workout.umulig.org` domain, magenta CTA button, and Carbon-matching dark colour scheme. Apply to the remote project with `supabase link` + `supabase config push`.
- **Joint class history (#138)** — expanding a gym-linked session in History now shows a "Kolleger i denne klassen" panel listing co-instructor sessions for the same class slot. Display name (or "Instruktør" fallback) is shown as a header per colleague, with their exercise list below. Fetched lazily on first expand and cached per `gym_calendar_id`. New RLS policy on `sessions` allows same-gym users to read each other's sessions. `fetchClassHistory(gymCalendarId)` added to `db.js`.
diff --git a/CLAUDE.md b/CLAUDE.md
index c64f57b..0ee5f3e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -49,10 +49,10 @@ Fully migrated to IBM Carbon Design System (issue #8, resolved 2026-04-29).
- IBM Plex fonts (Sans, Mono, Serif, Condensed) bundled locally in `app/public/fonts/` — no Google Fonts, no CDN
- `app/src/styles/carbon-tokens.css` — all Carbon CSS variables for g10 (light) and g100 (dark) themes, plus `@font-face` declarations; font URLs use `/fonts/...` (Vite public-dir absolute paths)
- `app/src/theme.jsx` — `ThemeProvider` sets `data-theme="g10"` or `data-theme="g100"` on ``, persists to `localStorage`, respects `prefers-color-scheme`, defaults to g100 (dark)
-- `Login.jsx` → Carbon `TextInput`, `Button`, `InlineNotification`, `Email` icon; `getDailyQuote()` renders a date-aware motivational quote below the subtitle — keyed by `MM-DD` for special dates (`01-01`, `05-05`, `05-17`, `12-24`), falls back to a per-weekday quote (mandag–søndag); 13px italic `var(--cds-text-secondary)`
+- `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 / Reupload bar appears when changes are detected; `Camera`, `Add`, `Renew` icons; 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); `PageHeading` hero; live search input on exercises tab with load-more (batches of 20 when >20 shown); Snarvei template carousel (capped at 6, "Se alle →" link to templates tab); search input on templates tab with load-more (batches of 12 when >12); exercise rows use `AccentChip` for primary muscles + Cond 700 name + 3px accent left strip; template cards use `borderRadius: var(--r-card)`; "Ny øvelse" button renders above Snarveier carousel to prevent tab-switch layout shift; exercise form via `ExerciseForm`
+- `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`
- `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)
@@ -181,7 +181,7 @@ week_plan_days
- SVG body uses `BODY_PATH` (bezier curves, viewBox `0 0 160 360`) — improved silhouette with curved shoulders, arms, waist and hips. Still simplified, not anatomically precise. `SHAPES` entries are either ellipses (`{ cx, cy, rx, ry }`) or SVG paths (`{ d }`); the render loop handles both. Key muscles with path shapes: `traps` (trapezoid with neck notch), `lats` (wing paths). `BodySVG` renders primary muscles as solid green glow, secondary as diagonal blue stripes (``).
- `useIsMobile(breakpoint=500)` — exported hook from `bodymap.jsx`. Below breakpoint: single body view with Front/Bak toggle. Above: side-by-side. Consumed via `BodyPanel` — do not use directly in page components.
- **Shared exercise row:** `app/src/components/ExerciseRow.jsx` — renders one editable exercise row (checkbox, inline name edit, sets/reps inputs, delete). Props: `exercise`, `onChange(updates)`, `onDelete()`, `layer` ("layer-01"/"layer-02"), `validateNumbers`, `autoFocusName`, `onNameBlur` (optional callback fired when the name input blurs — used by `ExerciseRowWithAutocomplete` to trigger muscle inference). The outer row div has no click handler — only the Checkbox toggles `enabled` (prevents accidental untick when editing fields). Used by `MuscleMap.jsx`, `History.jsx`, and `TemplateSessionEditor.jsx`.
-- **Planlegger:** `app/src/components/Planlegger.jsx` — weekly training planner view (issue #59). State: `weekOffset` (±week navigation), `assignments` (`{ [dow 1-7]: template | null }`), `templates`, `weekSessions` (logged sessions for the visible ISO week — issue #143), `pickerDow`, `confirmDelete`, `saving`, `saveError`, `hoveredMuscle`. Computed via `useMemo`: `monday`, `weekIso`, `weekLabel` (built inline with `Intl.DateTimeFormat` for the locale-aware month abbreviation + `t("planlegger.weekLabel", ...)`), `untrainedThisWeekIds` (muscle IDs not trained in any logged session for the visible ISO week — derived from `weekSessions` via `extractMuscles`; issue #143), `projectedExerciseMap` (union of all assigned templates' exercises via `buildMuscleMapFromExercises`), `sessionCount`, `muscleGroupCount`, `untrainedMuscleIds`, `showForslag` (≥2 untrained muscles), `forslagTemplates` (up to 3 templates from library covering untrained muscles). Layout: week nav chevrons → `PageHeading` → `SectionLabel "IKKE TRENT DENNE UKEN"` → wrap row of mono pill chips (History-style: `var(--r-pill)`, `var(--border-subtle-wl)`, `var(--text-muted-wl)`, `var(--cds-font-mono)` 11px) listing muscles not yet trained that week (or a single mono message when all 17 are trained) → `SectionLabel "PROJISERT DEKNING"` → projected `HeatmapBodySVG` (side-by-side/toggle) → fixed-height 48px hover-detail container (always rendered, prevents layout shift) → optional Forslag card → `SectionLabel "UKESPLAN"` → 7 × DayRow → inline `TemplatePicker` bottom-sheet overlay → `StickyCta` ("Fjern uke" ghost + "Lagre plan" primary) → confirm-delete strip. Persists via `fetchWeekPlan` / `saveWeekPlan` / `deleteWeekPlan` in `db.js`; loads logged sessions via `fetchSessionsForWeek` in parallel with the plan fetch. Duration (`N MIN`) omitted — `session_templates` has no duration column.
+- **Planlegger:** `app/src/components/Planlegger.jsx` — weekly training planner view (issue #59). State: `weekOffset` (±week navigation), `assignments` (`{ [dow 1-7]: template | null }`), `templates`, `weekSessions` (logged sessions for the visible ISO week — issue #143), `pickerDow`, `saving`, `saveError`, `hoveredMuscle`. Computed via `useMemo`: `monday`, `weekIso`, `weekLabel` (built inline with `Intl.DateTimeFormat` for the locale-aware month abbreviation + `t("planlegger.weekLabel", ...)`), `untrainedThisWeekIds` (muscle IDs not trained in any logged session for the visible ISO week — derived from `weekSessions` via `extractMuscles`; issue #143), `projectedExerciseMap` (union of all assigned templates' exercises via `buildMuscleMapFromExercises`), `sessionCount`, `muscleGroupCount`, `untrainedMuscleIds`, `showForslag` (≥2 untrained muscles), `forslagTemplates` (up to 3 templates from library covering untrained muscles). Layout: week nav chevrons → `PageHeading` → `SectionLabel "IKKE TRENT DENNE UKEN"` → wrap row of mono pill chips (History-style: `var(--r-pill)`, `var(--border-subtle-wl)`, `var(--text-muted-wl)`, `var(--cds-font-mono)` 11px) listing muscles not yet trained that week (or a single mono message when all 17 are trained) → `SectionLabel "PROJISERT DEKNING"` → projected `HeatmapBodySVG` (side-by-side/toggle) → fixed-height 48px hover-detail container (always rendered, prevents layout shift) → optional Forslag card → `SectionLabel "UKESPLAN"` → 7 × DayRow → inline `TemplatePicker` bottom-sheet overlay. No sticky save/delete bar — plan auto-saves on every add/remove; `deleteWeekPlan` is called automatically when all slots are cleared. Persists via `fetchWeekPlan` / `saveWeekPlan` / `deleteWeekPlan` in `db.js`; loads logged sessions via `fetchSessionsForWeek` in parallel with the plan fetch. Duration (`N MIN`) omitted — `session_templates` has no duration column.
- **Settings:** `app/src/components/Settings.jsx` — settings view reachable via the gear icon in the header (issue #123). Sections in order: (1) Språk — `RadioButtonGroup` for nb/en/fa; calls `i18n.changeLanguage()` + persists to `localStorage`; (2) Utseende — Carbon `Toggle` for dark/light theme with a live `BodyPanel` preview (fixed sample: primary `chest, quads, lats`; secondary `shoulders_front, hamstrings, triceps`); (3) Kontakt — feedback text + GitHub link; (4) Om appen — version number + "Vis endringslogg" opening `ChangelogModal`; (5) Konto — logged-in email (read-only) + danger logout button. `ChangelogModal` is no longer rendered in `PageShell` — it lives here exclusively.
- **BodyPanel:** `app/src/components/BodyPanel.jsx` — shared front/back body map. Manages its own `mobileView` toggle state internally. Props: `primary[]`, `secondary[]`, `muscleMap`, `marginBottom`. Replaces the duplicated mobile/desktop render pattern that previously existed in `MuscleMap`, `History`, and `TemplateSessionEditor`.
- **MusclePicker:** `app/src/components/MusclePicker.jsx` — interactive body map where clicking a muscle cycles off → primary → secondary → off. Props: `primary[]`, `secondary[]`, `onChange({ primary, secondary })`, `instanceId` (unique suffix to avoid SVG filter ID collisions). Used inside `ExerciseForm.jsx`.
diff --git a/app/public/locales/en/translation.json b/app/public/locales/en/translation.json
index 53a6b29..6cfc90a 100644
--- a/app/public/locales/en/translation.json
+++ b/app/public/locales/en/translation.json
@@ -193,8 +193,8 @@
},
"gymClassLabel": "Gym class",
"noClassSelected": "No class selected",
- "conflictWarningTitle": "Existing session:",
- "conflictWarningBody": "This class already has a saved session ({{date}}). Saving will replace it.",
+ "conflictWarningTitle": "Warning:",
+ "conflictWarningBody": "This class is already linked to a session from {{date}}. Saving will unlink that session — it keeps its data but loses the class connection.",
"duplicateGymSession": "This gym class already has a saved session.",
"reanalyzeServerError": "Server error ({{status}}): Invalid response from server",
"reanalyzeServerErrorDetail": "Server error ({{status}}): {{detail}}",
@@ -207,9 +207,8 @@
"sectionLabel": "LIBRARY",
"heading": "Your building blocks.",
"tabExercises": "Exercises",
- "tabTemplates": "Templates",
+ "tabTemplates": "My templates",
"newExercise": "New exercise",
- "shortcuts": "SHORTCUTS",
"searchPlaceholder": "Search exercise…",
"loadingExercises": "Loading exercises…",
"noExercises": "No exercises added yet.",
@@ -230,8 +229,7 @@
"exerciseRemovedWarning": "and will be removed from it.",
"exerciseCount": "{{count}} EX",
"searchTemplates": "Search templates…",
- "showMore": "Show {{count}} more",
- "seeAll": "See all"
+ "showMore": "Show {{count}} more"
},
"planlegger": {
"heading": "Plan the week",
diff --git a/app/public/locales/fa/translation.json b/app/public/locales/fa/translation.json
index aeeb8d7..7d9b641 100644
--- a/app/public/locales/fa/translation.json
+++ b/app/public/locales/fa/translation.json
@@ -193,8 +193,8 @@
},
"gymClassLabel": "کلاس ورزشی",
"noClassSelected": "کلاسی انتخاب نشده",
- "conflictWarningTitle": "جلسه موجود:",
- "conflictWarningBody": "این کلاس از قبل یک جلسه ذخیره شده دارد ({{date}}). ذخیره جدید جایگزین آن میشود.",
+ "conflictWarningTitle": "هشدار:",
+ "conflictWarningBody": "این کلاس از قبل به جلسهای از {{date}} متصل است. با ذخیره، اتصال آن جلسه حذف میشود — دادههایش حفظ میشود اما ارتباط با کلاس قطع میشود.",
"duplicateGymSession": "این کلاس از قبل یک جلسه ذخیره شده دارد.",
"reanalyzeServerError": "خطای سرور ({{status}}): پاسخ نامعتبر از سرور",
"reanalyzeServerErrorDetail": "خطای سرور ({{status}}): {{detail}}",
@@ -207,9 +207,8 @@
"sectionLabel": "کتابخانه",
"heading": "بلوکهای سازنده شما.",
"tabExercises": "تمرینها",
- "tabTemplates": "قالبها",
+ "tabTemplates": "قالبهای من",
"newExercise": "تمرین جدید",
- "shortcuts": "میانبرها",
"searchPlaceholder": "جستجوی تمرین…",
"loadingExercises": "در حال بارگذاری تمرینها…",
"noExercises": "هنوز تمرینی اضافه نشده.",
@@ -230,8 +229,7 @@
"exerciseRemovedWarning": "و از آن حذف خواهد شد.",
"exerciseCount": "{{count}} تمرین",
"searchTemplates": "جستجو در قالبها…",
- "showMore": "نمایش {{count}} بیشتر",
- "seeAll": "مشاهده همه"
+ "showMore": "نمایش {{count}} بیشتر"
},
"planlegger": {
"heading": "برنامهریزی هفته",
diff --git a/app/public/locales/nb/translation.json b/app/public/locales/nb/translation.json
index 3f5370a..637e374 100644
--- a/app/public/locales/nb/translation.json
+++ b/app/public/locales/nb/translation.json
@@ -193,8 +193,8 @@
},
"gymClassLabel": "Gymtime",
"noClassSelected": "Ingen time valgt",
- "conflictWarningTitle": "Eksisterende økt:",
- "conflictWarningBody": "Denne gymtimen har allerede en lagret økt ({{date}}). Lagring erstatter den.",
+ "conflictWarningTitle": "Advarsel:",
+ "conflictWarningBody": "Denne gymtimen er allerede koblet til en økt fra {{date}}. Lagring fjerner den koblingen — den andre øvelsen beholder sine data.",
"duplicateGymSession": "Denne gymtimen har allerede en økt lagret.",
"reanalyzeServerError": "Serverfeil ({{status}}): Ugyldig svar fra server",
"reanalyzeServerErrorDetail": "Serverfeil ({{status}}): {{detail}}",
@@ -207,9 +207,8 @@
"sectionLabel": "BIBLIOTEK",
"heading": "Dine byggeklosser.",
"tabExercises": "Øvelser",
- "tabTemplates": "Maler",
+ "tabTemplates": "Mine maler",
"newExercise": "Ny øvelse",
- "shortcuts": "SNARVEIER",
"searchPlaceholder": "Søk øvelse…",
"loadingExercises": "Laster øvelser…",
"noExercises": "Ingen øvelser lagt til ennå.",
@@ -230,8 +229,7 @@
"exerciseRemovedWarning": "og vil bli fjernet derfra.",
"exerciseCount": "{{count}} ØV",
"searchTemplates": "Søk i maler…",
- "showMore": "Vis {{count}} til",
- "seeAll": "Se alle"
+ "showMore": "Vis {{count}} til"
},
"planlegger": {
"heading": "Planlegg uken",
diff --git a/app/src/components/Bibliotek.jsx b/app/src/components/Bibliotek.jsx
index e08c099..f91529c 100644
--- a/app/src/components/Bibliotek.jsx
+++ b/app/src/components/Bibliotek.jsx
@@ -5,14 +5,13 @@ import {
} from "@carbon/react";
import { Add, TrashCan, Edit as EditIcon, ChevronRight, Search } from "@carbon/icons-react";
import { useTranslation } from "react-i18next";
-import { getIntlLocale } from "../lib/utils";
+import { logDevError } from "../lib/utils";
import PageShell, { SectionLabel, PageHeading, AccentChip } from "./PageShell";
import {
fetchLibraryExercises, saveLibraryExercise, updateLibraryExercise, deleteLibraryExercise,
fetchTemplates, saveTemplate, deleteTemplate, fetchTemplateNamesUsingExercise,
} from "../lib/db";
import { MUSCLES, BodySVG } from "../lib/bodymap.jsx";
-import { logDevError } from "../lib/utils";
import ExerciseForm from "./ExerciseForm";
export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
@@ -189,62 +188,6 @@ export default function Bibliotek({ onEditTemplate, initialTab = 0 }) {
)}
- {!showNewEx && (
- { setShowNewEx(true); setEditingEx(null); }}
- style={{ marginBottom: 16 }}>
- {t("bibliotek.newExercise")}
-
- )}
-
- {/* Shortcut carousel — template shortcuts */}
- {!tplLoading && templates.length > 0 && (
-
- )}
-
{/* Dirty state: error notifications + save bar */}
{isDirty && (
<>
@@ -797,10 +794,6 @@ export default function History({ initialDate }) {
)}
discardEdit(session)}>{t("common.discard")}
- { uploadingForSession.current = session; fileRef.current?.click(); }}>
- {isAnalyzing ? t("history.analyzing") : t("history.reuploadPhoto")}
- saveEdit(session)} style={{ marginLeft: "auto" }}>
{isSaving ? t("common.saving") : t("common.save")}
diff --git a/app/src/components/Login.jsx b/app/src/components/Login.jsx
index e05ea9e..1abdaa8 100644
--- a/app/src/components/Login.jsx
+++ b/app/src/components/Login.jsx
@@ -4,27 +4,23 @@ import { supabase } from "../lib/supabase";
import { Button, TextInput, InlineNotification } from "@carbon/react";
import { Email } from "@carbon/icons-react";
-// Daily quotes stay in Norwegian regardless of language setting.
function getDailyQuote() {
const now = new Date();
const mmdd = String(now.getMonth() + 1).padStart(2, "0") + "-" + String(now.getDate()).padStart(2, "0");
const special = {
- "01-01": "Nytt år, ny treningslogg. Dag 1 av 365.",
- "05-05": "05/05 – en dato som ser like bra ut baklengs. En god økt gjør det samme.",
- "05-17": "Gratulerer med dagen! 17. mai feires best med bein som allerede er slitne.",
- "12-24": "Julaften. Treningssalen er tom – det er din fordel.",
+ "01-01": "New year, new training log. Day 1 of 365.",
+ "12-24": "Christmas Eve. The gym is empty — that's your advantage.",
};
if (special[mmdd]) return special[mmdd];
- const weekday = now.getDay(); // 0=sun
return [
- "Søndag er ikke hviledag – det er oppladningsdag.",
- "Mandag: uken starter med deg.",
- "Tirsdag. Ikke mandag-angst, ikke fredags-latskap. Bare ren treningslyst.",
- "Onsdag – midtpunktet. Perfekt dag for et personlig rekord.",
- "Torsdag: en økt i dag og du går inn i helgen med samvittigheten i orden.",
- "Fredag! Siste sjanse til å gjøre uken komplett.",
- "Lørdag – de beste øktene skjer når ingen forventer det.",
- ][weekday];
+ "Sunday isn't a rest day — it's a recharge day.",
+ "Monday: the week starts with you.",
+ "Tuesday. No Monday dread, no Friday laziness. Just pure drive.",
+ "Wednesday — the midpoint. Perfect day for a personal best.",
+ "Thursday: one session today and you enter the weekend with a clear conscience.",
+ "Friday! Last chance to make the week complete.",
+ "Saturday — the best sessions happen when no one expects it.",
+ ][now.getDay()];
}
export default function Login() {
diff --git a/app/src/components/MuscleMap.jsx b/app/src/components/MuscleMap.jsx
index 708d840..c621e6a 100644
--- a/app/src/components/MuscleMap.jsx
+++ b/app/src/components/MuscleMap.jsx
@@ -427,16 +427,6 @@ export default function MuscleMap({ templatePreload, onTemplatePreloadConsumed }
From 4ecdc5c4eb798c6fc147f90443b240ffb82c98f5 Mon Sep 17 00:00:00 2001
From: Christopher Rotnes
Date: Thu, 7 May 2026 03:03:10 +0200
Subject: [PATCH 10/10] feat: restrict Claude recommendations to gym-hall
equipment (no machines)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Both buildRecommendPrompt and buildPeriodRecommendPrompt now tell Claude the
session takes place in a group gym hall with only free weights, bars, dumbbells,
mats, yoga blocks and resistance bands — no weight machines.
Co-Authored-By: Claude Sonnet 4.6
---
app/src/lib/prompts.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/app/src/lib/prompts.js b/app/src/lib/prompts.js
index a36af92..5a76d4b 100644
--- a/app/src/lib/prompts.js
+++ b/app/src/lib/prompts.js
@@ -29,6 +29,7 @@ export const buildRecommendPrompt = (trained, untrained, lang = 'nb') => {
return `Du er en personlig trener. Brukeren har trent disse musklene i dag: ${trained.join(", ")}.
Muskelgrupper som IKKE er trent: ${untrained.join(", ")}.
Foreslå 5 øvelser som dekker de utrente musklene. Gjerne øvelser som er vanlige på norske treningssentre.
+Treningslokalet er en stor gymsal uten treningmaskiner. Tilgjengelig utstyr: frivekter, vektstenger, manualer, matter, yogablokker, strikk/resistance bands. Foreslå KUN øvelser som passer dette utstyret.
Bruk KUN disse muscle-ID-ene: ${MUSCLE_IDS}.
Returner KUN et JSON-array, ingen annen tekst, ingen backticks:
[{"name":"Øvelsesnavn","primary":["muscle_id"],"secondary":["muscle_id"],"tip":"${tipInstruction}"}]`;
@@ -49,6 +50,7 @@ export const buildPeriodRecommendPrompt = (periodDays, sessionCount, trainedLabe
Trent (primær): ${trainedLabels || "ingen"}.
Ikke trent: ${untrainedLabels || "alle muskelgrupper er dekket"}.
Foreslå 5 øvelser som prioriterer de utrente musklene. Gjerne øvelser som er vanlige på norske treningssentre.
+Treningslokalet er en stor gymsal uten treningmaskiner. Tilgjengelig utstyr: frivekter, vektstenger, manualer, matter, yogablokker, strikk/resistance bands. Foreslå KUN øvelser som passer dette utstyret.
Bruk KUN disse muscle-ID-ene: ${MUSCLE_IDS}.
Returner KUN et JSON-array, ingen annen tekst, ingen backticks:
[{"name":"Øvelsesnavn","primary":["muscle_id"],"secondary":["muscle_id"],"tip":"${tipInstruction}"}]`;