Files
gravl/backend/src/routes/workouts.js
T
clawd d81e403f01 Phase 06 Tier 1: Complete Backend Implementation - Recovery Tracking & Swap System
COMPLETED TASKS:
 06-01: Workout Swap System
   - Added swapped_from_id to workout_logs
   - Created workout_swaps table for history
   - POST /api/workouts/:id/swap endpoint
   - GET /api/workouts/available endpoint
   - Reversible swaps with audit trail

 06-02: Muscle Group Recovery Tracking
   - Created muscle_group_recovery table
   - Implemented calculateRecoveryScore() function
   - GET /api/recovery/muscle-groups endpoint
   - GET /api/recovery/most-recovered endpoint
   - Auto-tracking on workout log completion

 06-03: Smart Workout Recommendations
   - GET /api/recommendations/smart-workout endpoint
   - 7-day workout analysis algorithm
   - Recovery-based filtering (>30% threshold)
   - Top 3 recommendations with context
   - Context-aware reasoning messages

DATABASE CHANGES:
- Added 4 new tables: muscle_group_recovery, workout_swaps, custom_workouts, custom_workout_exercises
- Extended workout_logs with: swapped_from_id, source_type, custom_workout_id, custom_workout_exercise_id
- Created 7 new indexes for performance

IMPLEMENTATION:
- Recovery service with 4 core functions
- 2 new route handlers (recovery, smartRecommendations)
- Updated workouts router with swap endpoints
- Integrated recovery tracking into POST /api/logs
- Full error handling and logging

TESTING:
- Test file created: /backend/test/phase-06-tests.js
- Ready for E2E and staging validation

STATUS: Ready for frontend integration and production review
Branch: feature/06-phase-06
2026-03-06 20:54:03 +01:00

146 lines
4.8 KiB
JavaScript

const express = require('express');
const logger = require('../utils/logger');
const { updateMuscleGroupRecovery } = require('../services/recoveryService');
function createWorkoutRouter({ pool }) {
const router = express.Router();
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'No token provided' });
try {
const jwt = require('jsonwebtoken');
const JWT_SECRET = process.env.JWT_SECRET || 'gravl-secret-key-change-in-production';
req.user = jwt.verify(token, JWT_SECRET);
next();
} catch (err) {
res.status(401).json({ error: 'Invalid token' });
}
};
// POST /api/workouts/:id/swap - Swap a logged workout with another
router.post('/:id/swap', authMiddleware, async (req, res) => {
try {
const logId = parseInt(req.params.id);
const { newWorkoutId } = req.body;
const userId = req.user.id;
if (!logId || !newWorkoutId) {
return res.status(400).json({ error: 'Missing logId or newWorkoutId' });
}
// Verify the original log exists and belongs to this user
const originalLogResult = await pool.query(
'SELECT * FROM workout_logs WHERE id = $1 AND user_id = $2',
[logId, userId]
);
if (originalLogResult.rows.length === 0) {
return res.status(404).json({ error: 'Workout log not found' });
}
const originalLog = originalLogResult.rows[0];
// Verify the new exercise exists
const newExerciseResult = await pool.query(
'SELECT * FROM exercises WHERE id = $1',
[newWorkoutId]
);
if (newExerciseResult.rows.length === 0) {
return res.status(404).json({ error: 'New exercise not found' });
}
const newExercise = newExerciseResult.rows[0];
const client = await pool.connect();
try {
await client.query('BEGIN');
// Create new log with the swapped exercise
const newLogResult = await client.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, swapped_from_id)
VALUES ($1, NULL, NULL, $2, $3, $4, $5, $6, 'program', NULL, $7)
RETURNING *`,
[userId, originalLog.date, originalLog.set_number, originalLog.weight, originalLog.reps, originalLog.completed, logId]
);
const newLog = newLogResult.rows[0];
// Record the swap in workout_swaps table
await client.query(
`INSERT INTO workout_swaps (user_id, original_log_id, swapped_log_id, swap_date, created_at, updated_at)
VALUES ($1, $2, $3, $4, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
[userId, logId, newLog.id, originalLog.date]
);
// Update muscle group recovery for the new exercise
if (originalLog.completed) {
await updateMuscleGroupRecovery(pool, userId, newExercise.muscle_group, 0.8);
}
await client.query('COMMIT');
logger.info('Workout swapped', { userId, originalLogId: logId, newLogId: newLog.id });
res.json({
success: true,
message: 'Workout swapped successfully',
swap: {
originalLogId: logId,
newLogId: newLog.id,
newExercise: {
id: newExercise.id,
name: newExercise.name,
muscleGroup: newExercise.muscle_group
},
date: originalLog.date
}
});
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
} catch (err) {
logger.error('Error swapping workout', { error: err.message, userId: req.user.id });
res.status(500).json({ error: 'Database error' });
}
});
// GET /api/workouts/available - Get list of available exercises for swapping
router.get('/available', authMiddleware, async (req, res) => {
try {
const userId = req.user.id;
const { muscleGroup, limit = 10 } = req.query;
let query = 'SELECT * FROM exercises';
const params = [];
if (muscleGroup) {
query += ' WHERE muscle_group = $1';
params.push(muscleGroup);
}
query += ` ORDER BY muscle_group, name LIMIT ${Math.min(parseInt(limit), 100)}`;
const result = await pool.query(query, params);
res.json({
userId,
count: result.rows.length,
exercises: result.rows
});
} catch (err) {
logger.error('Error fetching available exercises', { error: err.message, userId: req.user.id });
res.status(500).json({ error: 'Database error' });
}
});
return router;
}
module.exports = { createWorkoutRouter };