Skip to content

Add timezone support for accurate time-of-day context in AI responses - #372

Merged
2witstudios merged 3 commits into
masterfrom
claude/fix-pulse-timing-X2hoB
Feb 4, 2026
Merged

2witstudios merged 3 commits into
masterfrom
claude/fix-pulse-timing-X2hoB

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 4, 2026 •

Copy link
Copy Markdown
Owner

Summary

This PR adds timezone support to the application to ensure AI-generated pulse summaries use accurate time-of-day context based on the user's actual location, rather than server time.

Key Changes

  • Database Schema: Added timezone column to the users table to store user's IANA timezone preference (e.g., "America/New_York")

  • Timezone Utilities: Created new timestamp-utils.ts module with three core functions:

    • getUserTimeOfDay(): Determines time period (morning/afternoon/evening) based on user's timezone
    • getStartOfTodayInTimezone(): Calculates midnight in user's timezone for accurate "today" calculations
    • buildTimestampSystemPrompt(): Enhanced to include timezone and time-of-day context in AI system prompts
  • Client-Side Detection: Updated the Pulse component to detect browser timezone using Intl.DateTimeFormat().resolvedOptions().timeZone and send it to the API

  • API Endpoints: Modified both /api/pulse/generate and /api/pulse/cron to:

    • Accept timezone from client requests
    • Use stored user timezone as fallback
    • Auto-update user profile if client provides a different timezone
    • Pass timezone to all time-related calculations and AI prompts
  • Test Coverage: Added comprehensive test suite for timezone utilities covering:

    • Time-of-day detection across different timezones
    • Start-of-day calculations with timezone offsets
    • System prompt generation with timezone context
    • Updated existing test mocks to include the new timezone field

Implementation Details

  • Timezone detection follows a priority order: client-provided → stored user preference → UTC default
  • Uses native Intl.DateTimeFormat API for reliable timezone handling across different environments
  • All time calculations now respect user's local timezone rather than server UTC
  • AI prompts now include explicit timezone context to improve response relevance

https://claude.ai/code/session_014W8BwjdD8e5Jo1kYM45ey9

Summary by CodeRabbit

Release Notes

  • New Features

    • Users' local timezone is now automatically detected and used for time-based calculations, ensuring accurate time displays and context-aware scheduling based on their location.
    • Timezone information is captured from the browser and persisted in the user profile.
  • Tests

    • Updated test fixtures and added comprehensive timezone utility tests to validate timezone handling across the application.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Feb 4, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 2 minutes and 29 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR adds comprehensive timezone support to the application. It introduces database schema changes (timezone column), new timezone utility functions for validation and time calculations, updates pulse generation and cron routes to perform timezone-aware time computations, captures browser timezone from the frontend, and updates test data to include timezone fields.

Changes

