Skip to content

Bug: Completing a program session out of order can strand the user on a non-existent "ghost week" #232

Description

@evan188199-tech

Bug

Completing a program session out of order can write a currentWeek that doesn't exist in the program, stranding the user on an empty "ghost week".

Reproduction

Program: 2 weeks (W1: 2 sessions, W2: 2 sessions). Start at W1/S1, then complete W2/S2 while W2/S1 (and earlier) are still incomplete (out-of-order completion, which the app otherwise supports).

In complete-program-session.action.ts and app/api/programs/session-progress/[progressId]/complete/route.ts:

let nextWeek = currentWeek;
let nextSession = currentSession + 1;
const currentWeekSessions = enrollment.program.weeks.find((w) => w.weekNumber === currentWeek)?.sessions.length || 0;
if (nextSession > currentWeekSessions) {
  nextWeek = currentWeek + 1;   // <- never clamped
  nextSession = 1;
}
const isCompleted = completedSessionsCount >= totalSessions;
// ...
data: { currentWeek: isCompleted ? currentWeek : nextWeek, ... }

The wrap increments the week without checking that currentWeek + 1 actually exists. For the repro above (currentWeek=2, currentSession=2, completed count 1 < total 4isCompleted=false):

{ isCompleted: false, nextWeek: 3, nextSession: 1 }   // program only has weeks 1–2!

The enrollment is then saved as currentWeek=3, currentSession=1. On the program page (program-detail-page.tsx), setSelectedWeek(progress.stats.currentWeek) selects week 3, which has no sessions — the user is stuck on an empty week with no path forward (the only escape is re-selecting a real week manually, and their "resume" pointer is permanently wrong until they finish enough sessions to flip isCompleted).

The same wrap logic is duplicated in two files, so both are affected.

Fix

Clamp the week increment to the program's last week so it can never point past the end:

const maxWeek = Math.max(...enrollment.program.weeks.map((w) => w.weekNumber));
if (nextSession > currentWeekSessions) {
  nextWeek = Math.min(currentWeek + 1, maxWeek);
  nextSession = 1;
}

If the program is actually done, isCompleted already guards the write. This just stops the pointer from running off the end during out-of-order completion. I have a PR ready (fixing both copies).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions