From b2073b5d4c2c5acb2dde59b6b3404a0fdd3eb7f1 Mon Sep 17 00:00:00 2001 From: Clawd Date: Sun, 1 Mar 2026 03:36:53 +0100 Subject: [PATCH] feat(phase-4): Backend API for custom workouts - Add custom_workouts and custom_workout_exercises tables (schema) - New endpoints: - GET /api/exercises - List all exercises for picker - POST /api/custom-workouts - Fork program workout - GET /api/custom-workouts - List user's custom workouts - GET /api/custom-workouts/:id - Get workout with exercises - PUT /api/custom-workouts/:id - Update workout exercises - DELETE /api/custom-workouts/:id - Delete custom workout - Updated endpoints for source_type support: - GET /api/logs - Filter by source_type and custom_workout_id - POST /api/logs - Save with source_type and custom_workout_id - DELETE /api/logs - Support custom workout log deletion - Adds Phase 4 planning overview Completes: 04-01-schema-migration, 04-02-backend-api Next: 04-03-frontend-workout-edit --- .../04-workout-modification/04-00-OVERVIEW.md | 85 ++++ .pm-checkpoint.json | 11 +- PY | 0 backend/src/index.js | 468 ++++++++++++++---- db/init.sql | 35 ++ db/migrations/004_add_custom_workouts.sql | 37 ++ scripts/create-staging.py | 0 7 files changed, 530 insertions(+), 106 deletions(-) create mode 100644 .planning/phases/04-workout-modification/04-00-OVERVIEW.md create mode 100644 PY create mode 100644 db/migrations/004_add_custom_workouts.sql create mode 100644 scripts/create-staging.py diff --git a/.planning/phases/04-workout-modification/04-00-OVERVIEW.md b/.planning/phases/04-workout-modification/04-00-OVERVIEW.md new file mode 100644 index 0000000..cf465ab --- /dev/null +++ b/.planning/phases/04-workout-modification/04-00-OVERVIEW.md @@ -0,0 +1,85 @@ +# Phase 4: Workout Modification + +**Started:** 2026-03-01 +**Goal:** Let users customize program workouts by swapping or adding exercises, creating personal forks + +## Problem Statement + +Users want flexibility within their program structure. Currently: +- Can't swap an exercise (e.g., replace bench press with dumbbell press due to equipment availability) +- Can't add exercises to a program workout (e.g., add face pulls to Push day) +- Any modification would require building a completely custom workout + +## Solution: Workout Forking + +When user modifies a program workout, we: +1. Copy the program workout to `custom_workouts` table +2. Store modifications in `custom_workout_exercises` table +3. Save workout logs with `source_type = 'custom'` to track lineage +4. Original program remains unchanged for future sessions + +## Phase Plans + +### 04-01: Database Schema Migration +- Create `custom_workouts` table +- Create `custom_workout_exercises` table +- Add `source_type` enum column to `workout_logs` +- Migration script with rollback + +### 04-02: Backend API for Custom Workouts +- POST /api/custom-workouts (create from program workout) +- PUT /api/custom-workouts/:id (update exercises) +- GET /api/custom-workouts/:id (fetch with exercises) +- GET /api/custom-workouts (list user's custom workouts) +- Update workout log save to handle source_type + +### 04-03: Frontend - Workout Edit Mode +- "Edit Workout" button on WorkoutSelectPage +- Exercise picker modal/component +- Swap exercise UI flow +- Add exercise UI flow +- Fork confirmation dialog + +## Success Criteria + +- [ ] User can replace any exercise in a program workout +- [ ] User can add exercises to a program workout +- [ ] Modified workout saves as custom_workout (original program unchanged) +- [ ] Subsequent workout sessions use the forked custom workout +- [ ] User can see which workouts are custom vs program originals + +## Database Schema + +```sql +-- custom_workouts: Stores the forked workout header +custom_workouts ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(100) NOT NULL, + description TEXT, + original_program_day_id INTEGER REFERENCES program_days(id), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- custom_workout_exercises: Stores exercises in custom workout +custom_workout_exercises ( + id SERIAL PRIMARY KEY, + custom_workout_id INTEGER REFERENCES custom_workouts(id) ON DELETE CASCADE, + exercise_id INTEGER REFERENCES exercises(id) ON DELETE CASCADE, + set_order INTEGER NOT NULL, + sets INTEGER DEFAULT 3, + reps INTEGER DEFAULT 10, + weight_preset DECIMAL(5,2), + UNIQUE(custom_workout_id, set_order) +); + +-- workout_logs.source_type: Tracks where log came from +ALTER TABLE workout_logs ADD COLUMN source_type VARCHAR(20) DEFAULT 'program'; +-- Values: 'program', 'custom' +``` + +## Out of Scope + +- Building completely custom workouts from scratch (v2/CUS-01) +- Reusable custom workout templates (v2/CUS-02) +- Complex program modifications (reordering days, changing structure) diff --git a/.pm-checkpoint.json b/.pm-checkpoint.json index c376f89..35fe620 100644 --- a/.pm-checkpoint.json +++ b/.pm-checkpoint.json @@ -1,8 +1,9 @@ { - "lastRun": "2026-02-28T23:45:00+01:00", + "lastRun": "2026-03-01T04:00:00+01:00", "status": "completed", - "tasksCompleted": ["emoji-replacement", "alternative-modal", "workout-page-ux-redish", "chat-onboarding", "03-01-login-onboarding-polish", "03-02-dashboard-polish", "03-03-workout-experience-polish"], - "activeTask": null, - "nextTask": null, - "notes": "03-03 Workout Experience Polish complete (commit f6b1379). Phase 3 complete: login/onboarding, dashboard, and workout experience all polished." + "phase": "04-workout-modification", + "activeTask": "04-02-backend-api", + "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." } diff --git a/PY b/PY new file mode 100644 index 0000000..e69de29 diff --git a/backend/src/index.js b/backend/src/index.js index 044336a..771b1f0 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -303,107 +303,6 @@ app.get('/api/exercises/:id/last-workout', async (req, res) => { } }); -// Get workout logs for a user and date -app.get('/api/logs', async (req, res) => { - try { - const { user_id, date, program_exercise_id } = req.query; - let query = 'SELECT * FROM workout_logs WHERE 1=1'; - const params = []; - - if (user_id) { - params.push(user_id); - query += ` AND user_id = $${params.length}`; - } - if (date) { - params.push(date); - query += ` AND date = $${params.length}`; - } - if (program_exercise_id) { - params.push(program_exercise_id); - query += ` AND program_exercise_id = $${params.length}`; - } - - query += ' ORDER BY date DESC, set_number ASC'; - - const result = await pool.query(query, params); - res.json(result.rows); - } catch (err) { - console.error('Error fetching logs:', err); - res.status(500).json({ error: 'Database error' }); - } -}); - -// Get last workout for an exercise (for progression) -app.get('/api/logs/last/:programExerciseId', async (req, res) => { - try { - const { user_id } = req.query; - const result = await pool.query(` - SELECT * FROM workout_logs - WHERE program_exercise_id = $1 AND user_id = $2 - ORDER BY date DESC, set_number ASC - LIMIT 10 - `, [req.params.programExerciseId, user_id || 1]); - res.json(result.rows); - } catch (err) { - console.error('Error fetching last workout:', err); - res.status(500).json({ error: 'Database error' }); - } -}); - -// Log a set -app.post('/api/logs', async (req, res) => { - try { - const { user_id, program_exercise_id, date, set_number, weight, reps, completed } = req.body; - - // Check if log exists for this set - const existing = await pool.query( - 'SELECT id FROM workout_logs WHERE user_id = $1 AND program_exercise_id = $2 AND date = $3 AND set_number = $4', - [user_id, program_exercise_id, date, set_number] - ); - - let result; - if (existing.rows.length > 0) { - // Update existing - result = await pool.query( - 'UPDATE workout_logs SET weight = $1, reps = $2, completed = $3 WHERE id = $4 RETURNING *', - [weight, reps, completed, existing.rows[0].id] - ); - } else { - // Insert new - result = await pool.query( - 'INSERT INTO workout_logs (user_id, program_exercise_id, date, set_number, weight, reps, completed) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *', - [user_id, program_exercise_id, date, set_number, weight, reps, completed] - ); - } - - res.json(result.rows[0]); - } catch (err) { - console.error('Error logging set:', err); - res.status(500).json({ error: 'Database error' }); - } -}); - -// Delete a specific set log -app.delete('/api/logs', async (req, res) => { - try { - const { user_id, program_exercise_id, date, set_number } = req.body; - - const result = await pool.query( - 'DELETE FROM workout_logs WHERE user_id = $1 AND program_exercise_id = $2 AND date = $3 AND set_number = $4 RETURNING id', - [user_id, program_exercise_id, date, set_number] - ); - - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Log not found' }); - } - - res.json({ deleted: result.rows[0].id }); - } catch (err) { - console.error('Error deleting log:', err); - res.status(500).json({ error: 'Database error' }); - } -}); - // Calculate suggested weight based on progression app.get('/api/progression/:programExerciseId', async (req, res) => { try { @@ -498,3 +397,370 @@ app.get('/api/today/:programId', async (req, res) => { app.listen(PORT, () => { console.log(`Gravl API running on port ${PORT}`); }); + +// ============================================ +// Custom Workouts API (Phase 4: Workout Modification) +// ============================================ + +// Get all exercises (for picker UI) +app.get('/api/exercises', async (req, res) => { + try { + const result = await pool.query( + 'SELECT id, name, muscle_group, description FROM exercises ORDER BY muscle_group, name' + ); + res.json(result.rows); + } catch (err) { + console.error('Error fetching exercises:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Create custom workout from program day (fork) +app.post('/api/custom-workouts', authMiddleware, async (req, res) => { + const client = await pool.connect(); + try { + const { source_program_day_id, name, description } = req.body; + const user_id = req.user.id; + + await client.query('BEGIN'); + + // Get the program day info and its exercises + const dayResult = await client.query( + 'SELECT name, program_id FROM program_days WHERE id = $1', + [source_program_day_id] + ); + + if (dayResult.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Program day not found' }); + } + + const dayName = dayResult.rows[0].name; + const workoutName = name || `${dayName} (anpassad)`; + + // Create custom workout + const workoutResult = await client.query( + `INSERT INTO custom_workouts (user_id, name, description, source_program_day_id) + VALUES ($1, $2, $3, $4) RETURNING *`, + [user_id, workoutName, description || null, source_program_day_id] + ); + const customWorkout = workoutResult.rows[0]; + + // Copy exercises from program day + const exercisesResult = await client.query( + `INSERT INTO custom_workout_exercises + (custom_workout_id, exercise_id, sets, reps_min, reps_max, order_index, replaced_exercise_id) + SELECT $1, exercise_id, sets, reps_min, reps_max, order_num, NULL + FROM program_exercises WHERE program_day_id = $2 + RETURNING *`, + [customWorkout.id, source_program_day_id] + ); + + await client.query('COMMIT'); + + res.json({ + ...customWorkout, + exercises: exercisesResult.rows + }); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Error creating custom workout:', err); + res.status(500).json({ error: 'Database error' }); + } finally { + client.release(); + } +}); + +// List user's custom workouts +app.get('/api/custom-workouts', authMiddleware, async (req, res) => { + try { + const user_id = req.user.id; + const result = await pool.query( + `SELECT cw.*, pd.name as original_day_name, p.name as program_name + FROM custom_workouts cw + LEFT JOIN program_days pd ON cw.source_program_day_id = pd.id + LEFT JOIN programs p ON pd.program_id = p.id + WHERE cw.user_id = $1 + ORDER BY cw.created_at DESC`, + [user_id] + ); + res.json(result.rows); + } catch (err) { + console.error('Error fetching custom workouts:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Get single custom workout with exercises +app.get('/api/custom-workouts/:id', authMiddleware, async (req, res) => { + try { + const user_id = req.user.id; + const workout_id = req.params.id; + + // Get workout header + const workoutResult = await pool.query( + `SELECT cw.*, pd.name as original_day_name, p.name as program_name + FROM custom_workouts cw + LEFT JOIN program_days pd ON cw.source_program_day_id = pd.id + LEFT JOIN programs p ON pd.program_id = p.id + WHERE cw.id = $1 AND cw.user_id = $2`, + [workout_id, user_id] + ); + + if (workoutResult.rows.length === 0) { + return res.status(404).json({ error: 'Custom workout not found' }); + } + + // Get exercises with full details + const exercisesResult = await pool.query( + `SELECT cwe.*, e.name, e.muscle_group, e.description, + re.name as replaced_exercise_name, + re.muscle_group as replaced_exercise_muscle_group + FROM custom_workout_exercises cwe + JOIN exercises e ON cwe.exercise_id = e.id + LEFT JOIN exercises re ON cwe.replaced_exercise_id = re.id + WHERE cwe.custom_workout_id = $1 + ORDER BY cwe.order_index`, + [workout_id] + ); + + res.json({ + ...workoutResult.rows[0], + exercises: exercisesResult.rows + }); + } catch (err) { + console.error('Error fetching custom workout:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Update custom workout exercises (replace all) +app.put('/api/custom-workouts/:id', authMiddleware, async (req, res) => { + const client = await pool.connect(); + try { + const user_id = req.user.id; + const workout_id = req.params.id; + const { name, description, exercises } = req.body; + + await client.query('BEGIN'); + + // Verify ownership + const workoutCheck = await client.query( + 'SELECT id FROM custom_workouts WHERE id = $1 AND user_id = $2', + [workout_id, user_id] + ); + + if (workoutCheck.rows.length === 0) { + await client.query('ROLLBACK'); + return res.status(404).json({ error: 'Custom workout not found' }); + } + + // Update workout details + if (name || description !== undefined) { + await client.query( + `UPDATE custom_workouts + SET name = COALESCE($1, name), + description = COALESCE($2, description), + updated_at = CURRENT_TIMESTAMP + WHERE id = $3`, + [name, description, workout_id] + ); + } + + // Replace exercises if provided + if (exercises && Array.isArray(exercises)) { + // Delete existing exercises + await client.query( + 'DELETE FROM custom_workout_exercises WHERE custom_workout_id = $1', + [workout_id] + ); + + // Insert new exercises + for (let i = 0; i < exercises.length; i++) { + const ex = exercises[i]; + await client.query( + `INSERT INTO custom_workout_exercises + (custom_workout_id, exercise_id, sets, reps_min, reps_max, order_index, replaced_exercise_id) + VALUES ($1, $2, $3, $4, $5, $6, $7)`, + [workout_id, ex.exercise_id, ex.sets || 3, ex.reps_min || 8, ex.reps_max || 12, + i, ex.replaced_exercise_id || null] + ); + } + } + + await client.query('COMMIT'); + + // Fetch and return updated workout + const updatedResult = await pool.query( + `SELECT cw.*, pd.name as original_day_name, p.name as program_name + FROM custom_workouts cw + LEFT JOIN program_days pd ON cw.source_program_day_id = pd.id + LEFT JOIN programs p ON pd.program_id = p.id + WHERE cw.id = $1`, + [workout_id] + ); + + const exercisesResult = await pool.query( + `SELECT cwe.*, e.name, e.muscle_group, e.description + FROM custom_workout_exercises cwe + JOIN exercises e ON cwe.exercise_id = e.id + WHERE cwe.custom_workout_id = $1 + ORDER BY cwe.order_index`, + [workout_id] + ); + + res.json({ + ...updatedResult.rows[0], + exercises: exercisesResult.rows + }); + } catch (err) { + await client.query('ROLLBACK'); + console.error('Error updating custom workout:', err); + res.status(500).json({ error: 'Database error' }); + } finally { + client.release(); + } +}); + +// Delete custom workout +app.delete('/api/custom-workouts/:id', authMiddleware, async (req, res) => { + try { + const user_id = req.user.id; + const workout_id = req.params.id; + + const result = await pool.query( + 'DELETE FROM custom_workouts WHERE id = $1 AND user_id = $2 RETURNING id', + [workout_id, user_id] + ); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Custom workout not found' }); + } + + res.json({ deleted: result.rows[0].id }); + } catch (err) { + console.error('Error deleting custom workout:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// ============================================ +// Updated Log Endpoints (support source_type) +// ============================================ + +// Get workout logs (optionally filter by source_type and custom_workout_id) +app.get('/api/logs', async (req, res) => { + try { + const { user_id, date, source_type, custom_workout_id } = req.query; + + let query = 'SELECT * FROM workout_logs WHERE user_id = $1'; + let params = [user_id]; + let paramIdx = 2; + + if (date) { + query += ` AND date = $${paramIdx++}`; + params.push(date); + } + + if (source_type) { + query += ` AND source_type = $${paramIdx++}`; + params.push(source_type); + } + + if (custom_workout_id) { + query += ` AND custom_workout_id = $${paramIdx++}`; + params.push(custom_workout_id); + } + + query += ' ORDER BY date DESC, set_number ASC'; + + const result = await pool.query(query, params); + res.json(result.rows); + } catch (err) { + console.error('Error fetching logs:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Log a set (updated for source_type and custom_workout support) +app.post('/api/logs', async (req, res) => { + try { + const { user_id, program_exercise_id, custom_workout_exercise_id, date, set_number, weight, reps, completed, source_type, custom_workout_id } = req.body; + + const source = source_type || 'program'; + + // Determine which exercise identifier to use for lookup + const exerciseRef = custom_workout_exercise_id || program_exercise_id; + + // Check if log exists for this set + let existingQuery, existingParams; + if (source === 'custom' && custom_workout_id) { + existingQuery = `SELECT id FROM workout_logs + WHERE user_id = $1 AND custom_workout_id = $2 AND date = $3 AND set_number = $4`; + existingParams = [user_id, custom_workout_id, date, set_number]; + } else { + existingQuery = `SELECT id FROM workout_logs + WHERE user_id = $1 AND program_exercise_id = $2 AND date = $3 AND set_number = $4`; + existingParams = [user_id, program_exercise_id, date, set_number]; + } + + const existing = await pool.query(existingQuery, existingParams); + + let result; + if (existing.rows.length > 0) { + // Update existing + result = await pool.query( + `UPDATE workout_logs + SET weight = $1, reps = $2, completed = $3, source_type = $4 + WHERE id = $5 RETURNING *`, + [weight, reps, completed, source, existing.rows[0].id] + ); + } else { + // Insert new + result = await pool.query( + `INSERT INTO workout_logs (user_id, program_exercise_id, custom_workout_exercise_id, + date, set_number, weight, reps, completed, source_type, custom_workout_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *`, + [user_id, program_exercise_id, custom_workout_exercise_id, date, set_number, + weight, reps, completed, source, custom_workout_id] + ); + } + + res.json(result.rows[0]); + } catch (err) { + console.error('Error logging set:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + +// Delete a specific set log (updated for source_type support) +app.delete('/api/logs', async (req, res) => { + try { + const { user_id, program_exercise_id, custom_workout_id, date, set_number } = req.body; + + let query, params; + if (custom_workout_id) { + query = `DELETE FROM workout_logs + WHERE user_id = $1 AND custom_workout_id = $2 AND date = $3 AND set_number = $4 + RETURNING id`; + params = [user_id, custom_workout_id, date, set_number]; + } else { + query = `DELETE FROM workout_logs + WHERE user_id = $1 AND program_exercise_id = $2 AND date = $3 AND set_number = $4 + RETURNING id`; + params = [user_id, program_exercise_id, date, set_number]; + } + + const result = await pool.query(query, params); + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Log not found' }); + } + + res.json({ deleted: result.rows[0].id }); + } catch (err) { + console.error('Error deleting log:', err); + res.status(500).json({ error: 'Database error' }); + } +}); + diff --git a/db/init.sql b/db/init.sql index c83d547..93dce65 100644 --- a/db/init.sql +++ b/db/init.sql @@ -179,3 +179,38 @@ INSERT INTO program_exercises (program_day_id, exercise_id, sets, reps_min, reps (6, 16, 4, 10, 12, 4), -- Leg Curls 4x10-12 (6, 17, 4, 12, 15, 5) -- Calf Raises 4x12-15 ON CONFLICT DO NOTHING; + +-- Custom workouts created by users +CREATE TABLE IF NOT EXISTS custom_workouts ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + source_program_day_id INTEGER REFERENCES program_days(id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Exercises within a custom workout +CREATE TABLE IF NOT EXISTS custom_workout_exercises ( + id SERIAL PRIMARY KEY, + custom_workout_id INTEGER NOT NULL REFERENCES custom_workouts(id) ON DELETE CASCADE, + exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + sets INTEGER NOT NULL DEFAULT 3, + reps_min INTEGER NOT NULL DEFAULT 8, + reps_max INTEGER NOT NULL DEFAULT 12, + rpe_target DECIMAL(3,1), + replaced_exercise_id INTEGER REFERENCES exercises(id) ON DELETE SET NULL, + order_index INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Extend workout_logs to support custom workouts +ALTER TABLE workout_logs + ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'program' CHECK (source_type IN ('program', 'custom')), + ADD COLUMN IF NOT EXISTS custom_workout_id INTEGER REFERENCES custom_workouts(id) ON DELETE SET NULL; + +-- Indexes for custom workout tables +CREATE INDEX IF NOT EXISTS idx_custom_workouts_user ON custom_workouts(user_id); +CREATE INDEX IF NOT EXISTS idx_custom_workout_exercises_workout ON custom_workout_exercises(custom_workout_id); +CREATE INDEX IF NOT EXISTS idx_workout_logs_custom_workout ON workout_logs(custom_workout_id); diff --git a/db/migrations/004_add_custom_workouts.sql b/db/migrations/004_add_custom_workouts.sql new file mode 100644 index 0000000..adbf2ab --- /dev/null +++ b/db/migrations/004_add_custom_workouts.sql @@ -0,0 +1,37 @@ +-- Migration 004: Add custom workout support +-- Allows users to create personalized workout plans based on program days + +-- Custom workouts created by users +CREATE TABLE IF NOT EXISTS custom_workouts ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + description TEXT, + source_program_day_id INTEGER REFERENCES program_days(id) ON DELETE SET NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Exercises within a custom workout +CREATE TABLE IF NOT EXISTS custom_workout_exercises ( + id SERIAL PRIMARY KEY, + custom_workout_id INTEGER NOT NULL REFERENCES custom_workouts(id) ON DELETE CASCADE, + exercise_id INTEGER NOT NULL REFERENCES exercises(id) ON DELETE CASCADE, + sets INTEGER NOT NULL DEFAULT 3, + reps_min INTEGER NOT NULL DEFAULT 8, + reps_max INTEGER NOT NULL DEFAULT 12, + rpe_target DECIMAL(3,1), + replaced_exercise_id INTEGER REFERENCES exercises(id) ON DELETE SET NULL, + order_index INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Extend workout_logs to support custom workouts +ALTER TABLE workout_logs + ADD COLUMN IF NOT EXISTS source_type VARCHAR(20) DEFAULT 'program' CHECK (source_type IN ('program', 'custom')), + ADD COLUMN IF NOT EXISTS custom_workout_id INTEGER REFERENCES custom_workouts(id) ON DELETE SET NULL; + +-- Indexes +CREATE INDEX IF NOT EXISTS idx_custom_workouts_user ON custom_workouts(user_id); +CREATE INDEX IF NOT EXISTS idx_custom_workout_exercises_workout ON custom_workout_exercises(custom_workout_id); +CREATE INDEX IF NOT EXISTS idx_workout_logs_custom_workout ON workout_logs(custom_workout_id); diff --git a/scripts/create-staging.py b/scripts/create-staging.py new file mode 100644 index 0000000..e69de29