Cohort / File(s) Summary
Database Schema
packages/db/drizzle/0066_thick_wrecker.sql, packages/db/src/schema/auth.ts
Added timezone column (text type) to users table to store IANA timezone values.
Timezone Utilities
apps/web/src/lib/ai/core/timestamp-utils.ts, apps/web/src/lib/ai/core/__tests__/timestamp-utils.test.ts
Introduced timezone validation, normalization, time-of-day extraction, and start-of-day computation utilities. Updated buildTimestampSystemPrompt to accept timezone parameter. Added comprehensive test suite covering boundary conditions and cross-timezone behavior.
Pulse API Routes
apps/web/src/app/api/pulse/generate/route.ts, apps/web/src/app/api/pulse/cron/route.ts
Updated routes to parse client timezone, normalize it, store user timezone, and use timezone-aware time calculations for pulse generation and activity filtering. Both routes now pass userTimezone to timestamp prompt builder.
Frontend Timezone Capture
apps/web/src/components/layout/left-sidebar/Pulse.tsx
Modified handleRefresh to detect browser timezone via Intl API and include it in POST request body to pulse/generate endpoint.
Test Mock Data Updates
apps/processor/src/services/__tests__/user-validator.test.ts, apps/web/src/app/api/auth/__tests__/login-redirect.test.ts, apps/web/src/app/api/auth/__tests__/login.test.ts, apps/web/src/app/api/auth/__tests__/me.test.ts
Added timezone: null field to mockUser objects across auth and validator tests to align with extended User type.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Frontend as Pulse.tsx
    participant Browser as Browser API
    participant PulseAPI as /api/pulse/generate
    participant TimezoneUtils as Timezone Utils
    participant Database as DB

    User->>Frontend: Trigger pulse refresh
    Frontend->>Browser: Get resolved timezone
    Browser-->>Frontend: Browser timezone (e.g., "America/New_York")
    Frontend->>PulseAPI: POST with { timezone }
    PulseAPI->>TimezoneUtils: normalizeTimezone(clientTimezone)
    TimezoneUtils-->>PulseAPI: Validated timezone
    PulseAPI->>TimezoneUtils: getUserTimeOfDay(userTimezone)
    TimezoneUtils-->>PulseAPI: { hour, timeOfDay }
    PulseAPI->>TimezoneUtils: getStartOfTodayInTimezone(userTimezone)
    TimezoneUtils-->>PulseAPI: Start of day in UTC
    PulseAPI->>PulseAPI: Build timezone-aware prompt with timeOfDay
    alt Timezone differs from stored
        PulseAPI->>Database: Update user.timezone
        Database-->>PulseAPI: Success
    end
    PulseAPI->>PulseAPI: Generate pulse with timezone context
    PulseAPI-->>Frontend: Pulse response
    Frontend-->>User: Updated pulse
