Add user personal learn time feature - #89
Conversation
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
📝 WalkthroughWalkthroughAdds per-user weekly learning-time windows, CRUD APIs and UI to manage them, integrates those windows into AI study-plan generation and normalization (producing a planningHint when constrained), and updates calendar-sync behavior so sessions sync only after plan acceptance. ChangesLearning Times Configuration and Plan Generation
🎯 4 (Complex) | ⏱️ ~45 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed. For unrecoverable errors, disable the tool in CodeRabbit configuration. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
convex/learningPlanAi.ts (2)
589-594: 💤 Low valueMinor: Duplicate validation of learning time window bounds.
Lines 592–594 validate that
startMinutesandendMinutesare non-null and thatendMinutes > startMinutes, then skip invalid windows. Lines 609–611 repeat the identical check inside the loop that processes those same windows. The second check is redundant because invalid windows were already filtered out by the first check.♻️ Proposed cleanup
Store only validated windows in the map:
for (const learningTime of learningTimes) { const startMinutes = parseTimeToMinutes(learningTime.startTime); const endMinutes = parseTimeToMinutes(learningTime.endTime); if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes) { continue; } const windows = windowsByDay.get(learningTime.dayOfWeek) ?? []; windows.push(learningTime); windowsByDay.set(learningTime.dayOfWeek, windows); } const slots: LearningSlot[] = []; for (let offset = availableDays; offset >= 1; offset -= 1) { const date = buildDateFromOffset(examDateKey, offset); const dayOfWeek = getBerlinDayOfWeek(date); const windows = windowsByDay.get(dayOfWeek) ?? []; for (const window of windows) { const startMinutes = parseTimeToMinutes(window.startTime); const endMinutes = parseTimeToMinutes(window.endTime); - if (startMinutes === null || endMinutes === null || endMinutes <= startMinutes) { - continue; - } slots.push({Also applies to: 606-611
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/learningPlanAi.ts` around lines 589 - 594, The loop currently validates learning window bounds twice; change it to first filter and store only validated windows (using parseTimeToMinutes and ensuring startMinutes !== null, endMinutes !== null and endMinutes > startMinutes) before the second processing loop so you can remove the duplicate check present later (the second endMinutes <= startMinutes/null check). Locate the for (const learningTime of learningTimes) block and its subsequent processing loop and move/keep only the validation in the initial collection step (e.g., building the map of windows) so the later loop can assume valid startMinutes and endMinutes and drop the redundant guard.
580-580: 💤 Low valueOptional:
nextStartMinutesfield is initialized but never mutated or read.The
LearningSlottype includes anextStartMinutesfield, initialized tostartMinutesat line 619, but the code never updates or consumes it. This suggests incomplete slot-consumption tracking logic.If the field was intended for future use (e.g., packing multiple sessions into a single window), consider removing it for now to avoid confusion, or implement the intended behavior.
Also applies to: 619-619
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/learningPlanAi.ts` at line 580, The LearningSlot type defines nextStartMinutes and initializes it to startMinutes (symbol: LearningSlot, field: nextStartMinutes, initializer: startMinutes) but the code never reads or updates it; either remove nextStartMinutes from the LearningSlot definition and all initializations/usages to avoid dead state, or implement the intended consumption logic: update nextStartMinutes whenever a slot is partially consumed (where slots are allocated/adjusted in functions handling slot assignment) and ensure all consumers read nextStartMinutes instead of startMinutes when determining the next available time. Make the change consistently across the LearningSlot construction and any slot-assignment code paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@convex/learningPlanAi.ts`:
- Line 744: The dateKey is being set using slot.date.toISOString(), producing a
full timestamp; replace that with the same date-only format used elsewhere by
calling formatDateKey(slot.date) (or the module's formatDateKey function) so
dateKey matches the date-only strings produced by formatDateKey(date) and aligns
with session comparisons and calendar logic; update any surrounding comments or
variable names if needed to reflect it's a date-only key.
- Line 515: The formatDateKey function currently uses
date.toISOString().slice(0,10) which returns a UTC date and causes off-by-one
day errors versus the Berlin-based logic (e.g., getBerlinDayOfWeek); change
formatDateKey to format the Date in the "Europe/Berlin" timezone (e.g., via
Intl.DateTimeFormat or toLocaleDateString with timeZone: 'Europe/Berlin') and
produce a YYYY-MM-DD string (ISO date) from that Berlin-localized result instead
of slicing toISOString().
In `@src/app/learning-times/edit.tsx`:
- Around line 97-100: The save flow enables when learningTimes is still
undefined causing hasChanges to be true and upsertMine to overwrite existing
slots; fix by treating the query-loading state as not-ready: use the result
metadata from useQuery(api.learningTimes.listMine, ...)—or check learningTimes
!== undefined / query.isLoading—before computing selectedEntry and hasChanges
and before enabling the Save button or calling upsertMine; update the logic in
the selectedEntry/hasChanges computation and the Save button handler
(references: learningTimes, selectedEntry, hasChanges, useQuery,
api.learningTimes.listMine, upsertMine) so save is disabled while the selected
day is still loading (apply same guard to the other similar block at lines
~136-146).
In `@src/app/learning-times/index.tsx`:
- Around line 198-204: The FAB currently always renders and calls
openEditor(firstMissingDay) where firstMissingDay falls back to 1, causing
accidental overwrites; update the rendering logic around the Button in the
LearningTimes component so it is hidden or disabled when all seven days are
already configured: detect "allConfigured" by checking the weekday configuration
array (the same logic that computes firstMissingDay), and if true either don't
render the Button or render it with disabled={true} and an updated
accessibilityLabel; ensure openEditor is only invoked when a valid
firstMissingDay exists (e.g., guard on firstMissingDay !== null/undefined before
calling openEditor).
---
Nitpick comments:
In `@convex/learningPlanAi.ts`:
- Around line 589-594: The loop currently validates learning window bounds
twice; change it to first filter and store only validated windows (using
parseTimeToMinutes and ensuring startMinutes !== null, endMinutes !== null and
endMinutes > startMinutes) before the second processing loop so you can remove
the duplicate check present later (the second endMinutes <= startMinutes/null
check). Locate the for (const learningTime of learningTimes) block and its
subsequent processing loop and move/keep only the validation in the initial
collection step (e.g., building the map of windows) so the later loop can assume
valid startMinutes and endMinutes and drop the redundant guard.
- Line 580: The LearningSlot type defines nextStartMinutes and initializes it to
startMinutes (symbol: LearningSlot, field: nextStartMinutes, initializer:
startMinutes) but the code never reads or updates it; either remove
nextStartMinutes from the LearningSlot definition and all initializations/usages
to avoid dead state, or implement the intended consumption logic: update
nextStartMinutes whenever a slot is partially consumed (where slots are
allocated/adjusted in functions handling slot assignment) and ensure all
consumers read nextStartMinutes instead of startMinutes when determining the
next available time. Make the change consistently across the LearningSlot
construction and any slot-assignment code paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 96a6b3d2-f869-4756-ae98-3a191aabc92e
⛔ Files ignored due to path filters (1)
convex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (10)
convex/learningPlanAi.tsconvex/learningPlans.tsconvex/learningTimes.tsconvex/schema.tssrc/app/(app)/settings.tsxsrc/app/learning-plans/[planId]/index.tsxsrc/app/learning-plans/[planId]/review.tsxsrc/app/learning-times/edit.tsxsrc/app/learning-times/index.tsxsrc/features/learning-plans/types.ts
| year: "numeric", | ||
| }).format(date); | ||
|
|
||
| const formatDateKey = (date: Date) => date.toISOString().slice(0, 10); |
There was a problem hiding this comment.
Major: formatDateKey uses UTC, creating timezone mismatch with Berlin-based slot logic.
formatDateKey extracts the date portion from toISOString(), which always returns UTC. The rest of the code uses getBerlinDayOfWeek and expects Berlin timezone semantics. A Date object at 23:30 Berlin time on June 14 will be 21:30 UTC on June 14, so toISOString() still gives "2026-06-14", but at 01:30 Berlin time on June 15 it becomes 23:30 UTC on June 14, yielding "2026-06-14" instead of the correct Berlin date "2026-06-15".
This causes slot date keys to be off by one day near midnight in Berlin.
🔧 Recommended fix: format date in Berlin timezone
const formatDateKey = (date: Date) => {
- return date.toISOString().slice(0, 10);
+ const formatter = new Intl.DateTimeFormat("en-CA", {
+ timeZone: "Europe/Berlin",
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ });
+ return formatter.format(date); // returns YYYY-MM-DD in Berlin TZ
};🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@convex/learningPlanAi.ts` at line 515, The formatDateKey function currently
uses date.toISOString().slice(0,10) which returns a UTC date and causes
off-by-one day errors versus the Berlin-based logic (e.g., getBerlinDayOfWeek);
change formatDateKey to format the Date in the "Europe/Berlin" timezone (e.g.,
via Intl.DateTimeFormat or toLocaleDateString with timeZone: 'Europe/Berlin')
and produce a YYYY-MM-DD string (ISO date) from that Berlin-localized result
instead of slicing toISOString().
| const learningTimes = useQuery( | ||
| api.learningTimes.listMine, | ||
| user && isConvexAuthenticated ? {} : "skip", | ||
| ); |
There was a problem hiding this comment.
Block saving until the selected day has finished loading.
While learningTimes is still undefined, selectedEntry is treated as missing, so hasChanges becomes true and the save button enables with the default 17:00-17:30 values. On an existing day, a quick tap here will patch that saved slot back to the defaults once upsertMine runs.
Suggested fix
const learningTimes = useQuery(
api.learningTimes.listMine,
user && isConvexAuthenticated ? {} : "skip",
);
+ const isLearningTimesLoading = learningTimes === undefined;
...
const hasChanges =
!selectedEntry ||
startTime !== (selectedEntry?.startTime ?? DEFAULT_START_TIME) ||
endTime !== (selectedEntry?.endTime ?? DEFAULT_END_TIME);
const canRemove = Boolean(selectedEntry) && !isSaving;
const canSave =
+ !isLearningTimesLoading &&
hasChanges &&
parseTimeToMinutes(endTime) > parseTimeToMinutes(startTime) &&
!isSaving &&
Boolean(user) &&
isConvexAuthenticated;Also applies to: 136-146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/learning-times/edit.tsx` around lines 97 - 100, The save flow enables
when learningTimes is still undefined causing hasChanges to be true and
upsertMine to overwrite existing slots; fix by treating the query-loading state
as not-ready: use the result metadata from useQuery(api.learningTimes.listMine,
...)—or check learningTimes !== undefined / query.isLoading—before computing
selectedEntry and hasChanges and before enabling the Save button or calling
upsertMine; update the logic in the selectedEntry/hasChanges computation and the
Save button handler (references: learningTimes, selectedEntry, hasChanges,
useQuery, api.learningTimes.listMine, upsertMine) so save is disabled while the
selected day is still loading (apply same guard to the other similar block at
lines ~136-146).
| <Button | ||
| accessibilityLabel="Lernzeit hinzufügen" | ||
| className="h-[74px] w-[74px] rounded-full px-0" | ||
| onPress={() => openEditor(firstMissingDay)} | ||
| > | ||
| <Plus size={32} color="#FFFFFF" strokeWidth={1.8} /> | ||
| </Button> |
There was a problem hiding this comment.
Hide or disable the add FAB once all 7 days are configured.
firstMissingDay falls back to 1, so after the user has filled every weekday this “Lernzeit hinzufügen” button opens Monday for editing instead of adding anything. That’s misleading and makes accidental overwrites easy.
Suggested fix
+ const hasMissingDay = LEARNING_DAYS.some(
+ (day) => !learningTimes?.some((entry) => entry.dayOfWeek === day.value),
+ );
const firstMissingDay =
LEARNING_DAYS.find(
(day) =>
!learningTimes?.some((entry) => entry.dayOfWeek === day.value),
)?.value ?? 1;
...
- <Button
- accessibilityLabel="Lernzeit hinzufügen"
- className="h-[74px] w-[74px] rounded-full px-0"
- onPress={() => openEditor(firstMissingDay)}
- >
- <Plus size={32} color="`#FFFFFF`" strokeWidth={1.8} />
- </Button>
+ {hasMissingDay ? (
+ <Button
+ accessibilityLabel="Lernzeit hinzufügen"
+ className="h-[74px] w-[74px] rounded-full px-0"
+ onPress={() => openEditor(firstMissingDay)}
+ >
+ <Plus size={32} color="`#FFFFFF`" strokeWidth={1.8} />
+ </Button>
+ ) : null}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/app/learning-times/index.tsx` around lines 198 - 204, The FAB currently
always renders and calls openEditor(firstMissingDay) where firstMissingDay falls
back to 1, causing accidental overwrites; update the rendering logic around the
Button in the LearningTimes component so it is hidden or disabled when all seven
days are already configured: detect "allConfigured" by checking the weekday
configuration array (the same logic that computes firstMissingDay), and if true
either don't render the Button or render it with disabled={true} and an updated
accessibilityLabel; ensure openEditor is only invoked when a valid
firstMissingDay exists (e.g., guard on firstMissingDay !== null/undefined before
calling openEditor).
# Conflicts: # src/app/(app)/settings.tsx
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/app/learning-times/index.tsx (1)
102-109:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
goBackgrows the navigation stack instead of popping.When
router.canGoBack()is true, pushing/settingsappends a new entry rather than returning to the existing one. The user can end up with stacked screens, and a subsequent hardware back press lands on this screen again instead of exiting. Userouter.back()for the pop case.Suggested fix
const goBack = () => { if (router.canGoBack()) { - router.push("/settings"); + router.back(); return; } router.replace("/settings"); };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/app/learning-times/index.tsx` around lines 102 - 109, The goBack function currently pushes "/settings" when router.canGoBack() is true, which grows the navigation stack; change the pop case to call router.back() instead of router.push("/settings") so it returns to the previous entry, and keep router.replace("/settings") as the fallback when cannot go back; update the goBack function (referenced by the symbol goBack and usages of router.canGoBack(), router.push(), router.replace()) accordingly.convex/learningPlanAi.ts (2)
830-845:⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftStop scheduling after the recommended plan minutes are exhausted.
This normalization is driven by
slots.slice(0, MAX_GENERATED_SESSIONS), not by the requested plan length. If the model asks for two 30-minute blocks and the user has ten 60-minute windows, this code emits ten sessions totaling 600 minutes. That also hides the insufficiency case, becauseplannedMinutescan overshootrequestedMinutesinstead of reflecting the recommended plan.Also applies to: 875-885
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/learningPlanAi.ts` around lines 830 - 845, The normalization currently takes up to MAX_GENERATED_SESSIONS from slots and doesn't stop when the user's requestedMinutes are exhausted, causing plannedMinutes to overshoot; update normalizedSessions creation (and the similar logic around the block handling at 875-885) to consume slots sequentially while tracking remainingMinutes initialized to requestedMinutes and only emit a session when its duration (slot.endMinutes - slot.startMinutes) fits into remainingMinutes (or trim the final session to the remainingMinutes), decrement remainingMinutes accordingly, and stop producing more sessions once remainingMinutes <= 0; keep using prioritizedSessions/sessions to pick the source and preserve MIN_LEARNING_SLOT_MINUTES filtering but ensure the overall produced plannedMinutes cannot exceed requestedMinutes.
827-843:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPick the latest eligible template session, not the first one.
Line 839 uses
find(...)onprioritizedSessionsafter sorting ascending bypreferredDateKey. Once the earliest session becomes eligible, it wins every later slot as well, so later theory/practice/rehearsal templates never get applied and the normalized plan collapses into repeated copies of the first session.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/learningPlanAi.ts` around lines 827 - 843, The normalizedSessions mapping picks the first prioritizedSessions item whose preferredDateKey <= slot.dateKey, causing earlier templates to always win; change the selection to pick the latest eligible template instead (i.e., the prioritizedSessions entry with the greatest preferredDateKey that is <= slot.dateKey). In the normalizedSessions logic (around the variables slots, prioritizedSessions, preferredDateKey, session) replace the current find(...) with a “find-last” approach (e.g., iterate reversed or use a findLast utility/Array.prototype.at combined with reverse) so you choose the most recent eligible prioritizedSessions entry, keeping the same fallback behavior to prioritizedSessions[index % ...]?.session and sessions[index % ...] if none match.
🧹 Nitpick comments (1)
convex/learningPlans.test.ts (1)
122-159: ⚡ Quick winAssertion doesn't actually verify the draft session was left unsynced.
The session is at
2026-06-05 09:00(30 min) and the final entry is created at11:00— these never overlap, so the creation would succeed whether or not the draft session produced a day entry. The test therefore passes even ifreplaceGeneratedSessionsstarted syncing again. Assert directly that no"Lernen"day entry exists for the session's day:💚 Strengthen the assertion
- const createdId = await t.mutation(api.dayEntries.create, { - dayKey: "2026-06-05", - title: "Physik Test", - time: "11:00", - kind: "Leistungskontrolle", - plannedDateLabel: "5. Juni 2026", - durationMinutes: 45, - examTypeLabel: "Test", - }); - - expect(createdId).toBeTruthy(); + const entries = await t.query(api.dayEntries.listByDayKeys, { + dayKeys: ["2026-06-05"], + }); + expect( + entries["2026-06-05"]?.some((entry) => entry.kind === "Lernen"), + ).toBe(false);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@convex/learningPlans.test.ts` around lines 122 - 159, The test currently only creates an unrelated day entry at 11:00 so it never fails if replaceGeneratedSessions synced the generated "Lernen" session; after calling internal.learningPlans.replaceGeneratedSessions you should query the day entries for dayKey "2026-06-05" and assert that no entry exists with the generated session title ("Lernen") (i.e. call the appropriate query on day entries for that date and expect the result array to not contain an entry with title "Lernen") before proceeding to create the unrelated 11:00 entry using api.dayEntries.create; keep references to internal.learningPlans.replaceGeneratedSessions and api.dayEntries.create so the check is placed immediately after the replaceGeneratedSessions mutation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@convex/learningPlanAi.ts`:
- Around line 597-614: getOccupiedIntervalsByDay currently skips entries missing
time or durationMinutes, causing untimed/undurationed dayEntries to be ignored;
change it so any entry that lacks a valid time or lacks a positive
durationMinutes is treated as an all-day occupied interval (0 to 24*60) for that
entry.dayKey. Concretely, in getOccupiedIntervalsByDay (and where
parseTimeToMinutes is used) keep the existing behavior for entries with both a
valid time and positive durationMinutes (push {start, end: start +
durationMinutes}), but if entry.time is missing/parseTimeToMinutes returns null
or entry.durationMinutes is missing/<=0, push {start: 0, end: 24*60} for
entry.dayKey so untimed/undurationed OccupiedEntry records are subtracted from
learning windows. Ensure you still use entry.dayKey to group intervals.
In `@convex/learningPlans.ts`:
- Around line 671-698: The calendar scan in getAiContext iterates unbounded over
getLearningPlanCalendarDayKeys (derived from getAvailableDays), causing
potentially hundreds of sequential dayEntries queries; fix by capping the scan
window or replacing the loop with a ranged query: either introduce a
MAX_CALENDAR_SCAN_DAYS constant and apply it to the array returned by
getLearningPlanCalendarDayKeys before iterating, or compute the first and last
dayKey and perform a single ranged query against the dayEntries index
(withIndex("by_ownerTokenIdentifier_and_dayKey")) filtering ownerTokenIdentifier
and dayKey between those bounds (instead of per-day queries using
getDayKeyQueryVariants), and preserve deduplication behavior currently
implemented with seenEntryIds.
In `@src/app/learning-plans/`[planId]/review.tsx:
- Around line 155-169: The empty-state text currently always shows the "fully
occupied" message when snapshot exists but snapshot.sessions.length === 0;
update the rendering logic in review.tsx to derive the copy from
snapshot.plan.planningHint (or branch on its value) instead of hardcoding the
second variant: inspect snapshot.plan.planningHint (e.g., values like
"NO_LEARNING_TIMES" vs "ALL_TIMES_OCCUPIED" or equivalent) and render the
corresponding title and body for each case, or only render the existing "fully
occupied" card when planningHint indicates fully-occupied; keep the same styling
and components (View, Text) and use snapshot.plan.planningHint and
snapshot.sessions to decide which message to show.
---
Outside diff comments:
In `@convex/learningPlanAi.ts`:
- Around line 830-845: The normalization currently takes up to
MAX_GENERATED_SESSIONS from slots and doesn't stop when the user's
requestedMinutes are exhausted, causing plannedMinutes to overshoot; update
normalizedSessions creation (and the similar logic around the block handling at
875-885) to consume slots sequentially while tracking remainingMinutes
initialized to requestedMinutes and only emit a session when its duration
(slot.endMinutes - slot.startMinutes) fits into remainingMinutes (or trim the
final session to the remainingMinutes), decrement remainingMinutes accordingly,
and stop producing more sessions once remainingMinutes <= 0; keep using
prioritizedSessions/sessions to pick the source and preserve
MIN_LEARNING_SLOT_MINUTES filtering but ensure the overall produced
plannedMinutes cannot exceed requestedMinutes.
- Around line 827-843: The normalizedSessions mapping picks the first
prioritizedSessions item whose preferredDateKey <= slot.dateKey, causing earlier
templates to always win; change the selection to pick the latest eligible
template instead (i.e., the prioritizedSessions entry with the greatest
preferredDateKey that is <= slot.dateKey). In the normalizedSessions logic
(around the variables slots, prioritizedSessions, preferredDateKey, session)
replace the current find(...) with a “find-last” approach (e.g., iterate
reversed or use a findLast utility/Array.prototype.at combined with reverse) so
you choose the most recent eligible prioritizedSessions entry, keeping the same
fallback behavior to prioritizedSessions[index % ...]?.session and
sessions[index % ...] if none match.
In `@src/app/learning-times/index.tsx`:
- Around line 102-109: The goBack function currently pushes "/settings" when
router.canGoBack() is true, which grows the navigation stack; change the pop
case to call router.back() instead of router.push("/settings") so it returns to
the previous entry, and keep router.replace("/settings") as the fallback when
cannot go back; update the goBack function (referenced by the symbol goBack and
usages of router.canGoBack(), router.push(), router.replace()) accordingly.
---
Nitpick comments:
In `@convex/learningPlans.test.ts`:
- Around line 122-159: The test currently only creates an unrelated day entry at
11:00 so it never fails if replaceGeneratedSessions synced the generated
"Lernen" session; after calling internal.learningPlans.replaceGeneratedSessions
you should query the day entries for dayKey "2026-06-05" and assert that no
entry exists with the generated session title ("Lernen") (i.e. call the
appropriate query on day entries for that date and expect the result array to
not contain an entry with title "Lernen") before proceeding to create the
unrelated 11:00 entry using api.dayEntries.create; keep references to
internal.learningPlans.replaceGeneratedSessions and api.dayEntries.create so the
check is placed immediately after the replaceGeneratedSessions mutation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 08bd73af-1275-4c45-84c1-4fe7f8c39ca0
📒 Files selected for processing (7)
convex/dayEntries.tsconvex/learningPlanAi.tsconvex/learningPlans.test.tsconvex/learningPlans.tssrc/app/(app)/settings.tsxsrc/app/learning-plans/[planId]/review.tsxsrc/app/learning-times/index.tsx
| const getOccupiedIntervalsByDay = (occupiedEntries: OccupiedEntry[]) => { | ||
| const intervalsByDay = new Map<string, Array<{ start: number; end: number }>>(); | ||
|
|
||
| for (const entry of occupiedEntries) { | ||
| if (!entry.time || !entry.durationMinutes || entry.durationMinutes <= 0) { | ||
| continue; | ||
| } | ||
|
|
||
| const start = parseTimeToMinutes(entry.time); | ||
| if (start === null) continue; | ||
|
|
||
| const intervals = intervalsByDay.get(entry.dayKey) ?? []; | ||
| intervals.push({ start, end: start + entry.durationMinutes }); | ||
| intervalsByDay.set(entry.dayKey, intervals); | ||
| } | ||
|
|
||
| return intervalsByDay; | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C4 '\boccupiedEntries\b|\bdayKey\b|\btime\b|\bdurationMinutes\b' \
convex/learningPlans.ts \
convex/dayEntries.ts \
convex/schema.tsRepository: Dayova/dayova-mvp
Length of output: 21935
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Required guidelines read
if [ -f convex/_generated/ai/guidelines.md ]; then
echo "=== convex/_generated/ai/guidelines.md (head) ==="
head -n 120 convex/_generated/ai/guidelines.md
else
echo "Missing: convex/_generated/ai/guidelines.md"
fi
# 2) Find all places that insert/patch dayEntries (where time/durationMinutes might be omitted)
echo "=== insert dayEntries locations ==="
rg -n --hidden --no-ignore -S 'ctx\.db\.(insert|patch)\("dayEntries"' convex -g'*.ts' -g'*.js' -C2
echo "=== insert dayEntries: quick scan for time/durationMinutes fields ==="
rg -n --hidden --no-ignore -S 'insert\("dayEntries".{0,200}(time|durationMinutes)' convex -g'*.ts' -g'*.js' -C2 || true
echo "=== patch dayEntries: quick scan for time/durationMinutes fields ==="
rg -n --hidden --no-ignore -S 'patch\("dayEntries".{0,200}(time|durationMinutes)' convex -g'*.ts' -g'*.js' -C2 || trueRepository: Dayova/dayova-mvp
Length of output: 8603
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show dayEntries schema/validators and create() handler validation
echo "=== convex/dayEntries.ts: entryFields + create mutation (relevant lines) ==="
rg -n "const entryFields|export const create = mutation" convex/dayEntries.ts
python3 - <<'PY'
import itertools,sys,os,subprocess,re,math,statistics
path="convex/dayEntries.ts"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
# print around create handler roughly based on earlier rg output; scan for "export const create = mutation"
for i,l in enumerate(lines):
if "export const create = mutation" in l:
start=max(0,i-40); end=min(len(lines), i+120)
print("".join(f"{j+1:6d}:{lines[j]}" for j in range(start,end)))
break
PY
# Inspect assertNoScheduleConflict
echo "=== convex/scheduleConflicts.ts: assertNoScheduleConflict ==="
rg -n "assertNoScheduleConflict" convex/scheduleConflicts.ts
python3 - <<'PY'
path="convex/scheduleConflicts.ts"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
for i,l in enumerate(lines):
if "export const assertNoScheduleConflict" in l or "function assertNoScheduleConflict" in l or "assertNoScheduleConflict" in l and "export" in l:
start=max(0,i-40); end=min(len(lines), i+200)
print("".join(f"{j+1:6d}:{lines[j]}" for j in range(start,end)))
break
PY
# Inspect the occupiedEntries builder in convex/learningPlans.ts
echo "=== convex/learningPlans.ts: occupiedEntries builder (around lines ~660-710) ==="
python3 - <<'PY'
path="convex/learningPlans.ts"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
# locate "occupiedEntries"
for i,l in enumerate(lines):
if "const occupiedEntries" in l:
start=max(0,i-30); end=min(len(lines), i+90)
print("".join(f"{j+1:6d}:{lines[j]}" for j in range(start,end)))
break
PY
# Inspect learningPlanAi.ts where occupiedEntries passed into getOccupiedIntervalsByDay
echo "=== convex/learningPlanAi.ts: getOccupiedIntervalsByDay usage context (around function) ==="
python3 - <<'PY'
path="convex/learningPlanAi.ts"
with open(path,'r',encoding='utf-8') as f:
lines=f.readlines()
# find getOccupiedIntervalsByDay definition
for i,l in enumerate(lines):
if "const getOccupiedIntervalsByDay" in l:
start=max(0,i-60); end=min(len(lines), i+80)
print("".join(f"{j+1:6d}:{lines[j]}" for j in range(start,end)))
break
PYRepository: Dayova/dayova-mvp
Length of output: 14997
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== convex/dayEntries.ts: entryFields + publicEntry (around entryFields) ==="
python3 - <<'PY'
import re
path="convex/dayEntries.ts"
with open(path,'r',encoding='utf-8') as f: lines=f.readlines()
# find "const entryFields"
for i,l in enumerate(lines):
if "const entryFields" in l:
start=max(0,i-20); end=min(len(lines), i+80)
for j in range(start,end):
print(f"{j+1:6d}:{lines[j].rstrip()}")
break
PY
echo "=== convex/dayEntries.ts: publicEntry definition (if any) ==="
python3 - <<'PY'
path="convex/dayEntries.ts"
with open(path,'r',encoding='utf-8') as f: lines=f.readlines()
for i,l in enumerate(lines):
if "const publicEntry" in l or "function publicEntry" in l or "export const publicEntry" in l:
start=max(0,i-20); end=min(len(lines), i+120)
for j in range(start,end):
print(f"{j+1:6d}:{lines[j].rstrip()}")
break
PY
echo "=== search for any 'kind' values / all-day semantics ==="
rg -n "kind.*\"|\".*kind\"|all[- ]day|day[- ]wide|Ganzt" convex/dayEntries.ts convex/learningPlanAi.ts convex/learningPlans.ts convex/scheduleConflicts.tsRepository: Dayova/dayova-mvp
Length of output: 9538
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "dayEntries\.create|api\.dayEntries\.create" -S . --glob '!**/node_modules/**'Repository: Dayova/dayova-mvp
Length of output: 1029
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== convex/dayEntries.test.ts ==="
wc -l convex/dayEntries.test.ts
sed -n '1,140p' convex/dayEntries.test.ts
echo "=== src/app/entry/new.tsx (create day entry UI) ==="
wc -l src/app/entry/new.tsx
# show around where it constructs payload for api.dayEntries.create
rg -n "dayEntries\.create|createDayEntry|durationMinutes|time" src/app/entry/new.tsx
# print around those matches
python3 - <<'PY'
import re
path="src/app/entry/new.tsx"
with open(path,'r',encoding='utf-8') as f: lines=f.readlines()
hits=[]
for i,l in enumerate(lines):
if any(k in l for k in ["api.dayEntries.create","createDayEntry","durationMinutes","time:","time?"]):
hits.append(i)
for i in hits[:20]:
start=max(0,i-20); end=min(len(lines), i+40)
print("\n--- context lines",start+1,"-",end,"(match at",i+1,") ---\n")
for j in range(start,end):
print(f"{j+1:5d}:{lines[j].rstrip()}")
PYRepository: Dayova/dayova-mvp
Length of output: 13521
Fix: untimed/undurationed dayEntries are silently ignored when generating occupied intervals
convex/dayEntries.tsdefines bothtimeanddurationMinutesasv.optional(...), anddayEntries.createdoes not validate that they are present together—sodayEntriesdocs can exist with either field missing.convex/learningPlanAi.ts#getOccupiedIntervalsByDayskips those entries (if (!entry.time || !entry.durationMinutes || entry.durationMinutes <= 0) continue;), meaning they won’t subtract from learning windows and the generated plan can overlap them.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@convex/learningPlanAi.ts` around lines 597 - 614, getOccupiedIntervalsByDay
currently skips entries missing time or durationMinutes, causing
untimed/undurationed dayEntries to be ignored; change it so any entry that lacks
a valid time or lacks a positive durationMinutes is treated as an all-day
occupied interval (0 to 24*60) for that entry.dayKey. Concretely, in
getOccupiedIntervalsByDay (and where parseTimeToMinutes is used) keep the
existing behavior for entries with both a valid time and positive
durationMinutes (push {start, end: start + durationMinutes}), but if entry.time
is missing/parseTimeToMinutes returns null or entry.durationMinutes is
missing/<=0, push {start: 0, end: 24*60} for entry.dayKey so
untimed/undurationed OccupiedEntry records are subtracted from learning windows.
Ensure you still use entry.dayKey to group intervals.
| const occupiedEntries: Array<{ | ||
| dayKey: string; | ||
| time?: string; | ||
| durationMinutes?: number; | ||
| }> = []; | ||
| const seenEntryIds = new Set<string>(); | ||
| for (const dayKey of getLearningPlanCalendarDayKeys(plan.examDateKey)) { | ||
| for (const queryDayKey of getDayKeyQueryVariants(dayKey)) { | ||
| const entries = await ctx.db | ||
| .query("dayEntries") | ||
| .withIndex("by_ownerTokenIdentifier_and_dayKey", (q) => | ||
| q | ||
| .eq("ownerTokenIdentifier", identity.tokenIdentifier) | ||
| .eq("dayKey", queryDayKey), | ||
| ) | ||
| .take(50); | ||
|
|
||
| for (const entry of entries) { | ||
| if (seenEntryIds.has(entry._id)) continue; | ||
| seenEntryIds.add(entry._id); | ||
| occupiedEntries.push({ | ||
| dayKey, | ||
| time: entry.time, | ||
| durationMinutes: entry.durationMinutes, | ||
| }); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Unbounded calendar scan in getAiContext can issue hundreds of sequential queries.
The occupancy scan iterates over every calendar day returned by getLearningPlanCalendarDayKeys, whose length equals getAvailableDays(plan.examDateKey) — and that value has no upper bound. For an exam several weeks or months out, this becomes days × dayKeyVariants sequential dayEntries lookups (each take(50)), which slows AI-context retrieval and risks hitting Convex read limits during plan generation.
Since getAvailableDays is only consumed by this helper, capping it is a safe, localized fix:
⚡ Proposed cap on the scan window
+const MAX_PLAN_DAYS = 120;
+
const getAvailableDays = (examDateKey: string) => {
const examTime = new Date(examDateKey).getTime();
if (!Number.isFinite(examTime)) return 7;
- return Math.max(0, Math.ceil((examTime - Date.now()) / 86_400_000));
+ return Math.min(
+ MAX_PLAN_DAYS,
+ Math.max(0, Math.ceil((examTime - Date.now()) / 86_400_000)),
+ );
};Alternatively, replace the per-day loop with a single ranged query over by_ownerTokenIdentifier_and_dayKey between the first and last day key.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@convex/learningPlans.ts` around lines 671 - 698, The calendar scan in
getAiContext iterates unbounded over getLearningPlanCalendarDayKeys (derived
from getAvailableDays), causing potentially hundreds of sequential dayEntries
queries; fix by capping the scan window or replacing the loop with a ranged
query: either introduce a MAX_CALENDAR_SCAN_DAYS constant and apply it to the
array returned by getLearningPlanCalendarDayKeys before iterating, or compute
the first and last dayKey and perform a single ranged query against the
dayEntries index (withIndex("by_ownerTokenIdentifier_and_dayKey")) filtering
ownerTokenIdentifier and dayKey between those bounds (instead of per-day queries
using getDayKeyQueryVariants), and preserve deduplication behavior currently
implemented with seenEntryIds.
# Conflicts: # convex/generatedGermanTextRepair.test.ts # convex/learningPlanAi.ts # convex/learningPlans.test.ts # convex/learningPlans.ts
Adds personal learning times and uses them when generating AI learning plans.
This branch introduces a new
userLearningTimesConvex table with authenticated queries/mutations for listing, saving, and removing a user’s weekly learning availability. The app now exposes a Lernzeiten settings flow where users can add or edit available learning windows per weekday.Learning plan generation now includes those personal learning times in the AI context and normalizes generated sessions into the user’s available time slots. If no learning times exist, or if the saved availability cannot fit the full recommended plan, the generated plan stores and displays a planning hint on the review and sessions screens.
Also includes a staged UI adjustment on the review screen that moves the “Lernplan erstellen” and add-session controls into a fixed bottom action bar with safe-area spacing.
Summary by CodeRabbit
New Features
Bug Fixes / Behavior