Files
gravl/.planning/phases/02-flexible-sets/02-02-PLAN.md
T

8.3 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
02-flexible-sets 02 execute 2
02-01
backend/src/index.js
frontend/src/App.jsx
true
truths artifacts key_links
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
path provides contains
backend/src/index.js DELETE /api/logs endpoint accepting user_id, program_exercise_id, date, set_number DELETE.*workout_logs
path provides contains
frontend/src/App.jsx deleteLog function that calls DELETE /api/logs; passed to WorkoutPage as onDeleteSet deleteLog
from to via pattern
ExerciseCard handleDeleteSet App.jsx deleteLog onDeleteSet prop through WorkoutPage onDeleteSet
from to via pattern
App.jsx deleteLog DELETE /api/logs fetch with method DELETE method.*DELETE
from to via pattern
DELETE /api/logs workout_logs table DELETE FROM workout_logs WHERE user_id=$1 AND program_exercise_id=$2 AND date=$3 AND set_number=$4 DELETE FROM workout_logs
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.

<execution_context> @/home/intense/.claude/get-shit-done/workflows/execute-plan.md @/home/intense/.claude/get-shit-done/templates/summary.md </execution_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 Task 1: Add DELETE /api/logs endpoint to backend backend/src/index.js Add a DELETE endpoint that removes a specific set log row. Place it directly after the existing POST /api/logs route (around line 329).
// 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). 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). 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.

Task 2: Wire deleteLog through App.jsx and WorkoutPage to ExerciseCard frontend/src/App.jsx, frontend/src/pages/WorkoutPage.jsx **In App.jsx:**

Add a deleteLog function alongside the existing logSet function:

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:

<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:

function WorkoutPage({ day, week, logs, onLogSet, onDeleteSet, onBack, fetchProgression }) {

Pass onDeleteSet through to each ExerciseCard:

<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. 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 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.

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

<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>

After completion, create `.planning/phases/02-flexible-sets/02-02-SUMMARY.md`