Skip to content

feat(tasks): timezone-aware scheduling picker (chat#1881 item 3c) - #1883

Merged
sweetmantech merged 1 commit into
mainfrom
feat/schedule-timezone-picker
Jul 22, 2026
Merged

feat(tasks): timezone-aware scheduling picker (chat#1881 item 3c)#1883
sweetmantech merged 1 commit into
mainfrom
feat/schedule-timezone-picker

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

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 #1881item 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 fallback
    • getTimezoneOptions()Intl.supportedValuesOf("timeZone") with a curated fallback list, always including the local zone
    • getTaskTimezone(task) — reads the optional stored timezone, falls back to local
    • formatTimezoneLabel(zone) — "America/Los_Angeles" → "America/Los Angeles (GMT-7)"
  • components/TimezoneSelect/TimezoneSelect.tsx — shadcn Select-based picker, wired into the task details edit dialog beneath the cron editor
  • lib/tasks/createTask.ts / updateTask.ts — send optional timezone in the body
  • hooks/useCreateTask.ts — defaults new tasks to the local zone
  • formatScheduledActionDate — appends the local timezone abbreviation (e.g. "Jan 15, 2:30 PM PST")
  • Read-only schedule section shows the zone label

API dependency

The API accepts an optional timezone field — 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 include timezone)
  • 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 timezone on 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 picker under the cron editor; schedule displays include the zone label.
    • New tasks default to the local zone; create/update send optional timezone (empty omitted).
    • Dates render with the local short TZ (e.g. “PST”) in formatScheduledActionDate.
    • Helpers in lib/timezone: getLocalTimezone, getTimezoneOptions (with fallback), getTaskTimezone, formatTimezoneLabel.
    • Tests cover helpers and that create/update bodies include timezone when provided.
  • Migration

    • No action needed. Forward-compatible: API will honor timezone once its change ships.

Written for commit 19fa51a. Summary will update on new commits.

Review in cubic

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>
@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 22, 2026 1:39pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 53 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a6794403-10b7-4375-8538-c5753a1d6d98

📥 Commits

Reviewing files that changed from the base of the PR and between d258412 and 19fa51a.

⛔ Files ignored due to path filters (4)
  • lib/tasks/__tests__/createTask.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/tasks/__tests__/updateTask.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/timezone/__tests__/getLocalTimezone.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/timezone/__tests__/getTimezoneOptions.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (14)
  • components/TimezoneSelect/TimezoneSelect.tsx
  • components/VercelChat/dialogs/tasks/TaskDetailsDialog.tsx
  • components/VercelChat/dialogs/tasks/TaskDetailsDialogActionButtons.tsx
  • components/VercelChat/dialogs/tasks/TaskDetailsDialogContent.tsx
  • components/VercelChat/dialogs/tasks/TaskScheduleSection.tsx
  • components/VercelChat/dialogs/tasks/useTaskDetailsDialog.ts
  • hooks/useCreateTask.ts
  • lib/tasks/createTask.ts
  • lib/tasks/updateTask.ts
  • lib/timezone/formatTimezoneLabel.ts
  • lib/timezone/getLocalTimezone.ts
  • lib/timezone/getTaskTimezone.ts
  • lib/timezone/getTimezoneOptions.ts
  • lib/utils/formatScheduledActionDate.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/schedule-timezone-picker

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread hooks/useCreateTask.ts
schedule: DEFAULT_SCHEDULE,
artist_account_id: artistAccountId,
model: DEFAULT_MODEL,
timezone: getLocalTimezone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 18 files

Confidence score: 3/5

  • lib/timezone/getTaskTimezone.ts now 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.ts applies a local-timezone default only in one create path, while other entry points still call createTask without 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.ts relies on Intl.supportedValuesOf('timeZone') without guaranteeing UTC, so Chrome users may be unable to select UTC and could be forced into an unintended zone — explicitly inject UTC into the options list.
  • components/TimezoneSelect/TimezoneSelect.tsx recreates many Intl.DateTimeFormat instances 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();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread hooks/useCreateTask.ts
schedule: DEFAULT_SCHEDULE,
artist_account_id: artistAccountId,
model: DEFAULT_MODEL,
timezone: getLocalTimezone(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

✅ Preview verification — timezone picker works end-to-end (prefill + write + persist)

Preview: https://chat-h196kscaa-recoup.vercel.app/tasks (reads test-recoup-api, which I synced with main so it has the 3c write+read: api#780 + api#781). Signed in as a real account.

Seeded a task via the API with America/Bogota, then drove the task-details dialog:

  1. Edit prefill reads the real timezone (the key fix). Opening the task, the Timezone picker prefills America/Bogota (GMT-5) — read from task.timezone (the api#781 read path), not the browser zone. This is exactly what closes the overwrite footgun: getTaskTimezone(task) now gets the schedule's value.
  2. Write works. Changed the picker to Asia/Tokyo (GMT+9) → Save → GET /api/tasks returns timezone: "Asia/Tokyo" (the schedule was updated via the Trigger.dev source of truth).
  3. Persists + re-reads. Re-opened the task → picker prefills Asia/Tokyo (GMT+9).

1. Edit prefill — America/Bogota (from the read path, not the browser):

picker prefills America/Bogota

2. After changing to Asia/Tokyo + Save, re-opened — the change persists:

picker prefills Asia/Tokyo after save

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 assets/pr1883-preview-verification.)

@sweetmantech
sweetmantech merged commit fb0c7c0 into main Jul 22, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant