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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to Workout Lens are documented here.

## [1.5.6] — 2026-05-15

### Developer / Infrastructure
- **CI/CD pipeline overhaul (issues #218–#225)** — workflow files renamed to `ci.yml`, `cleanup-staging.yml`, and new `audit.yml`; main pipeline split into a `validate` job (lint + test) and a `deploy` job (`needs: validate`) so lint/test failures give instant feedback as a separate status check without waiting for a build; concurrency group added to cancel stale in-progress runs on the same branch; `timeout-minutes: 15` on all jobs; Vite cache key corrected from `app/src/**` to `app/package-lock.json` (dep cache is not affected by source changes); `output_location: "."` annotated with a comment pointing to CLAUDE.md pitfall #9; `lfs: false` removed (default); `npm audit` moved out of the PR gate into a weekly `audit.yml` schedule (`--omit=dev --audit-level=high --audit-level=high`); production master deploys tagged with `environment: production`, PR and dev-branch deploys tagged with `environment: preview`.
- **`normalizeName` extracted to `sportyUtils.js`** — the quote-stripping function was inline in `sportySync.js` (which imports from `@azure/functions`), making it untestable from the `app/` Vitest runner. Extracted to `app/api/sportyUtils.js` with no Azure SDK dependency; 5 unit tests added in `app/api/__tests__/sportySync.test.js`.
- **Stale test description fixed** — `claudeUtils.test.js` described the model allowlist as "two production model IDs" when only one exists; corrected.

## [1.5.5] — 2026-05-15

### Changed
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ recommendation_cache
- Supabase Auth uses magic links (`emailRedirectTo: window.location.origin`)
- Anthropic API calls go through `app/api/claude.js` — Azure Function v4 model, browser hits `/api/claude`
- **Azure Functions entry point:** `app/api/index.js` imports all function files (`claude.js`, `sportySync.js`, `recsCacheCleanup.js`). `package.json#main` points to `index.js`. Azure Functions v4 only loads the single file referenced in `main` — add new function files here or they will never be registered. **The API `package.json` only has `@azure/functions` as a dependency — use raw `fetch` to the Supabase REST API (as `sportySync.js` does), never `import { createClient } from '@supabase/supabase-js'` in API files.**
- **Sporty.no sync:** `app/api/sportySync.js` — timer trigger at 04:00, 11:00, and 14:00 UTC upserts today's sessions from `https://sporty.no/api/v1/businessunits/8/groupactivities` into `gym_calendar` by `sporty_id`. Class names are passed through `normalizeName()` before storage, which strips any quoted annotation (e.g. `"SVART TRØYE"`) — handles both straight and curly double quotes generically. Business unit `8` is hardcoded — intentional for now (single-gym product); if extended to multiple gyms, this must become an env var or DB config. HTTP trigger `POST /api/sporty-sync` available for manual testing; accepts optional JSON body `{ "shiftDays": -7 }` to offset all timestamps by N days (useful for backfilling historical gym calendar data without re-running the live API). `GET /api/sporty-health` returns DB row counts (total rows, earliest/latest row timestamps, today's session count — no session list) — requires `x-api-key: <SPORTY_SYNC_API_KEY>` header (same key as the sync endpoint). Requires `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `SPORTY_SYNC_API_KEY` as Azure app settings (service role needed because the timer has no auth user). **The timer registration is guarded by `process.env.AZURE_FUNCTIONS_ENVIRONMENT === 'Production'`** — it is skipped entirely in local dev because the SWA CLI emulator only supports HTTP triggers.
- **Sporty.no sync:** `app/api/sportySync.js` — timer trigger at 04:00, 11:00, and 14:00 UTC upserts today's sessions from `https://sporty.no/api/v1/businessunits/8/groupactivities` into `gym_calendar` by `sporty_id`. Class names are passed through `normalizeName()` (imported from `sportyUtils.js`) before storage, which strips any quoted annotation (e.g. `"SVART TRØYE"`) — handles both straight and curly double quotes generically.
- **`app/api/sportyUtils.js`** — pure utility with no Azure SDK dependency. Exports `normalizeName(name)`. Kept separate from `sportySync.js` so it can be unit-tested without mocking the Azure Functions runtime. Business unit `8` is hardcoded — intentional for now (single-gym product); if extended to multiple gyms, this must become an env var or DB config. HTTP trigger `POST /api/sporty-sync` available for manual testing; accepts optional JSON body `{ "shiftDays": -7 }` to offset all timestamps by N days (useful for backfilling historical gym calendar data without re-running the live API). `GET /api/sporty-health` returns DB row counts (total rows, earliest/latest row timestamps, today's session count — no session list) — requires `x-api-key: <SPORTY_SYNC_API_KEY>` header (same key as the sync endpoint). Requires `SUPABASE_URL`, `SUPABASE_SERVICE_ROLE_KEY`, and `SPORTY_SYNC_API_KEY` as Azure app settings (service role needed because the timer has no auth user). **The timer registration is guarded by `process.env.AZURE_FUNCTIONS_ENVIRONMENT === 'Production'`** — it is skipped entirely in local dev because the SWA CLI emulator only supports HTTP triggers.
- **Rec cache cleanup:** `app/api/recsCacheCleanup.js` — timer trigger every Sunday at 03:00 UTC. Two DELETE passes: (1) entries older than 7 days (TTL — forces fresh recommendations; also catches version-orphaned entries that were never refreshed); (2) entries whose `cache_key` does not start with `v${RECS_PROMPT_VERSION}_` (sweeps orphans immediately after a prompt/model version bump). Uses raw `fetch` to Supabase REST API (no SDK). Requires `SUPABASE_URL` and `SUPABASE_SERVICE_ROLE_KEY`.
- **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.
Expand Down Expand Up @@ -366,6 +367,7 @@ Files and what they cover:
| File | Subject | Notes |
|---|---|---|
| `app/api/__tests__/claudeUtils.test.js` | model allowlist, `MAX_TOKENS_LIMIT`, `checkRateLimit` (incl. window expiry via `vi.useFakeTimers()`), `verifySupabaseJwt` | Covers all branches of the API guards. |
| `app/api/__tests__/sportySync.test.js` | `normalizeName` | Straight quotes, curly quotes, plain names, fully-annotated names, whitespace trimming. Imports from `sportyUtils.js` (no Azure SDK dep). |
| `app/src/lib/__tests__/bodymap.test.js` | `calcMuscles` | Explicit muscles, EX_DB fallback, dedup, miss path. |
| `app/src/lib/__tests__/muscleMapReducer.test.js` | `MuscleMap` reducer | One test per action type; reducer is the spine of the upload→confirm→muscles flow. |
| `app/src/lib/__tests__/prompts.test.js` | `ANALYZE_PROMPT`, `buildRecommendPrompt`, `buildPeriodRecommendPrompt`, `buildMuscleInferencePrompt` | Guards that every `MUSCLES` ID appears in every prompt — the prompt-vs-runtime mismatch is the easiest way to silently break Claude responses. |
Expand All @@ -380,6 +382,7 @@ Coverage (`npm run test:ci`) is configured in `vite.config.js` to scope to `src/

## Azure deploy notes
- **Supabase Auth redirect URLs** — add the following in Supabase → Authentication → URL Configuration → Additional redirect URLs: `http://localhost:4280` (local dev), your production URL + `<prod-url>/**` (prod), and `<your-swa-subdomain>-*.westeurope.7.azurestaticapps.net` (PR preview wildcard). The app uses `emailRedirectTo: window.location.origin` so no per-PR config is needed (#135)
- **GitHub Environments** — two environments must exist in GitHub repository Settings → Environments: `production` (used by master-branch deploys) and `preview` (used by PR and dev-branch deploys). Both use the same repository-level secrets — do NOT move secrets into environment-level secrets unless you introduce separate credentials per environment, because the Azure SWA token is shared between production and preview deploys.
- For secrets/settings/live URL, see README → Deployment section

## Code quality backlog (issue #213)
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ app/
index.js # Entry point — imports all Azure Functions
claude.js # Azure Function — proxies requests to Anthropic API
sportySync.js # Azure Function — timer (04:00+11:00 UTC) + HTTP trigger for sporty.no sync
sportyUtils.js # Pure utility — normalizeName() (no Azure SDK dep; unit-tested)
host.json # Azure Functions runtime config
package.json # API dependencies
staticwebapp.config.json # Azure SWA routing config
Expand All @@ -135,8 +136,8 @@ app/
| Branch | Purpose |
|---|---|
| `master` | Production — auto-deploys to Azure SWA on every push |
| `dev` | Staging — Azure SWA creates a preview URL on push |
| Feature branches | PR against `dev`; Azure SWA creates a preview per PR |
| `dev` | Pre-merge integration — deploys to the same production slot as `master`; use sparingly |
| Feature branches | PR against `master` or `dev`; Azure SWA creates an isolated preview URL per open PR |

## Issue format

Expand Down