221 lines
8.3 KiB
Markdown
221 lines
8.3 KiB
Markdown
---
|
|
phase: 02-flexible-sets
|
|
plan: "02"
|
|
type: execute
|
|
wave: 2
|
|
depends_on: ["02-01"]
|
|
files_modified:
|
|
- backend/src/index.js
|
|
- frontend/src/App.jsx
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Deleting a set row that was previously logged removes it from the database"
|
|
- "Adding and logging sets beyond the original program count persists to the database"
|
|
- "After saving/refreshing, the dynamic set count reflects what was logged (no phantom sets, no missing sets)"
|
|
- "The backend DELETE endpoint returns 200 on success and 404 if the log row did not exist"
|
|
artifacts:
|
|
- path: "backend/src/index.js"
|
|
provides: "DELETE /api/logs endpoint accepting user_id, program_exercise_id, date, set_number"
|
|
contains: "DELETE.*workout_logs"
|
|
- path: "frontend/src/App.jsx"
|
|
provides: "deleteLog function that calls DELETE /api/logs; passed to WorkoutPage as onDeleteSet"
|
|
contains: "deleteLog"
|
|
key_links:
|
|
- from: "ExerciseCard handleDeleteSet"
|
|
to: "App.jsx deleteLog"
|
|
via: "onDeleteSet prop through WorkoutPage"
|
|
pattern: "onDeleteSet"
|
|
- from: "App.jsx deleteLog"
|
|
to: "DELETE /api/logs"
|
|
via: "fetch with method DELETE"
|
|
pattern: "method.*DELETE"
|
|
- from: "DELETE /api/logs"
|
|
to: "workout_logs table"
|
|
via: "DELETE FROM workout_logs WHERE user_id=$1 AND program_exercise_id=$2 AND date=$3 AND set_number=$4"
|
|
pattern: "DELETE FROM workout_logs"
|
|
---
|
|
|
|
<objective>
|
|
Add a DELETE /api/logs backend endpoint and wire it through App.jsx so that when a user removes a set row that was already logged, the database record is deleted.
|
|
|
|
Purpose: Without this, deleted set rows leave orphan rows in workout_logs, causing ghost sets to reappear on next load. The frontend dynamic state from plan 01 is correct per-session, but only persists with proper backend deletion.
|
|
|
|
Output: Backend DELETE endpoint + App.jsx deleteLog function + WorkoutPage onDeleteSet prop wired to ExerciseCard.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@/home/intense/.claude/get-shit-done/workflows/execute-plan.md
|
|
@/home/intense/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/phases/02-flexible-sets/02-CONTEXT.md
|
|
@.planning/phases/02-flexible-sets/02-01-SUMMARY.md
|
|
@backend/src/index.js
|
|
@frontend/src/App.jsx
|
|
@frontend/src/pages/WorkoutPage.jsx
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add DELETE /api/logs endpoint to backend</name>
|
|
<files>backend/src/index.js</files>
|
|
<action>
|
|
Add a DELETE endpoint that removes a specific set log row. Place it directly after the existing POST /api/logs route (around line 329).
|
|
|
|
```js
|
|
// 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' });
|
|
}
|
|
});
|
|
```
|
|
|
|
No auth middleware on this endpoint — consistent with the existing POST /api/logs which also has no auth middleware (user_id comes from the request body). Do not add authMiddleware unless the existing POST /api/logs has it (it does not).
|
|
</action>
|
|
<verify>
|
|
Start backend (`npm start` in backend/) and run:
|
|
```
|
|
curl -X DELETE http://localhost:3001/api/logs \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"user_id":1,"program_exercise_id":1,"date":"2026-02-21","set_number":1}'
|
|
```
|
|
Returns `{"deleted": N}` if row existed, or `{"error":"Log not found"}` with 404 if not. Server does not crash on missing body fields (returns 404 or 500 gracefully).
|
|
</verify>
|
|
<done>
|
|
DELETE /api/logs endpoint exists, deletes the matching workout_logs row by composite key (user_id, program_exercise_id, date, set_number), returns 200+id on success, 404 if not found.
|
|
</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Wire deleteLog through App.jsx and WorkoutPage to ExerciseCard</name>
|
|
<files>frontend/src/App.jsx, frontend/src/pages/WorkoutPage.jsx</files>
|
|
<action>
|
|
**In App.jsx:**
|
|
|
|
Add a `deleteLog` function alongside the existing `logSet` function:
|
|
|
|
```js
|
|
const deleteLog = async (programExerciseId, setNumber) => {
|
|
try {
|
|
await fetch(`${API_URL}/logs`, {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
user_id: userId,
|
|
program_exercise_id: programExerciseId,
|
|
date: today,
|
|
set_number: setNumber
|
|
})
|
|
})
|
|
// Remove from local logs state
|
|
setLogs(prev => ({
|
|
...prev,
|
|
[programExerciseId]: (prev[programExerciseId] || []).filter(l => l.set_number !== setNumber)
|
|
}))
|
|
} catch (err) {
|
|
console.error('Failed to delete log:', err)
|
|
}
|
|
}
|
|
```
|
|
|
|
Pass `deleteLog` to `WorkoutPage` as `onDeleteSet`:
|
|
```jsx
|
|
<WorkoutPage
|
|
day={selectedDay}
|
|
week={currentWeek}
|
|
logs={logs}
|
|
onLogSet={logSet}
|
|
onDeleteSet={deleteLog}
|
|
onBack={() => setView('dashboard')}
|
|
fetchProgression={fetchProgression}
|
|
/>
|
|
```
|
|
|
|
**In WorkoutPage.jsx:**
|
|
|
|
Update the `WorkoutPage` function signature to accept `onDeleteSet`:
|
|
```js
|
|
function WorkoutPage({ day, week, logs, onLogSet, onDeleteSet, onBack, fetchProgression }) {
|
|
```
|
|
|
|
Pass `onDeleteSet` through to each `ExerciseCard`:
|
|
```jsx
|
|
<ExerciseCard
|
|
key={exercise.id || idx}
|
|
exercise={exercise}
|
|
logs={logs[exercise.id] || []}
|
|
progression={progressions[exercise.id]}
|
|
expanded={expandedExercise === exercise.id}
|
|
onToggle={() => setExpandedExercise(
|
|
expandedExercise === exercise.id ? null : exercise.id
|
|
)}
|
|
onLogSet={onLogSet}
|
|
onDeleteSet={onDeleteSet}
|
|
/>
|
|
```
|
|
|
|
The ExerciseCard already has `onDeleteSet` in its prop signature and calls it in `handleDeleteSet` from plan 01. No changes needed inside ExerciseCard itself — just confirm `onDeleteSet` is being passed through correctly.
|
|
|
|
**Behavior when delete is called:**
|
|
- If the set being deleted has `completed: true` in setList (was logged to DB), `deleteLog` fires and removes the DB row
|
|
- If the set is new and not yet logged (e.g. user added a set but didn't complete it), `onDeleteSet` is still called but the DELETE request will return 404 — the catch block silently ignores this (the row didn't exist; no harm done)
|
|
|
|
This means the `handleDeleteSet` in ExerciseCard should always call `onDeleteSet` regardless of whether the set was completed — the backend handles the "not found" case gracefully.
|
|
</action>
|
|
<verify>
|
|
In the dev server:
|
|
1. Start a workout, complete set 1 of an exercise (logs it to DB)
|
|
2. Verify it's logged: `curl "http://localhost:3001/api/logs?user_id=1&program_exercise_id=1&date=TODAY"`
|
|
3. Delete set 1 row using the trash icon
|
|
4. Verify it's gone from DB: repeat the curl — set_number=1 should no longer appear
|
|
5. Add a new set (Vanligt set), complete it — verify it appears in DB as the next set_number
|
|
6. Reload the workout — no ghost sets, count matches what was logged
|
|
</verify>
|
|
<done>
|
|
deleteLog in App.jsx calls DELETE /api/logs. WorkoutPage passes onDeleteSet to ExerciseCard. Deleting a completed set removes it from the database and from local logs state. Non-logged sets delete silently without error.
|
|
</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
Run `npm run build` in frontend/ — must pass with no errors.
|
|
|
|
Full flow test:
|
|
1. Open a workout
|
|
2. Add 2 extra sets to the first exercise (Vanligt set)
|
|
3. Complete all sets — verify they all persist in DB
|
|
4. Delete the middle set — verify DB row removed, UI renumbers
|
|
5. Save workout (navigate back to dashboard)
|
|
6. Re-open same workout — set count matches what was logged, no ghost rows
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
DELETE /api/logs endpoint exists in backend. App.jsx has deleteLog function. WorkoutPage passes onDeleteSet to ExerciseCard. Deleting a logged set removes it from DB. Adding and completing new sets saves beyond original program set count. Frontend build passes.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-flexible-sets/02-02-SUMMARY.md`
|
|
</output>
|