Loading
sequenceDiagram
    participant CronJob as Pulse Cron
    participant UserService as User Service
    participant TimezoneUtils as Timezone Utils
    participant PromptBuilder as Prompt Builder
    participant ActivityData as Activity Data

    CronJob->>UserService: Fetch all users
    UserService-->>CronJob: User list with timezone
    loop For each user
        CronJob->>TimezoneUtils: normalizeTimezone(user.timezone)
        TimezoneUtils-->>CronJob: Normalized timezone
        CronJob->>TimezoneUtils: getStartOfTodayInTimezone(timezone)
        TimezoneUtils-->>CronJob: Today's start in UTC
        CronJob->>TimezoneUtils: getUserTimeOfDay(timezone)
        TimezoneUtils-->>CronJob: { hour, timeOfDay }
        CronJob->>ActivityData: Fetch activities since today
        ActivityData-->>CronJob: Drive activities with drive scopes
        CronJob->>PromptBuilder: buildTimestampSystemPrompt(userTimezone)
        PromptBuilder-->>CronJob: Timezone-aware system prompt
        CronJob->>CronJob: Generate pulse with timezone context
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 A rabbit hops through time zones round,
No matter where the user's found,
With normalized zones and time of day,
The pulses flow the proper way! ⏰
From browser to database bright,
We celebrate this timezone right! 🌍✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective: adding timezone support to ensure AI responses use accurate time-of-day context based on user's local timezone rather than server time.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/fix-pulse-timing-X2hoB

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/pulse/generate/route.ts`:
- Around line 109-128: Validate any clientTimezone string before using or
persisting it: implement an isValidTimezone(tz: string) helper that returns true
only if new Intl.DateTimeFormat('en-US', { timeZone: tz }) does not throw (catch
RangeError). Use this helper when parsing req.json() to only set clientTimezone
if valid, fall back to storing/using user?.timezone only if that value also
passes validation, and otherwise default to 'UTC'. Apply the same validation
before calling db.update(users).set({ timezone: clientTimezone }) so you never
persist an invalid timezone.

In `@apps/web/src/lib/ai/core/timestamp-utils.ts`:
- Around line 53-78: The code constructs localMidnight with new Date(dateStr)
which is parsed in the server's local timezone; instead build the timestamp as
UTC using Date.UTC(year, month, day, 0, 0, 0) (i.e., replace new Date(dateStr)
with new Date(Date.UTC(year, month, day, 0, 0, 0))) and then use that Date
instance when calling tzFormatter.formatToParts(localMidnight) to obtain
tzOffset and compute offsetMs as before (variables: dateStr, localMidnight,
tzFormatter, tzOffset, offsetMatch); this ensures the midnight candidate is
constructed in UTC instead of the server local timezone before applying the
timezone offset to produce the correct UTC moment for the user's midnight.
🧹 Nitpick comments (4)
apps/web/src/lib/ai/core/timestamp-utils.ts (2)

23-26: Time-of-day categorization has overlapping logic.

The timeOfDay variable is initialized to 'day' but this value can never be returned because the conditions cover all hours (0-11 → morning, 12-16 → afternoon, 17+ → evening). The 'day' initialization is dead code.

While this doesn't cause a bug, consider removing the unreachable default or clarifying intent:

♻️ Suggested simplification
-  let timeOfDay = 'day';
-  if (hour < 12) timeOfDay = 'morning';
-  else if (hour < 17) timeOfDay = 'afternoon';
-  else timeOfDay = 'evening';
+  let timeOfDay: string;
+  if (hour < 12) timeOfDay = 'morning';
+  else if (hour < 17) timeOfDay = 'afternoon';
+  else timeOfDay = 'evening';

66-78: Edge case: "GMT" without offset may not match regex.

Some runtimes format UTC timezone as simply "GMT" rather than "GMT+00:00". The regex at Line 69 requires the +/- and digits, so it would fall through to the fallback at Line 81. While the fallback is correct for UTC, consider adding explicit handling or documenting this behavior.

apps/web/src/lib/ai/core/__tests__/timestamp-utils.test.ts (1)

14-26: Consider using vi.useFakeTimers() for cleaner Date mocking.

The manual Date class override pattern is verbose and requires cleanup in each test. Vitest provides built-in timer mocking that's cleaner and safer:

♻️ Suggested refactor using Vitest fake timers
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { describe, it, expect } from 'vitest';
 import {
   getUserTimeOfDay,
   getStartOfTodayInTimezone,
   buildTimestampSystemPrompt,
 } from '../timestamp-utils';

 describe('timestamp-utils', () => {
+  afterEach(() => {
+    vi.useRealTimers();
+  });
+
   describe('getUserTimeOfDay', () => {
     it('should return morning for hours before 12', () => {
-      // Mock the current time to 9 AM UTC
-      const testDate = new Date('2024-06-15T09:00:00Z');
-      const originalDate = Date;
-      global.Date = class extends originalDate {
-        constructor(...args: unknown[]) {
-          if (args.length === 0) {
-            super(testDate);
-          } else {
-            // `@ts-expect-error` - spread args
-            super(...args);
-          }
-        }
-        static now() {
-          return testDate.getTime();
-        }
-      } as DateConstructor;
+      vi.useFakeTimers();
+      vi.setSystemTime(new Date('2024-06-15T09:00:00Z'));

       const result = getUserTimeOfDay('UTC');
       expect(result.hour).toBe(9);
       expect(result.timeOfDay).toBe('morning');
-
-      global.Date = originalDate;
     });
apps/web/src/app/api/pulse/cron/route.ts (1)

311-311: Non-null assertion on driveId may cause runtime errors.

Line 311 uses activity.driveId! with a non-null assertion, but driveId could potentially be null based on the query. Consider adding a filter or guard:

♻️ Suggested defensive check
     activitiesForDiff.push({
       id: activity.id,
       timestamp: activity.timestamp,
       pageId: activity.pageId,
       resourceTitle: activity.resourceTitle,
       changeGroupId: activity.changeGroupId,
       aiConversationId: activity.aiConversationId,
       isAiGenerated: activity.isAiGenerated,
       actorEmail: activity.actorEmail,
       actorDisplayName: activity.actorName,
       content: activity.contentSnapshot ?? null,
-      driveId: activity.driveId!,
+      driveId: activity.driveId ?? '',
     });

Or filter out activities without driveId earlier in the pipeline.

Comment thread apps/web/src/app/api/pulse/generate/route.ts
Comment thread apps/web/src/lib/ai/core/timestamp-utils.ts Outdated
claude and others added 2 commits February 4, 2026 09:14
The pulse feature was using server time for all calculations, causing
issues like "good morning" messages appearing at the wrong time of day
for users in different timezones.

Changes:
- Add timezone field to users table schema
- Update buildTimestampSystemPrompt to accept timezone parameter
- Update pulse/generate route to use user timezone for time-of-day
  calculations and pass timezone to AI context
- Update Pulse component to send browser timezone on refresh
- Update pulse cron route to use stored user timezone
- Auto-save user timezone preference when provided by client

The browser timezone is detected using Intl.DateTimeFormat and stored
in the user profile for use by the cron job when generating scheduled
pulses.

https://claude.ai/code/session_014W8BwjdD8e5Jo1kYM45ey9
@2witstudios
2witstudios force-pushed the claude/fix-pulse-timing-X2hoB branch from 45af101 to 397fb3c Compare February 4, 2026 15:14

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/lib/ai/core/timestamp-utils.ts`:
- Around line 38-55: The getUserTimeOfDay function can receive "24" at midnight
because Intl.DateTimeFormat with hour12: false may use h24; update the
DateTimeFormat options in getUserTimeOfDay (where formatter is constructed) to
include hourCycle: 'h23' (after calling normalizeTimezone(timezone)) so the
formatter always returns 0–23, then continue parsing as before; no other logic
changes needed.

