Skip to content

feat: overlap detection is implemented client-side only in the drag-and-drop routine builder — concurrent API calls or direct POST requests can create overlapping tasks in MongoDB with no server-side conflict check #1672

Description

@divyanshim27

Summary

DailyForge's core differentiator is documented as:

"Automatically detects and prevents scheduling conflicts for the
same day"

The overlap detection logic lives in the React RoutineBuilder.jsx
component — it checks in-memory state before allowing a drop. However,
routineController.js (backend) does not validate for time slot
conflicts before saving to MongoDB.

Concrete Bypass Scenarios

Scenario 1 — Race condition:
Two browser tabs are open to the same routine. Both add tasks to the same
time slot at the same millisecond. Both pass the client-side check (both
see the slot as empty). Both POST to /api/routines/:id. Both succeed.
MongoDB now has two overlapping tasks.

Scenario 2 — Direct API call:

# Add "Study" 09:00–10:00 to Monday
curl -X POST http://localhost:5000/api/routines/abc123/tasks \
  -H "Authorization: Bearer <token>" \
  -d '{"day": "Monday", "startTime": "09:00", "endTime": "10:00", "taskId": "study"}'

# Add "Meeting" 09:30–10:30 to Monday — overlaps, but backend accepts it
curl -X POST http://localhost:5000/api/routines/abc123/tasks \
  -H "Authorization: Bearer <token>" \
  -d '{"day": "Monday", "startTime": "09:30", "endTime": "10:30", "taskId": "meeting"}'
# Result: Both saved. Conflict exists in MongoDB.

Proposed Fix

Add conflict detection in routineController.js:

// routineController.js — addTaskToRoutine handler
export const addTaskToRoutine = async (req, res) => {
  const { day, startTime, endTime, taskId } = req.body;
  const routine = await Routine.findById(req.params.id);

  // ✅ Server-side overlap detection
  const start = timeToMinutes(startTime);
  const end   = timeToMinutes(endTime);

  const conflict = routine.tasks.find(t =>
    t.day === day &&
    timeToMinutes(t.startTime) < end &&
    timeToMinutes(t.endTime) > start
  );

  if (conflict) {
    return res.status(409).json({
      success: false,
      message: `Time conflict: "${conflict.title}" occupies ${conflict.startTime}${conflict.endTime} on ${day}`,
    });
  }

  // Proceed with save...
};

function timeToMinutes(timeStr) {
  const [h, m] = timeStr.split(':').map(Number);
  return h * 60 + m;
}

Acceptance Criteria

  • routineController.js validates time overlap before saving any task
  • Conflict returns HTTP 409 with descriptive message naming the conflicting task
  • timeToMinutes() utility function covers HH:MM format
  • Unit test: POST overlapping task → 409; non-overlapping → 201

Labels: bug, backend, data-integrity, level: intermediate

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendIssues related to server-side, database logic or APIsbugSomething isn't workingfeatureImprove existing feature or add newfrontendIssues related to UI/UX

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions