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
21 changes: 13 additions & 8 deletions docs/product-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -670,13 +670,14 @@ adjustments creates an immutable new plan version.
**so that** the system has a structured record of my program to tune.

**Behavior:**
- Route: `POST /api/workout-plans` with `{ text }` body
- Frontend: "New Plan" button in page header opens a modal Dialog containing `PlanEditor` textarea
- Route: `POST /api/workout-plans` with `{ rawText }` body
- `plan-parser.ts` uses regex heuristics to extract days, exercises, sets × reps, target load, RPE, and notes; falls back to a single "Notes" day when structure isn't recognized
- Persisted as `plan_versions` row with `source = 'user'`, `version_number = 1`
- Input capped at 50 000 chars (HTTP 413 if exceeded); unstructured fallback blob capped at 10 000 chars
- `workout_plans` unique constraint enforces one plan per user in v1
- `GET /api/workout-plans/current` returns the active plan with version data
- `PUT /api/workout-plans/:id` replaces the plan text and re-parses
- `PUT /api/workout-plans/:id` replaces the plan text and re-parses (creates new version)

**E2E Coverage:** `e2e/workout-plan-tuner.spec.ts`

Expand All @@ -687,12 +688,16 @@ adjustments creates an immutable new plan version.

**Behavior:**
- Route: `/plan`
- Day cards show: day name, exercise rows with sets × reps, target load/RPE, notes
- Collapsible version history panel lists all prior versions with source badge (`user` or `tuner`) and version number
- Empty state: paste prompt with textarea and submit button
- `PlanVersionHistory` component renders list-only (no per-version diff view in v1)

**E2E Coverage:** `e2e/workout-plan-tuner.spec.ts`
- Active version displayed as an expanded card with green ring indicator (`ring-2 ring-green-500/70`) and "Active" badge with CheckCircle2 icon
- Previous versions shown as collapsed cards below, each showing: version number, source badge (Manual edit / AI tuned / Imported), creation date, notes
- Clicking a collapsed version card expands it to reveal full day/exercise breakdown via `PlanDayCard` components
- Day cards show: day name, target muscles badges, exercise rows with sets × reps, target load/RPE, notes
- All versions fetched from `GET /api/workout-plans/:id/versions` via `usePlanVersions` hook
- Empty state: centered message with "Create your first plan" button that opens the create modal
- Plan creation is a modal Dialog triggered by "New Plan" button in the page header (matching Reports page pattern)
- Loading state: skeleton cards (consistent with other pages)

**E2E Coverage:** `e2e/workout-plan-tuner.spec.ts` — empty state, create modal flow, active indicator, version list, expand/collapse

### UC-PLAN-03: Optimize plan from weekly report