Comment on lines +38 to +55
export function getUserTimeOfDay(timezone?: string | null): { hour: number; timeOfDay: TimeOfDay } {
const tz = normalizeTimezone(timezone);

// Get the hour in the user's timezone
const formatter = new Intl.DateTimeFormat('en-US', {
timeZone: tz,
hour: 'numeric',
hour12: false,
});

const hour = parseInt(formatter.format(new Date()), 10);

let timeOfDay: TimeOfDay;
if (hour < 12) timeOfDay = 'morning';
else if (hour < 17) timeOfDay = 'afternoon';
else timeOfDay = 'evening';

return { hour, timeOfDay };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n apps/web/src/lib/ai/core/timestamp-utils.ts | head -60

Repository: 2witstudios/PageSpace

Length of output: 2215


🌐 Web query:

Intl.DateTimeFormat hour12 false behavior midnight "24" ECMAScript specification

💡 Result:

In ECMA-402 (Intl), hour12: false does not mandate "00" vs "24" at midnight. Instead, it forces Intl.DateTimeFormat to use the locale’s 24‑hour hour cycle ([[hourCycle24]]), which is allowed to be either:

  • "h23" = hours 0–23 (midnight formats as 00:00)
  • "h24" = hours 1–24 (midnight can format as 24:00) (402.ecma-international.org)

Which one you get is therefore locale-data / implementation dependent (within the spec’s constraints), because [[hourCycle24]] is implementation-defined but constrained to "h23" or "h24". (402.ecma-international.org)

Also, resolvedOptions().hour12 is derived from the resolved hourCycle for web-compat (true for h11/h12, false for h23/h24). (402.ecma-international.org)

Practical note: "h24" (24:00 midnight) is generally not used in common locale data, so most engines/locales will yield "h23" → 00:00. (developer.mozilla.org)

Citations:


Add explicit hourCycle: 'h23' to guard against edge-case "24" return at midnight.

Per ECMA-402, hour12: false allows implementations to use either h23 (0–23) or h24 (1–24) hour cycles. While h24 is uncommon in locale data, the spec permits it, which would cause midnight to return "24". Add the hourCycle option to enforce consistent behavior.

🔧 Suggested fix
   const formatter = new Intl.DateTimeFormat('en-US', {
     timeZone: tz,
     hour: 'numeric',
     hour12: false,
+    hourCycle: 'h23',
   });
🤖 Prompt for AI Agents
In `@apps/web/src/lib/ai/core/timestamp-utils.ts` around lines 38 - 55, The
getUserTimeOfDay function can receive "24" at midnight because
Intl.DateTimeFormat with hour12: false may use h24; update the DateTimeFormat
options in getUserTimeOfDay (where formatter is constructed) to include
hourCycle: 'h23' (after calling normalizeTimezone(timezone)) so the formatter
always returns 0–23, then continue parsing as before; no other logic changes
needed.

@2witstudios
2witstudios merged commit 6c9ec0d into master Feb 4, 2026
8 of 10 checks passed
@2witstudios
2witstudios deleted the claude/fix-pulse-timing-X2hoB branch February 6, 2026 01:27
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.

2 participants