diff --git a/.pm-checkpoint.json b/.pm-checkpoint.json index 35fe620..e23ca11 100644 --- a/.pm-checkpoint.json +++ b/.pm-checkpoint.json @@ -1,9 +1,12 @@ { - "lastRun": "2026-03-01T04:00:00+01:00", - "status": "completed", + "lastRun": "2026-03-01T08:44:00+01:00", + "status": "in_progress", "phase": "04-workout-modification", - "activeTask": "04-02-backend-api", + "activeTask": "04-03-frontend-workout-edit", "tasksCompleted": ["01-input-ux", "02-flexible-sets", "03-design-polish", "04-01-schema-migration", "04-02-backend-api"], "nextTask": "04-03-frontend-workout-edit", - "notes": "Backend API complete for custom workouts. Added 6 new endpoints + updated 3 log endpoints with source_type support. Next: Frontend edit UI." + "recoveryFrom": "2026-03-01T06:42:00+01:00", + "agentSession": "mild-reef", + "agentType": "claude-code", + "notes": "Frontend agent spawned for 04-03. Working on: Edit Workout button, Exercise picker modal, swap/add exercise flows, fork confirmation dialog. Session: mild-reef" } diff --git a/TODO.md b/TODO.md index e69de29..a6f8771 100644 --- a/TODO.md +++ b/TODO.md @@ -0,0 +1,67 @@ +# Gravl PM - Active Task Queue + +## Current: 04-03 Frontend - Workout Edit Mode +**Status:** IN PROGRESS (recovery from interruption) +**Agent:** Frontend (Claude Code) +**Directory:** /workspace/gravl/frontend + +### Tasks + +#### 1. Add "Edit Workout" Button +- Add edit button/icon on WorkoutSelectPage for program workouts +- Only show for workouts that are part of a program +- Button triggers edit mode/modal + +#### 2. Create ExercisePicker Modal/Component +- Modal for selecting exercises from the database +- Search/filter functionality +- Exercise list with categories +- Select exercise with click/tap +- Reuse existing exercise data from current workout flow + +#### 3. Implement Swap Exercise Flow +- On exercise row in edit mode, show swap button +- Open ExercisePicker modal +- Replace selected exercise in workout structure +- Maintain set/rep info where applicable + +#### 4. Implement Add Exercise Flow +- "Add Exercise" button at bottom of workout +- Open ExercisePicker modal +- Append new exercise to workout with default sets/reps +- Allow configuring sets/reps for new exercise + +#### 5. Fork Confirmation Dialog +- When user first modifies a program workout +- Explain: "This creates your personal version of this workout" +- Options: "Cancel", "Create My Version" +- Show only once per workout (set flag) + +#### 6. Save Custom Workout +- POST to /api/custom-workouts on first modification (creates fork) +- PUT to /api/custom-workouts/:id on subsequent changes +- Update local state to use custom_workout_id +- Mark workout as "custom" in UI + +### API Endpoints Available (from 04-02) +- POST /api/custom-workouts - Create custom workout from program +- PUT /api/custom-workouts/:id - Update exercises +- GET /api/custom-workouts/:id - Fetch with exercises +- GET /api/custom-workouts - List user's custom workouts + +### Database Schema (from 04-01) +- custom_workouts table with user_id, name, original_program_day_id +- custom_workout_exercises table with exercise_id, set_order, sets, reps + +### Success Criteria +- [ ] "Edit Workout" button visible on program workouts +- [ ] Exercise picker modal opens and shows exercises +- [ ] Can swap an exercise (replaces in workout) +- [ ] Can add new exercise (appends to workout) +- [ ] Fork confirmation shown on first edit +- [ ] Custom workout saves to backend +- [ ] Subsequent sessions use custom workout + +### Next After This +- 04-04: Visual distinction (custom vs program badges) +- 04-05: Reset to original program option diff --git a/frontend/EOF b/frontend/EOF new file mode 100644 index 0000000..e69de29 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3050ef2..9fbace7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,6 +13,7 @@ "react-router-dom": "^6.21.0" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", @@ -742,6 +743,22 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@remix-run/router": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", @@ -1481,6 +1498,53 @@ "dev": true, "license": "ISC" }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", diff --git a/frontend/package.json b/frontend/package.json index 5ad8420..83c5e54 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "react-router-dom": "^6.21.0" }, "devDependencies": { + "@playwright/test": "^1.58.2", "@types/react": "^18.2.43", "@types/react-dom": "^18.2.17", "@vitejs/plugin-react": "^4.2.1", diff --git a/frontend/playwright.config.js b/frontend/playwright.config.js new file mode 100644 index 0000000..a158b5f --- /dev/null +++ b/frontend/playwright.config.js @@ -0,0 +1,12 @@ +module.exports = { + testDir: "./tests", + use: { + baseURL: process.env.STAGING_URL || "https://gravl.homelab.local", + headless: true, + screenshot: "only-on-failure", + }, + projects: [{ + name: "chromium", + use: { browserName: "chromium" } + }] +}; diff --git a/frontend/tests/gravl.spec.js b/frontend/tests/gravl.spec.js new file mode 100644 index 0000000..24da1ae --- /dev/null +++ b/frontend/tests/gravl.spec.js @@ -0,0 +1,17 @@ +const { test, expect } = require("@playwright/test"); + +test("login page loads", async ({ page }) => { + await page.goto("/login"); + await expect(page.locator("form")).toBeVisible(); +}); + +test("logo exists", async ({ page }) => { + await page.goto("/login"); + const logo = await page.locator("svg, img[class*=logo], .logo").first(); + await expect(logo).toBeVisible(); +}); + +test("dashboard loads", async ({ page }) => { + await page.goto("/"); + await expect(page).toHaveTitle(/Gravl/); +}); diff --git a/tests/example.spec.js b/tests/example.spec.js new file mode 100644 index 0000000..e69de29