const express = require('express'); const cors = require('cors'); const { Pool } = require('pg'); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const logger = require('./utils/logger'); const requestLoggerMiddleware = require('./middleware/requestLogger'); const { getHealthStatus, getUptime } = require('./utils/health'); const { createExerciseResearchRouter } = require('./routes/exerciseResearch'); const { createExerciseRecommendationRouter } = require('./routes/exerciseRecommendations'); const { searchExerciseResearch } = require('./services/exaSearch'); const app = express(); const PORT = process.env.PORT || 3001; const JWT_SECRET = process.env.JWT_SECRET || 'gravl-secret-key-change-in-production'; const pool = new Pool({ host: process.env.DB_HOST || 'postgres', port: process.env.DB_PORT || 5432, user: process.env.DB_USER || 'postgres', password: process.env.DB_PASSWORD || 'homelab_postgres_2026', database: process.env.DB_NAME || 'gravl' }); // Middleware setup app.use(cors()); app.use(express.json()); app.use(requestLoggerMiddleware); // Add request logging middleware app.use('/api/exercises', createExerciseResearchRouter({ pool, exaSearch: searchExerciseResearch })); app.use('/api/exercises', createExerciseRecommendationRouter()); const authMiddleware = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).json({ error: 'No token' }); try { req.user = jwt.verify(token, JWT_SECRET); next(); } catch { res.status(401).json({ error: 'Invalid token' }); } }; // Enhanced health endpoint with uptime and database status app.get('/api/health', async (req, res) => { try { const health = await getHealthStatus(pool); const statusCode = health.status === 'healthy' ? 200 : (health.status === 'degraded' ? 200 : 503); res.status(statusCode).json(health); } catch (err) { logger.error('Health check error', { error: err.message }); res.status(503).json({ status: 'unhealthy', uptime: getUptime(), timestamp: new Date().toISOString(), error: 'Health check failed' }); } }); app.post('/api/auth/register', async (req, res) => { try { const { email, password } = req.body; if (!email || !password) return res.status(400).json({ error: 'Email and password required' }); const hash = await bcrypt.hash(password, 10); const result = await pool.query( 'INSERT INTO users (email, password_hash) VALUES ($1, $2) RETURNING id, email', [email.toLowerCase(), hash] ); const token = jwt.sign({ id: result.rows[0].id, email: result.rows[0].email }, JWT_SECRET, { expiresIn: '30d' }); logger.info('User registered', { userId: result.rows[0].id, email: result.rows[0].email }); res.json({ token, user: result.rows[0] }); } catch (err) { if (err.code === '23505') { logger.warn('Registration failed - email exists', { email: req.body.email }); return res.status(400).json({ error: 'Email already exists' }); } logger.error('Register error', { error: err.message }); res.status(500).json({ error: 'Server error' }); } }); app.post('/api/auth/login', async (req, res) => { try { const { email, password } = req.body; const result = await pool.query('SELECT * FROM users WHERE email = $1', [email.toLowerCase()]); if (!result.rows.length) { logger.warn('Login failed - user not found', { email }); return res.status(401).json({ error: 'Invalid credentials' }); } const user = result.rows[0]; const valid = await bcrypt.compare(password, user.password_hash); if (!valid) { logger.warn('Login failed - invalid password', { userId: user.id }); return res.status(401).json({ error: 'Invalid credentials' }); } const token = jwt.sign({ id: user.id, email: user.email }, JWT_SECRET, { expiresIn: '30d' }); const { password_hash, ...safeUser } = user; logger.info('User logged in', { userId: user.id, email: user.email }); res.json({ token, user: safeUser }); } catch (err) { logger.error('Login error', { error: err.message }); res.status(500).json({ error: 'Server error' }); } }); app.get('/api/user/profile', authMiddleware, async (req, res) => { try { const userResult = await pool.query( 'SELECT id, email, gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete FROM users WHERE id = $1', [req.user.id] ); if (!userResult.rows.length) return res.status(404).json({ error: 'User not found' }); const user = userResult.rows[0]; // Get latest measurements const measResult = await pool.query( 'SELECT weight, neck_cm, waist_cm, hip_cm, body_fat_pct, measured_at FROM user_measurements WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 1', [req.user.id] ); // Get latest strength const strResult = await pool.query( 'SELECT bench_1rm, squat_1rm, deadlift_1rm, measured_at FROM user_strength WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 1', [req.user.id] ); res.json({ ...user, measurements: measResult.rows[0] || null, strength: strResult.rows[0] || null }); } catch (err) { logger.error('Profile error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); app.put('/api/user/profile', authMiddleware, async (req, res) => { try { const { gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete } = req.body; const num = v => (v === '' || v === undefined) ? null : v; const result = await pool.query( `UPDATE users SET gender=$1, age=$2, height_cm=$3, experience_level=$4, goal=$5, workouts_per_week=$6, onboarding_complete=$7 WHERE id=$8 RETURNING id, email, gender, age, height_cm, experience_level, goal, workouts_per_week, onboarding_complete`, [gender, num(age), num(height_cm), experience_level, goal, num(workouts_per_week), onboarding_complete, req.user.id] ); logger.info('User profile updated', { userId: req.user.id }); res.json(result.rows[0]); } catch (err) { logger.error('Update profile error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); // Add measurements app.post('/api/user/measurements', authMiddleware, async (req, res) => { try { const { weight, neck_cm, waist_cm, hip_cm, body_fat_pct } = req.body; const num = v => (v === '' || v === undefined) ? null : v; const result = await pool.query( `INSERT INTO user_measurements (user_id, weight, neck_cm, waist_cm, hip_cm, body_fat_pct) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *`, [req.user.id, num(weight), num(neck_cm), num(waist_cm), num(hip_cm), num(body_fat_pct)] ); logger.info('Measurements added', { userId: req.user.id }); res.json(result.rows[0]); } catch (err) { logger.error('Add measurements error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); // Get measurements history app.get('/api/user/measurements', authMiddleware, async (req, res) => { try { const result = await pool.query( 'SELECT * FROM user_measurements WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 100', [req.user.id] ); res.json(result.rows); } catch (err) { logger.error('Get measurements error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); // Add strength record app.post('/api/user/strength', authMiddleware, async (req, res) => { try { const { bench_1rm, squat_1rm, deadlift_1rm } = req.body; const num = v => (v === '' || v === undefined) ? null : v; const result = await pool.query( `INSERT INTO user_strength (user_id, bench_1rm, squat_1rm, deadlift_1rm) VALUES ($1, $2, $3, $4) RETURNING *`, [req.user.id, num(bench_1rm), num(squat_1rm), num(deadlift_1rm)] ); logger.info('Strength record added', { userId: req.user.id }); res.json(result.rows[0]); } catch (err) { logger.error('Add strength error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); // Get strength history app.get('/api/user/strength', authMiddleware, async (req, res) => { try { const result = await pool.query( 'SELECT * FROM user_strength WHERE user_id = $1 ORDER BY measured_at DESC LIMIT 100', [req.user.id] ); res.json(result.rows); } catch (err) { logger.error('Get strength error', { error: err.message, userId: req.user.id }); res.status(500).json({ error: 'Server error' }); } }); // Get all programs app.get('/api/programs', async (req, res) => { try { const result = await pool.query('SELECT * FROM programs ORDER BY id'); res.json(result.rows); } catch (err) { logger.error('Error fetching programs', { error: err.message }); res.status(500).json({ error: 'Database error' }); } }); // Get program details with days app.get('/api/programs/:id', async (req, res) => { try { const program = await pool.query('SELECT * FROM programs WHERE id = $1', [req.params.id]); if (program.rows.length === 0) { return res.status(404).json({ error: 'Program not found' }); } const days = await pool.query(` SELECT pd.*, json_agg(json_build_object( 'id', pe.id, 'exercise_id', e.id, 'name', e.name, 'muscle_group', e.muscle_group, 'sets', pe.sets, 'reps_min', pe.reps_min, 'reps_max', pe.reps_max, 'order', pe.order_num ) ORDER BY pe.order_num) as exercises FROM program_days pd LEFT JOIN program_exercises pe ON pd.id = pe.program_day_id LEFT JOIN exercises e ON pe.exercise_id = e.id WHERE pd.program_id = $1 GROUP BY pd.id ORDER BY pd.day_number `, [req.params.id]); res.json({ ...program.rows[0], days: days.rows }); } catch (err) { logger.error('Error fetching program', { error: err.message, programId: req.params.id }); res.status(500).json({ error: 'Database error' }); } }); // Get exercises for a specific day app.get('/api/days/:dayId/exercises', async (req, res) => { try { const result = await pool.query(` SELECT pe.id, pe.sets, pe.reps_min, pe.reps_max, pe.order_num, e.id as exercise_id, e.name, e.muscle_group, e.description FROM program_exercises pe JOIN exercises e ON pe.exercise_id = e.id WHERE pe.program_day_id = $1 ORDER BY pe.order_num `, [req.params.dayId]); res.json(result.rows); } catch (err) { logger.error('Error fetching exercises', { error: err.message, dayId: req.params.dayId }); res.status(500).json({ error: 'Database error' }); } }); // Get alternative exercises for a given exercise (same muscle group) app.get('/api/exercises/:id/alternatives', async (req, res) => { try { const exerciseResult = await pool.query( 'SELECT muscle_group FROM exercises WHERE id = $1', [req.params.id] ); if (!exerciseResult.rows.length) { return res.status(404).json({ error: 'Exercise not found' }); } const muscleGroup = exerciseResult.rows[0].muscle_group; const alternatives = await pool.query( `SELECT id, name, muscle_group, description FROM exercises WHERE muscle_group = $1 AND id <> $2 ORDER BY name`, [muscleGroup, req.params.id] ); res.json(alternatives.rows); } catch (err) { logger.error('Error fetching alternatives', { error: err.message, exerciseId: req.params.id }); res.status(500).json({ error: 'Database error' }); } }); // Get last workout for a specific exercise id app.get('/api/exercises/:id/last-workout', async (req, res) => { try { const { user_id } = req.query; const result = await pool.query(` WITH latest AS ( SELECT wl.date FROM workout_logs wl JOIN program_exercises pe ON wl.program_exercise_id = pe.id WHERE pe.exercise_id = $1 AND wl.user_id = $2 ORDER BY wl.date DESC LIMIT 1 ) SELECT wl.* FROM workout_logs wl JOIN program_exercises pe ON wl.program_exercise_id = pe.id JOIN latest l ON wl.date = l.date WHERE pe.exercise_id = $1 AND wl.user_id = $2 ORDER BY wl.set_number ASC `, [req.params.id, user_id || 1]); res.json(result.rows); } catch (err) { logger.error('Error fetching last workout for exercise', { error: err.message, exerciseId: req.params.id }); res.status(500).json({ error: 'Database error' }); } }); // Calculate suggested weight based on progression app.get('/api/progression/:programExerciseId', async (req, res) => { try { const { user_id } = req.query; // Get exercise details const exerciseInfo = await pool.query(` SELECT pe.*, e.name FROM program_exercises pe JOIN exercises e ON pe.exercise_id = e.id WHERE pe.id = $1 `, [req.params.programExerciseId]); if (exerciseInfo.rows.length === 0) { return res.status(404).json({ error: 'Exercise not found' }); } const exercise = exerciseInfo.rows[0]; // Get last workout logs for this exercise const lastLogs = await pool.query(` SELECT * FROM workout_logs WHERE program_exercise_id = $1 AND user_id = $2 AND completed = true ORDER BY date DESC, set_number ASC LIMIT $3 `, [req.params.programExerciseId, user_id || 1, exercise.sets]); if (lastLogs.rows.length === 0) { return res.json({ suggestedWeight: 20, // Starting weight reason: 'No previous data - start light' }); } const lastWeight = lastLogs.rows[0].weight; const allSetsHitMaxReps = lastLogs.rows.every(log => log.reps >= exercise.reps_max); if (allSetsHitMaxReps) { // Progress: increase weight by 2.5kg return res.json({ suggestedWeight: lastWeight + 2.5, reason: `Hit ${exercise.reps_max} reps on all sets - increase weight!` }); } return res.json({ suggestedWeight: lastWeight, reason: 'Keep same weight until you hit max reps on all sets' }); } catch (err) { logger.error('Error calculating progression', { error: err.message, programExerciseId: req.params.programExerciseId }); res.status(500).json({ error: 'Database error' }); } }); // Get today's workout based on program day cycle app.get('/api/today/:programId', async (req, res) => { try { const { week } = req.query; const currentWeek = week || 1; // Get program days const days = await pool.query(` SELECT pd.*, json_agg(json_build_object( 'id', pe.id, 'exercise_id', e.id, 'name', e.name, 'muscle_group', e.muscle_group, 'sets', pe.sets, 'reps_min', pe.reps_min, 'reps_max', pe.reps_max, 'order', pe.order_num ) ORDER BY pe.order_num) as exercises FROM program_days pd LEFT JOIN program_exercises pe ON pd.id = pe.program_day_id LEFT JOIN exercises e ON pe.exercise_id = e.id WHERE pd.program_id = $1 GROUP BY pd.id ORDER BY pd.day_number `, [req.params.programId]); res.json({ week: parseInt(currentWeek), days: days.rows }); } catch (err) { logger.error('Error fetching today workout', { error: err.message, programId: req.params.programId }); res.status(500).json({ error: 'Database error' }); } }); if (require.main === module) { app.listen(PORT, '0.0.0.0', () => { logger.info(`Gravl API started`, { port: PORT, environment: process.env.NODE_ENV || 'development' }); }); } // ============================================ // 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) { logger.error('Error fetching exercises', { error: err.message }); 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'); logger.info('Custom workout created', { userId: user_id, workoutId: customWorkout.id }); res.json({ ...customWorkout, exercises: exercisesResult.rows }); } catch (err) { await client.query('ROLLBACK'); logger.error('Error creating custom workout', { error: err.message, userId: req.user.id }); 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) { logger.error('Error fetching custom workouts', { error: err.message, userId: req.user.id }); 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) { logger.error('Error fetching custom workout', { error: err.message, userId: req.user.id, workoutId: req.params.id }); 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'); logger.info('Custom workout updated', { userId: user_id, workoutId: workout_id }); // 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'); logger.error('Error updating custom workout', { error: err.message, userId: req.user.id, workoutId: req.params.id }); 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' }); } logger.info('Custom workout deleted', { userId: user_id, workoutId: workout_id }); res.json({ deleted: result.rows[0].id }); } catch (err) { logger.error('Error deleting custom workout', { error: err.message, userId: req.user.id, workoutId: req.params.id }); 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) { logger.error('Error fetching logs', { error: err.message }); 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] ); } logger.debug('Workout set logged', { userId: user_id, exerciseId: exerciseRef, weight, reps }); res.json(result.rows[0]); } catch (err) { logger.error('Error logging set', { error: err.message }); 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' }); } logger.info('Workout log deleted', { userId: user_id, date, setNumber: set_number }); res.json({ deleted: result.rows[0].id }); } catch (err) { logger.error('Error deleting log', { error: err.message }); res.status(500).json({ error: 'Database error' }); } }); module.exports = app;