diff --git a/.jules/bolt.md b/.jules/bolt.md index ba67a71..c425e3c 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -3,3 +3,7 @@ ## 2026-07-16 - [O(N*M) Nested Loop Lookups in Grading Engines] **Learning:** In interactive scenarios (such as Bid Elevator and STR Triage), grading engines frequently iterate over user decisions and match them against scenario properties (like keywords or search terms). Performing `array.find()` inside loop bodies or filter predicates results in costly O(N*M) lookups. **Action:** Convert arrays to `Map` lookups before entering loops/nested scans. Mapping keys once in O(M) time enables O(1) lookups during execution, transforming the time complexity of the grading logic to O(N + M). + +## 2026-07-20 - [Scoping Lesson Progress Queries to Visible Lessons] +**Learning:** When loading the user's lesson progress on general dashboard pages (e.g., Courses Catalog, Student Dashboard, or Certificate pending lists), querying `db.lessonProgress` without filtering `lessonId` causes the database to scan and return the entire student's progress history across all courses. As courses and lesson count scale, this results in bloated database payloads and high memory/CPU usage. +**Action:** Always extract the list of visible lesson IDs first and use `lessonId: { in: lessonIds }` to scope the database queries. This keeps query results lightweight and bounded. diff --git a/src/app/(dashboard)/courses/[courseSlug]/certificate/page.tsx b/src/app/(dashboard)/courses/[courseSlug]/certificate/page.tsx index 3640169..59e7827 100644 --- a/src/app/(dashboard)/courses/[courseSlug]/certificate/page.tsx +++ b/src/app/(dashboard)/courses/[courseSlug]/certificate/page.tsx @@ -194,13 +194,21 @@ async function PendingLessons({ take: 50, }); + const allLessonIds = allLessons.map((l) => l.id); const completedSet = new Set( - ( - await db.lessonProgress.findMany({ - where: { userId, status: 'COMPLETED', deletedAt: null }, - select: { lessonId: true }, - }) - ).map((p) => p.lessonId), + allLessonIds.length > 0 + ? ( + await db.lessonProgress.findMany({ + where: { + userId, + lessonId: { in: allLessonIds }, + status: 'COMPLETED', + deletedAt: null, + }, + select: { lessonId: true }, + }) + ).map((p) => p.lessonId) + : [], ); const pending = allLessons.filter((l) => !completedSet.has(l.id)).slice(0, 8); diff --git a/src/app/(dashboard)/courses/page.tsx b/src/app/(dashboard)/courses/page.tsx index 1d0c6c3..2d02c00 100644 --- a/src/app/(dashboard)/courses/page.tsx +++ b/src/app/(dashboard)/courses/page.tsx @@ -37,11 +37,19 @@ export default async function CoursesIndexPage() { }, }); - // Get user's lesson progress - const lessonProgress = await db.lessonProgress.findMany({ - where: { userId: user.id, deletedAt: null }, - select: { lessonId: true, status: true }, - }); + const allLessons = courses.flatMap((c) => c.modules.flatMap((m) => m.lessons)); + + // Get user's lesson progress scoped to course lessons + const lessonProgress = allLessons.length > 0 + ? await db.lessonProgress.findMany({ + where: { + userId: user.id, + lessonId: { in: allLessons.map((l) => l.id) }, + deletedAt: null, + }, + select: { lessonId: true, status: true }, + }) + : []; const progressMap = new Map(lessonProgress.map((p) => [p.lessonId, p.status])); return ( diff --git a/src/app/(dashboard)/dashboard/page.tsx b/src/app/(dashboard)/dashboard/page.tsx index 546298d..a91acae 100644 --- a/src/app/(dashboard)/dashboard/page.tsx +++ b/src/app/(dashboard)/dashboard/page.tsx @@ -30,16 +30,23 @@ export default async function DashboardPage() { }, }); - // Get user's lesson progress - const lessonProgress = await db.lessonProgress.findMany({ - where: { userId: user.id, deletedAt: null }, - select: { lessonId: true, status: true }, - }); - const progressMap = new Map(lessonProgress.map((p) => [p.lessonId, p.status])); - // Compute aggregate stats const allLessons = courses.flatMap((c) => c.modules.flatMap((m) => m.lessons)); const totalLessons = allLessons.length; + + // Get user's lesson progress (scoped to course lessons to prevent massive payloads) + const lessonProgress = allLessons.length > 0 + ? await db.lessonProgress.findMany({ + where: { + userId: user.id, + lessonId: { in: allLessons.map((l) => l.id) }, + deletedAt: null, + }, + select: { lessonId: true, status: true }, + }) + : []; + const progressMap = new Map(lessonProgress.map((p) => [p.lessonId, p.status])); + const completedLessons = allLessons.filter((l) => progressMap.get(l.id) === ProgressStatus.COMPLETED).length; const inProgressLessons = allLessons.filter((l) => progressMap.get(l.id) === ProgressStatus.IN_PROGRESS).length;