Expand Down
174 changes: 174 additions & 0 deletions e2e/fixtures/plan-versions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
{
"data": [
{
"id": "version-fixture-002",
"planId": "plan-fixture-001",
"versionNumber": 2,
"source": "tuner",
"parentVersionId": "version-fixture-001",
"data": {
"splitType": "PPL",
"progressionPersonality": "balanced",
"days": [
{
"name": "Push A",
"targetMuscles": ["chest", "front deltoid", "triceps"],
"exercises": [
{
"id": "ex-001",
"exerciseName": "Bench Press",
"orderInDay": 1,
"sets": [
{ "type": "warmup", "targetReps": 8, "targetWeightKg": 60 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 82.5 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 82.5 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 82.5 }
],
"progressionRule": "double",
"primaryMuscle": "chest",
"secondaryMuscles": ["front deltoid", "triceps"],
"pattern": "push",
"equipment": "barbell",
"sfrTier": "S"
}
]
},
{
"name": "Pull A",
"targetMuscles": ["back", "rear deltoid", "biceps"],
"exercises": [
{
"id": "ex-004",
"exerciseName": "Barbell Row",
"orderInDay": 1,
"sets": [
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 82.5 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 82.5 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 82.5 }
],
"progressionRule": "double",
"primaryMuscle": "back",
"secondaryMuscles": ["rear deltoid", "biceps"],
"pattern": "pull",
"equipment": "barbell",
"sfrTier": "S"
}
]
}
]
},
"createdAt": "2026-04-11T11:00:00.000Z",
"acceptedAt": "2026-04-11T11:00:00.000Z",
"notes": "AI tuned — 2 changes applied"
},
{
"id": "version-fixture-001",
"planId": "plan-fixture-001",
"versionNumber": 1,
"source": "user",
"parentVersionId": null,
"data": {
"splitType": "PPL",
"progressionPersonality": "balanced",
"days": [
{
"name": "Push A",
"targetMuscles": ["chest", "front deltoid", "triceps"],
"exercises": [
{
"id": "ex-001",
"exerciseName": "Bench Press",
"orderInDay": 1,
"sets": [
{ "type": "warmup", "targetReps": 8, "targetWeightKg": 60 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 80 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 80 },
{ "type": "normal", "targetReps": [5, 8], "targetWeightKg": 80 }
],
"progressionRule": "double",
"primaryMuscle": "chest",
"secondaryMuscles": ["front deltoid", "triceps"],
"pattern": "push",
"equipment": "barbell",
"sfrTier": "S"
},
{
"id": "ex-002",
"exerciseName": "Incline Dumbbell Press",
"orderInDay": 2,
"sets": [
{ "type": "normal", "targetReps": [8, 12], "targetWeightKg": 30 },
{ "type": "normal", "targetReps": [8, 12], "targetWeightKg": 30 },
{ "type": "normal", "targetReps": [8, 12], "targetWeightKg": 30 }
],
"progressionRule": "double",
"primaryMuscle": "chest",
"secondaryMuscles": ["front deltoid"],
"pattern": "push",
"equipment": "dumbbell",
"sfrTier": "A"
},
{
"id": "ex-003",
"exerciseName": "Overhead Press",
"orderInDay": 3,
"sets": [
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 50 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 50 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 50 }
],
"progressionRule": "linear",
"primaryMuscle": "front deltoid",
"secondaryMuscles": ["triceps"],
"pattern": "push",
"equipment": "barbell",
"sfrTier": "A"
}
]
},
{
"name": "Pull A",
"targetMuscles": ["back", "rear deltoid", "biceps"],
"exercises": [
{
"id": "ex-004",
"exerciseName": "Barbell Row",
"orderInDay": 1,
"sets": [
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 80 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 80 },
{ "type": "normal", "targetReps": [6, 10], "targetWeightKg": 80 }
],
"progressionRule": "double",
"primaryMuscle": "back",
"secondaryMuscles": ["rear deltoid", "biceps"],
"pattern": "pull",
"equipment": "barbell",
"sfrTier": "S"
},
{
"id": "ex-005",
"exerciseName": "Pull-ups",
"orderInDay": 2,
"sets": [
{ "type": "normal", "targetReps": [6, 10] },
{ "type": "normal", "targetReps": [6, 10] },
{ "type": "normal", "targetReps": [6, 10] }
],
"progressionRule": "double",
"primaryMuscle": "back",
"secondaryMuscles": ["biceps"],
"pattern": "pull",
"equipment": "bodyweight",
"sfrTier": "S"
}
]
}
]
},
"createdAt": "2026-04-11T09:00:00.000Z",
"acceptedAt": "2026-04-11T09:00:00.000Z",
"notes": "Parsed from user paste"
}
]
}
28 changes: 21 additions & 7 deletions e2e/pages/PlanPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@ export class PlanPagePOM {
readonly page: Page;
readonly heading: Locator;
readonly emptyState: Locator;
readonly newPlanButton: Locator;
readonly createFirstPlanButton: Locator;
readonly createDialog: Locator;
readonly pasteTextarea: Locator;
readonly submitButton: Locator;
readonly versionHistorySection: Locator;

constructor(page: Page) {
this.page = page;
this.heading = page.getByRole('heading', { name: 'Workout Plan' });
this.emptyState = page.getByText('Create your plan');
this.emptyState = page.getByText('No workout plan yet.');
this.newPlanButton = page.getByRole('button', { name: /new plan/i });
this.createFirstPlanButton = page.getByRole('button', { name: /create your first plan/i });
this.createDialog = page.getByRole('heading', { name: /create workout plan/i });
this.pasteTextarea = page.getByTestId('plan-textarea');
this.submitButton = page.getByRole('button', { name: /parse & save/i });
this.versionHistorySection = page.getByText(/version history/i);
}

async goto() {
await this.page.goto('/plan');
}

/** Submit a raw text plan via the paste editor. */
/** Submit a raw text plan via the create modal. */
async submitPlan(rawText: string) {
await this.pasteTextarea.fill(rawText);
await this.submitButton.click();
Expand All @@ -40,15 +44,25 @@ export class PlanPagePOM {
return this.page.getByText(/v\d+/i).first();
}

/** Returns the active version indicator badge. */
get activeIndicator() {
return this.page.getByTestId('active-indicator');
}

/** Returns a version card by version number. */
getVersionCard(versionNumber: number): Locator {
return this.page.getByTestId(`version-card-${versionNumber}`);
}

/** Returns a day card by day name (matches CardTitle div text). */
getDayCard(dayName: string): Locator {
return this.page.getByText(dayName, { exact: true }).first();
}

/** Expects N day cards to be visible. */
async expectDayCount(n: number) {
/** Expects N version cards to be visible. */
async expectVersionCount(n: number) {
const { expect } = await import('@playwright/test');
const cards = this.page.locator('[data-slot="card"]');
const cards = this.page.locator('[data-testid^="version-card-"]');
await expect(cards).toHaveCount(n);
}

Expand Down
Loading
Loading