Commit 4ec26a0
authored
feat(scheduled-tasks): minute-granular calendar + user timezone preference (#5038)
* feat(scheduled-tasks): position week/day chips at their exact minute
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).
* feat(settings): user timezone preference for scheduled tasks
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)
* fix(scheduled-tasks): interpret launch/end times in the account timezone
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
* feat(scheduled-tasks): Google-Calendar-style side-by-side overlap layout
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.
* fix(scheduled-tasks): zone-consistent recurrence/edit + duplicate + loading
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)
* fix(scheduled-tasks): edit/duplicate use the task's own timezone, not 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.
* fix(scheduled-tasks): duplicating a past one-time task seeds a future launch
Audit follow-up: a duplicate of a one-time task whose launch already passed
opened with Schedule disabled. It now falls through to the next-hour default so
the new task is immediately schedulable. Also clarifies the DST spring-forward
note on zonedWallClockToUtc and drops a stray test comment.
* fix(scheduled-tasks): clear duplicate pre-fill when starting a fresh create
The create modal had two open-sources (isCreateOpen + duplicatePrefill) that
weren't coordinated. Starting a header/slot create now clears any duplicate
pre-fill (and duplicating closes any open create), so a stale duplicate draft
can never bleed into a blank or slot-seeded create.
* fix(scheduled-tasks): make create/duplicate/edit modals mutually exclusive
Opening any of the three modal flows (blank create, duplicate pre-fill, task
edit/record) now closes the other two, so the create modal can never co-exist
with the edit modal and no stale state survives a switch.
* feat(scheduled-tasks): render calendar in the effective timezone
Position each occurrence at its wall-clock time in the task's own
timezone, and draw the now-line / today highlight in the viewer's
effective zone, so the calendar always shows a task at the local time it
was scheduled for — matching the modal. Adds zonedClockDate as the single
zone boundary; the default case (account zone == browser zone) is
unchanged.
* fix(scheduled-tasks): re-sync calendar day frame when timezone resolves
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.
* fix(scheduled-tasks): pad view window for timezone slop; re-center scroll 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.
* test(scheduled-tasks): pass timezone to cronToRecurrence ends-on case
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.
* fix(scheduled-tasks): re-seed blank-create launch when timezone resolves
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.
* fix(scheduled-tasks): DST fall-back resolve, today month-cell default, 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.1 parent 32b380f commit 4ec26a0
25 files changed
Lines changed: 17309 additions & 215 deletions
File tree
- apps/sim
- app
- api/users/me/settings
- workspace/[workspaceId]
- scheduled-tasks
- components
- schedule-calendar
- components/time-grid
- task-modal
- hooks
- utils
- settings/components/general
- hooks/queries
- lib
- api/contracts
- core/utils
- packages/db
- migrations
- meta
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
24 | 24 | | |
25 | 25 | | |
26 | 26 | | |
| 27 | + | |
27 | 28 | | |
28 | 29 | | |
29 | 30 | | |
| |||
52 | 53 | | |
53 | 54 | | |
54 | 55 | | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| |||
78 | 80 | | |
79 | 81 | | |
80 | 82 | | |
| 83 | + | |
81 | 84 | | |
82 | 85 | | |
83 | 86 | | |
| |||
Lines changed: 76 additions & 31 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
7 | 8 | | |
8 | 9 | | |
9 | 10 | | |
| 11 | + | |
10 | 12 | | |
11 | 13 | | |
| 14 | + | |
12 | 15 | | |
13 | 16 | | |
14 | 17 | | |
15 | 18 | | |
16 | 19 | | |
17 | | - | |
| 20 | + | |
18 | 21 | | |
19 | 22 | | |
20 | 23 | | |
| |||
27 | 30 | | |
28 | 31 | | |
29 | 32 | | |
| 33 | + | |
| 34 | + | |
30 | 35 | | |
31 | 36 | | |
32 | 37 | | |
33 | 38 | | |
34 | | - | |
| 39 | + | |
35 | 40 | | |
36 | 41 | | |
37 | 42 | | |
38 | 43 | | |
39 | 44 | | |
40 | 45 | | |
41 | 46 | | |
| 47 | + | |
42 | 48 | | |
43 | 49 | | |
44 | | - | |
| 50 | + | |
45 | 51 | | |
46 | 52 | | |
47 | 53 | | |
| |||
53 | 59 | | |
54 | 60 | | |
55 | 61 | | |
56 | | - | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
57 | 66 | | |
58 | 67 | | |
59 | 68 | | |
60 | 69 | | |
61 | 70 | | |
62 | 71 | | |
63 | 72 | | |
64 | | - | |
65 | | - | |
66 | | - | |
67 | | - | |
68 | | - | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
69 | 77 | | |
70 | | - | |
| 78 | + | |
71 | 79 | | |
72 | 80 | | |
73 | | - | |
74 | 81 | | |
75 | 82 | | |
76 | | - | |
77 | | - | |
78 | 83 | | |
79 | 84 | | |
80 | 85 | | |
81 | | - | |
82 | 86 | | |
83 | 87 | | |
84 | | - | |
85 | | - | |
86 | 88 | | |
87 | 89 | | |
88 | 90 | | |
89 | 91 | | |
90 | 92 | | |
91 | 93 | | |
92 | | - | |
| 94 | + | |
93 | 95 | | |
94 | 96 | | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
95 | 126 | | |
96 | | - | |
97 | | - | |
| 127 | + | |
| 128 | + | |
98 | 129 | | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
104 | 145 | | |
105 | 146 | | |
106 | 147 | | |
| |||
113 | 154 | | |
114 | 155 | | |
115 | 156 | | |
116 | | - | |
| 157 | + | |
117 | 158 | | |
118 | 159 | | |
119 | 160 | | |
120 | 161 | | |
121 | 162 | | |
| 163 | + | |
122 | 164 | | |
123 | 165 | | |
124 | 166 | | |
125 | | - | |
| 167 | + | |
126 | 168 | | |
127 | 169 | | |
128 | 170 | | |
| |||
170 | 212 | | |
171 | 213 | | |
172 | 214 | | |
173 | | - | |
| 215 | + | |
174 | 216 | | |
175 | | - | |
| 217 | + | |
176 | 218 | | |
177 | 219 | | |
178 | 220 | | |
179 | | - | |
180 | 221 | | |
181 | 222 | | |
182 | | - | |
183 | | - | |
184 | 223 | | |
185 | 224 | | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
186 | 231 | | |
187 | 232 | | |
188 | 233 | | |
| |||
Lines changed: 12 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
4 | 5 | | |
5 | 6 | | |
6 | 7 | | |
| |||
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
| 25 | + | |
| 26 | + | |
24 | 27 | | |
25 | 28 | | |
26 | 29 | | |
| |||
33 | 36 | | |
34 | 37 | | |
35 | 38 | | |
36 | | - | |
| 39 | + | |
37 | 40 | | |
38 | | - | |
39 | | - | |
40 | 41 | | |
41 | 42 | | |
42 | 43 | | |
| |||
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
56 | | - | |
57 | | - | |
| 57 | + | |
| 58 | + | |
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
61 | 62 | | |
62 | 63 | | |
| 64 | + | |
63 | 65 | | |
64 | 66 | | |
65 | 67 | | |
| |||
70 | 72 | | |
71 | 73 | | |
72 | 74 | | |
73 | | - | |
74 | 75 | | |
75 | 76 | | |
76 | 77 | | |
| |||
96 | 97 | | |
97 | 98 | | |
98 | 99 | | |
99 | | - | |
| 100 | + | |
| 101 | + | |
100 | 102 | | |
101 | | - | |
| 103 | + | |
102 | 104 | | |
103 | 105 | | |
104 | 106 | | |
| |||
126 | 128 | | |
127 | 129 | | |
128 | 130 | | |
| 131 | + | |
129 | 132 | | |
130 | 133 | | |
131 | 134 | | |
132 | | - | |
| 135 | + | |
133 | 136 | | |
134 | 137 | | |
135 | 138 | | |
| |||
Lines changed: 14 additions & 9 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| |||
16 | 16 | | |
17 | 17 | | |
18 | 18 | | |
19 | | - | |
20 | 19 | | |
| 20 | + | |
| 21 | + | |
21 | 22 | | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
25 | | - | |
26 | | - | |
27 | | - | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
28 | 29 | | |
29 | 30 | | |
30 | 31 | | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
34 | | - | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
66 | 67 | | |
67 | 68 | | |
68 | 69 | | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
69 | 74 | | |
70 | 75 | | |
71 | 76 | | |
72 | 77 | | |
73 | 78 | | |
74 | 79 | | |
75 | 80 | | |
76 | | - | |
77 | | - | |
78 | | - | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
79 | 84 | | |
80 | 85 | | |
81 | 86 | | |
| |||
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | | - | |
| 1 | + | |
0 commit comments