feat(scheduled-tasks): minute-granular calendar + user timezone preference#5038
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Week/day calendar places task chips at exact minutes in a per-day overlay ( Task modal submits the effective timezone, re-seeds defaults when settings load, and supports Duplicate (context menu + Reviewed by Cursor Bugbot for commit a095ef9. Configure here. |
Greptile SummaryThis PR delivers two follow-ups to the scheduled-tasks calendar: minute-granular chip positioning in week/day views (replacing hour-bucketing with a
Confidence Score: 5/5Safe to merge — all timezone arithmetic is correct, the DST two-pass resolution is well-tested, and the lane-layout algorithm matches the test expectations. The change is well-scoped: new utilities are pure functions with comprehensive unit tests, the calendar refactor cleanly removes hour-bucketing, and the timezone preference flows correctly from settings through scheduling. The two findings are both non-blocking style/consistency issues that do not affect correctness.
Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant GS as General Settings UI
participant API as /api/users/me/settings
participant DB as settings table
participant TM as TaskModal
participant SC as ScheduleCalendar
participant BE as Schedules API
U->>GS: Select timezone in picker
GS->>API: PATCH timezone validated by ianaTimezoneSchema
API->>DB: UPDATE settings SET timezone
DB-->>API: ok
API-->>GS: success
Note over GS: useTimezone() now returns saved zone
U->>SC: Open calendar
SC->>SC: useCalendar(timezone) - today and anchor set via zonedClockDate
U->>SC: Click time slot
SC->>TM: open slot with timezone
TM->>TM: defaultLaunch and isLaunchInPast both use zonedWallClockToUtc
U->>TM: Submit draft
TM->>BE: POST schedule with timezone and UTC-resolved time
BE->>DB: INSERT schedule
Note over SC: taskToCalendarEvent positions start via zonedClockDate(runAt, task.timezone)
Reviews (14): Last reviewed commit: "fix(scheduled-tasks): DST fall-back reso..." | Re-trigger Greptile |
Replace hour-bucketed event rendering with a per-day absolute overlay that places each task chip at timeToOffset(start), so a 5:38 task sits at 5:38 instead of the top of the 5:00 cell. Hour cells become click/gridline-only; the overlay is non-interactive so empty-space clicks still create. Removes the now-obsolete eventsByHour/hourKey/bucketEventsByHour path (month view already used eventsByDay).
Add a Timezone preference under Settings → General. Scheduled tasks now run in the user's chosen IANA zone instead of whatever device created them. - settings table gains a nullable `timezone` column (migration 0236); null means "use the browser-detected zone", so existing users are unchanged - contract: validated IANA `timezone` on the settings get/update shapes - useTimezone() resolves the saved zone or the browser fallback; the task modal captures it instead of recomputing the device zone - General settings adds a searchable timezone combobox defaulting to the detected zone - shared timezone util (getBrowserTimezone / getSupportedTimezones)
88bddd4 to
7be0163
Compare
|
@greptile |
|
@cursor review |
Address review: one-time runs and the end-of-day boundary were resolved in the browser zone, so a task could fire at the wrong instant when the account zone differed from the device. Resolve wall-clock launch/end through the account zone (DST-correct), and evaluate the past-launch guard and the default seed in that zone too — matching how the recurring cron is already evaluated. - timezone util: zonedWallClockToUtc (DST-correct, no library), wallClockNow; getSupportedTimezones falls back to a common set and always includes UTC - recurrenceToScheduleFields takes the timezone and resolves time/endsAt in it - settings timezone Label drops its dangling htmlFor - tests for the zone converter (UTC / +5:30 / DST) and the zoned mappings
|
@greptile |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4021af7. Configure here.
Tasks whose pills would collide now split the column into side-by-side lanes (like Google Calendar) instead of stacking on top of each other; tasks that don't overlap keep the full width. Adds a pure layoutColumn lane-assignment helper (interval clustering + greedy lane packing) with tests.
|
@greptile re-review the latest commits |
|
@cursor review |
…oading Address review (zone consistency): - recurrenceToCron derives weekday/day-of-month from a UTC-parsed calendar date so the cron targets the right day regardless of device zone - cronToRecurrence + editSeedFor recover the launch date/time and ends-on date read back in the schedule's zone (zonedWallClock), so editing shows the right values when the account zone differs from the device - defaultLaunch no longer compares browser-local slot days against account-zone "today" Features: - right-click Duplicate: opens a pre-filled create modal from any task (TaskEditSeed now extends a shared TaskPrefill; modal gains a prefill prop) - loading.tsx paints only the header chrome (the page is a calendar, not a table) so it no longer pops table -> calendar; the empty calendar loads tasks in - task context menu drops "See details" (finished tasks open on click)
|
@greptile re-review the latest commits |
|
@cursor review |
… the account one A task created in one zone but edited after the account zone changed (or duplicated) seeded its launch in the task's stored zone while validating and submitting in the current account zone, drifting unchanged run times. TaskPrefill now carries the task's timezone; the modal seeds AND submits in it for edit/duplicate, and only blank creates use the account zone.
|
@cursor review |
When useTimezone() resolves from the browser fallback to the saved account zone after mount, re-derive today (and the focused day, while it is still on today) so the grid frame, now-line, and fetched range stay in agreement. The focused day is preserved across the change once the user has navigated away.
|
@greptile review |
|
@cursor review |
…roll on zone change visibleRange now expands the rendered span by a day on each side so an occurrence whose own-zone display day is on screen is never filtered out by the account-zone frame; bucketEventsByDay still places each on its zoned day, dropping any off-screen. The week/day auto-scroll re-centers when the effective timezone resolves.
|
@greptile review |
|
@cursor review |
The second cronToRecurrence call omitted the required timezone, so the recovered end date depended on the test runner's system zone instead of the schedule zone. Pin it to UTC for determinism.
|
@greptile review |
|
@cursor review |
useTimezone() starts on the browser fallback, so a blank create's next-top-of-the-hour default (and its past-launch guard) could be seeded in the wrong zone and submitted in the resolved account zone. Re-seed the default when the effective zone changes, unless the user has edited the fields; slot/edit/duplicate seeds are zone-stable and untouched.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 90a66c4. Configure here.
…, late-night pill bounds Audit follow-ups: - zonedWallClockToUtc resolves to the self-consistent instant, fixing one-time launches on the autumn fall-back day (were an hour early) while keeping the spring-forward gap rolling forward; adds DST regression tests. - defaultLaunch: today's whole-day (month-cell) click defaults to the next top of the hour like the header action, not a past 9am that disables Save. - DayEvents clips to the day bounds so a late-night pill never spills past the final hour row; now-line sits above event pills.
|
@greptile review |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit a095ef9. Configure here.
Summary
Two follow-ups to the scheduled-tasks calendar (PR #4979):
Minute-granular week/day positioning. Task chips in the week/day time grid now sit at their exact start time (a 5:38 task renders at 5:38, not the top of the 5:00 cell). Each day column renders a non-interactive absolute overlay positioning every chip at
top: timeToOffset(start)— the same mechanism the current-time line uses — so empty-space clicks still create. This made the hour-bucketing path (eventsByHour/hourKey/bucketEventsByHour) obsolete, so it's removed (month view already usedeventsByDay).User timezone preference. Settings → General gains a searchable Timezone picker. Scheduled tasks now run in the user's chosen IANA zone instead of whatever device created them.
settingstable gains a nullabletimezonecolumn (migration 0236);null= "use the browser-detected zone", so existing users are unchanged.useTimezone()resolves the saved zone or the browser fallback; the task modal captures it instead of recomputing the device zone.Type of Change
Testing
type-check,check:api-validation:strict,check:react-query,lint:checkall passChecklist