feat(tasks): timezone-aware scheduling picker (chat#1881 item 3c) - #1883
Conversation
Capture the viewer's local IANA timezone (Intl.DateTimeFormat(). resolvedOptions().timeZone) and send it to the API on task create/update so weekly reports fire at 9am local, not 9am UTC. - lib/timezone/: getLocalTimezone, getTimezoneOptions, getTaskTimezone, formatTimezoneLabel helpers (one export per file) - TimezoneSelect picker wired into the task details edit dialog - createTask/updateTask send optional `timezone`; useCreateTask defaults to the local zone - schedule display shows the zone label and local-time abbreviation The API accepts an optional `timezone` field (sibling api PR in flight); this change is forward-compatible and no-ops until that ships. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Next review available in: 53 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (14)
✨ Finishing Touches🧪 Generate unit tests (beta)
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.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 19fa51af02
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| schedule: DEFAULT_SCHEDULE, | ||
| artist_account_id: artistAccountId, | ||
| model: DEFAULT_MODEL, | ||
| timezone: getLocalTimezone(), |
There was a problem hiding this comment.
Default all task creation paths to local timezone
Please carry this default through every user-facing create path, not just the generic Tasks button: useConfirmFirstTask and useCreateStarterTask still reach createTask through buildFirstTaskParams/createStarterTask without a timezone, while their UI schedules “Mondays at 9am”. In non-UTC browsers those reports will continue to be created in the API default/UTC zone, so the 9am-local fix is bypassed for onboarding/home starter tasks.
Useful? React with 👍 / 👎.
| task: Record<string, unknown> & { timezone?: string | null }, | ||
| ): string { | ||
| const timezone = task.timezone; | ||
| return typeof timezone === "string" && timezone ? timezone : getLocalTimezone(); |
There was a problem hiding this comment.
Avoid inferring a timezone for legacy tasks
Please don't substitute the viewer's local zone when a returned task has no persisted timezone. That case covers existing rows and the rollout window where the API ignores the new field, and the old schedule is still interpreted in the backend default/UTC; this fallback makes read-only details label it as local and sends that local zone on the next Save even if the user only edits the title, silently changing when legacy tasks run.
Useful? React with 👍 / 👎.
| zones = FALLBACK_ZONES; | ||
| } | ||
|
|
||
| return zones.includes(local) ? zones : [local, ...zones]; |
There was a problem hiding this comment.
Include UTC in the selectable timezone list
Please add UTC explicitly after using the runtime-supported list. The Intl.supportedValuesOf('timeZone') list can omit UTC even though UTC is a valid timeZone, so non-UTC users cannot choose UTC from the picker; existing UTC tasks also cannot be changed back to UTC after selecting another zone.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
5 issues found across 18 files
Confidence score: 3/5
lib/timezone/getTaskTimezone.tsnow falls back legacy tasks to the viewer’s local zone instead of the backend default (UTC), which can shift existing schedules and fire reminders at the wrong local time after merge — align the fallback with legacy UTC behavior (or gate migration logic) before merging.hooks/useCreateTask.tsapplies a local-timezone default only in one create path, while other entry points still callcreateTaskwithout a timezone, so task behavior may vary by flow and produce inconsistent scheduling for users — centralize timezone defaulting in shared task-creation params before merge.lib/timezone/getTimezoneOptions.tsrelies onIntl.supportedValuesOf('timeZone')without guaranteeingUTC, so Chrome users may be unable to select UTC and could be forced into an unintended zone — explicitly injectUTCinto the options list.components/TimezoneSelect/TimezoneSelect.tsxrecreates manyIntl.DateTimeFormatinstances on re-render and includes an unreachable placeholder path, which is mostly a performance/cleanup concern rather than a blocker — memoize formatted labels and remove dead placeholder logic in a follow-up (or now if touching this file anyway).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="components/TimezoneSelect/TimezoneSelect.tsx">
<violation number="1" location="components/TimezoneSelect/TimezoneSelect.tsx:44">
P3: The `placeholder="Select a timezone"` on `SelectValue` is unreachable at runtime. `editTimezone` is always initialized from `getTaskTimezone(task)` which never returns an empty string — it falls back to the viewer's local zone or `"UTC"`. Since the value is always truthy, the placeholder text will never be shown. Consider either removing the placeholder prop or documenting the assumed invariant if you want to keep it for future resilience.</violation>
<violation number="2" location="components/TimezoneSelect/TimezoneSelect.tsx:51">
P3: Editing another task field re-renders this picker and reconstructs an `Intl.DateTimeFormat` for every supported timezone. Memoizing formatted option labels alongside `options` avoids hundreds of formatter allocations per render.</violation>
</file>
<file name="lib/timezone/getTaskTimezone.ts">
<violation number="1" location="lib/timezone/getTaskTimezone.ts:14">
P1: Falling back to the viewer's local timezone when a task has no persisted `timezone` is problematic for legacy tasks. Existing tasks have their schedule interpreted in the backend's default zone (UTC). This fallback causes the read-only UI to incorrectly label a UTC-based schedule as local time, and more critically, on the next save—even for unrelated edits like a title change—the local zone is sent to the API via `updateTask`, silently shifting when the task actually runs.
Consider returning `null` or `"UTC"` when the task has no stored timezone, and only applying the viewer's local zone for *new* task creation.</violation>
</file>
<file name="hooks/useCreateTask.ts">
<violation number="1" location="hooks/useCreateTask.ts:42">
P2: The local timezone default is applied here for the generic create path, but other task creation entry points (e.g., `useConfirmFirstTask`, `useCreateStarterTask` via `buildFirstTaskParams`) also call `createTask` without passing a `timezone`. For those flows—which typically schedule "Mondays at 9am"—the schedule will still be interpreted in the API's default zone (UTC), defeating the purpose of the timezone-aware scheduling feature for onboarding/starter tasks.</violation>
</file>
<file name="lib/timezone/getTimezoneOptions.ts">
<violation number="1" location="lib/timezone/getTimezoneOptions.ts:43">
P2: `Intl.supportedValuesOf('timeZone')` does not reliably include `"UTC"` across engines—Chrome omits it while Safari includes it. Since the code uses the runtime list directly without ensuring UTC's presence, Chrome-based users won't see UTC in the picker. This means existing UTC-scheduled tasks cannot be changed back to UTC after selecting another zone.
Consider explicitly adding `"UTC"` to the `zones` array if it's not already present (similar to how the local zone is prepended).</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| task: Record<string, unknown> & { timezone?: string | null }, | ||
| ): string { | ||
| const timezone = task.timezone; | ||
| return typeof timezone === "string" && timezone ? timezone : getLocalTimezone(); |
There was a problem hiding this comment.
P1: Falling back to the viewer's local timezone when a task has no persisted timezone is problematic for legacy tasks. Existing tasks have their schedule interpreted in the backend's default zone (UTC). This fallback causes the read-only UI to incorrectly label a UTC-based schedule as local time, and more critically, on the next save—even for unrelated edits like a title change—the local zone is sent to the API via updateTask, silently shifting when the task actually runs.
Consider returning null or "UTC" when the task has no stored timezone, and only applying the viewer's local zone for new task creation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/timezone/getTaskTimezone.ts, line 14:
<comment>Falling back to the viewer's local timezone when a task has no persisted `timezone` is problematic for legacy tasks. Existing tasks have their schedule interpreted in the backend's default zone (UTC). This fallback causes the read-only UI to incorrectly label a UTC-based schedule as local time, and more critically, on the next save—even for unrelated edits like a title change—the local zone is sent to the API via `updateTask`, silently shifting when the task actually runs.
Consider returning `null` or `"UTC"` when the task has no stored timezone, and only applying the viewer's local zone for *new* task creation.</comment>
<file context>
@@ -0,0 +1,15 @@
+ task: Record<string, unknown> & { timezone?: string | null },
+): string {
+ const timezone = task.timezone;
+ return typeof timezone === "string" && timezone ? timezone : getLocalTimezone();
+}
</file context>
| schedule: DEFAULT_SCHEDULE, | ||
| artist_account_id: artistAccountId, | ||
| model: DEFAULT_MODEL, | ||
| timezone: getLocalTimezone(), |
There was a problem hiding this comment.
P2: The local timezone default is applied here for the generic create path, but other task creation entry points (e.g., useConfirmFirstTask, useCreateStarterTask via buildFirstTaskParams) also call createTask without passing a timezone. For those flows—which typically schedule "Mondays at 9am"—the schedule will still be interpreted in the API's default zone (UTC), defeating the purpose of the timezone-aware scheduling feature for onboarding/starter tasks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useCreateTask.ts, line 42:
<comment>The local timezone default is applied here for the generic create path, but other task creation entry points (e.g., `useConfirmFirstTask`, `useCreateStarterTask` via `buildFirstTaskParams`) also call `createTask` without passing a `timezone`. For those flows—which typically schedule "Mondays at 9am"—the schedule will still be interpreted in the API's default zone (UTC), defeating the purpose of the timezone-aware scheduling feature for onboarding/starter tasks.</comment>
<file context>
@@ -38,6 +39,7 @@ export function useCreateTask() {
schedule: DEFAULT_SCHEDULE,
artist_account_id: artistAccountId,
model: DEFAULT_MODEL,
+ timezone: getLocalTimezone(),
});
},
</file context>
| const supported = (Intl as IntlWithSupportedValues).supportedValuesOf?.( | ||
| "timeZone", | ||
| ); | ||
| zones = supported?.length ? supported : FALLBACK_ZONES; |
There was a problem hiding this comment.
P2: Intl.supportedValuesOf('timeZone') does not reliably include "UTC" across engines—Chrome omits it while Safari includes it. Since the code uses the runtime list directly without ensuring UTC's presence, Chrome-based users won't see UTC in the picker. This means existing UTC-scheduled tasks cannot be changed back to UTC after selecting another zone.
Consider explicitly adding "UTC" to the zones array if it's not already present (similar to how the local zone is prepended).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/timezone/getTimezoneOptions.ts, line 43:
<comment>`Intl.supportedValuesOf('timeZone')` does not reliably include `"UTC"` across engines—Chrome omits it while Safari includes it. Since the code uses the runtime list directly without ensuring UTC's presence, Chrome-based users won't see UTC in the picker. This means existing UTC-scheduled tasks cannot be changed back to UTC after selecting another zone.
Consider explicitly adding `"UTC"` to the `zones` array if it's not already present (similar to how the local zone is prepended).</comment>
<file context>
@@ -0,0 +1,49 @@
+ const supported = (Intl as IntlWithSupportedValues).supportedValuesOf?.(
+ "timeZone",
+ );
+ zones = supported?.length ? supported : FALLBACK_ZONES;
+ } catch {
+ zones = FALLBACK_ZONES;
</file context>
| <SelectContent className="max-h-72"> | ||
| {options.map((zone) => ( | ||
| <SelectItem key={zone} value={zone} className="text-xs"> | ||
| {formatTimezoneLabel(zone)} |
There was a problem hiding this comment.
P3: Editing another task field re-renders this picker and reconstructs an Intl.DateTimeFormat for every supported timezone. Memoizing formatted option labels alongside options avoids hundreds of formatter allocations per render.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/TimezoneSelect/TimezoneSelect.tsx, line 51:
<comment>Editing another task field re-renders this picker and reconstructs an `Intl.DateTimeFormat` for every supported timezone. Memoizing formatted option labels alongside `options` avoids hundreds of formatter allocations per render.</comment>
<file context>
@@ -0,0 +1,63 @@
+ <SelectContent className="max-h-72">
+ {options.map((zone) => (
+ <SelectItem key={zone} value={zone} className="text-xs">
+ {formatTimezoneLabel(zone)}
+ </SelectItem>
+ ))}
</file context>
| </Label> | ||
| <Select value={value} onValueChange={onValueChange} disabled={disabled}> | ||
| <SelectTrigger id="task-timezone" className="w-full text-xs"> | ||
| <SelectValue placeholder="Select a timezone"> |
There was a problem hiding this comment.
P3: The placeholder="Select a timezone" on SelectValue is unreachable at runtime. editTimezone is always initialized from getTaskTimezone(task) which never returns an empty string — it falls back to the viewer's local zone or "UTC". Since the value is always truthy, the placeholder text will never be shown. Consider either removing the placeholder prop or documenting the assumed invariant if you want to keep it for future resilience.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/TimezoneSelect/TimezoneSelect.tsx, line 44:
<comment>The `placeholder="Select a timezone"` on `SelectValue` is unreachable at runtime. `editTimezone` is always initialized from `getTaskTimezone(task)` which never returns an empty string — it falls back to the viewer's local zone or `"UTC"`. Since the value is always truthy, the placeholder text will never be shown. Consider either removing the placeholder prop or documenting the assumed invariant if you want to keep it for future resilience.</comment>
<file context>
@@ -0,0 +1,63 @@
+ </Label>
+ <Select value={value} onValueChange={onValueChange} disabled={disabled}>
+ <SelectTrigger id="task-timezone" className="w-full text-xs">
+ <SelectValue placeholder="Select a timezone">
+ {formatTimezoneLabel(value)}
+ </SelectValue>
</file context>
✅ Preview verification — timezone picker works end-to-end (prefill + write + persist)Preview: Seeded a task via the API with
1. Edit prefill — America/Bogota (from the read path, not the browser): 2. After changing to Asia/Tokyo + Save, re-opened — the change persists: The picker also shows a helpful "Scheduled runs fire at this zone's local time." hint and a full IANA list. Test task cleaned up. Verdict: 3c·chat verified end-to-end — the last piece of 3c. With api#780/#781 merged, the picker prefills the real tz and round-trips correctly. CI green, MERGEABLE. Tracking: chat#1881 3c·chat. (Screenshots on |


What
Adds a timezone picker to the task scheduling UI so a task's cron schedule is interpreted in the user's chosen zone. Defaults to the viewer's local IANA timezone (
Intl.DateTimeFormat().resolvedOptions().timeZone) and sends it to the API on create/update. Scheduled times display in local time with the zone abbreviation.Part of #1881 — item 3c (chat side). Fixes the "weekly email lands 9am UTC (middle of the US night)" problem so it lands 9am local.
Changes
lib/timezone/— new helpers, one export per file:getLocalTimezone()— viewer's IANA zone, UTC fallbackgetTimezoneOptions()—Intl.supportedValuesOf("timeZone")with a curated fallback list, always including the local zonegetTaskTimezone(task)— reads the optional storedtimezone, falls back to localformatTimezoneLabel(zone)— "America/Los_Angeles" → "America/Los Angeles (GMT-7)"components/TimezoneSelect/TimezoneSelect.tsx— shadcnSelect-based picker, wired into the task details edit dialog beneath the cron editorlib/tasks/createTask.ts/updateTask.ts— send optionaltimezonein the bodyhooks/useCreateTask.ts— defaults new tasks to the local zoneformatScheduledActionDate— appends the local timezone abbreviation (e.g. "Jan 15, 2:30 PM PST")API dependency
The API accepts an optional
timezonefield — sibling api PR in flight (issue #1881 item 3c, api side). This chat change is forward-compatible: it sends the field, which the API ignores until the column/handler ship, then honors once deployed. Types here read the column optionally, so no regen is blocked.Tests / gates
pnpm vitest run lib/timezone lib/tasks— 25 passing (new: getLocalTimezone, getTimezoneOptions, createTask/updateTask includetimezone)pnpm exec tsc --noEmit— no new errors from these files (pre-existing baseline unchanged)pnpm build— compiles successfully (local page-data collection fails only on missing env creds, unrelated)Refs #1881
🤖 Generated with Claude Code
Summary by cubic
Adds a timezone picker to task scheduling so cron runs in the selected IANA zone. Defaults to the viewer’s local timezone, sends
timezoneon create/update, and shows times with zone labels. Addresses chat#1881 item 3c so weekly emails land at 9am local, not 9am UTC.New Features
timezone(empty omitted).formatScheduledActionDate.lib/timezone:getLocalTimezone,getTimezoneOptions(with fallback),getTaskTimezone,formatTimezoneLabel.timezonewhen provided.Migration
timezoneonce its change ships.Written for commit 19fa51a. Summary will update on new commits.