Skip to content

feat: add recording timezone and UTC offset to sleep entries - #2070

Merged
CodeWithCJ merged 3 commits into
CodeWithCJ:mainfrom
apedley:fix/2033-track-sleep-timezones
Aug 10, 2026
Merged

feat: add recording timezone and UTC offset to sleep entries#2070
CodeWithCJ merged 3 commits into
CodeWithCJ:mainfrom
apedley:fix/2033-track-sleep-timezones

Conversation

@apedley

@apedley apedley commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Tip

Help us review and merge your PR faster!
Please ensure you have completed the Checklist below.
For Frontend changes, please run pnpm run validate to check for any errors.
PRs that include tests and clear screenshots are highly preferred!
Note: AI-generated descriptions must be manually edited for conciseness. Do not paste raw AI summaries.

Description

What problem does this PR solve?
Sleep bed/wake times were stored as UTC instants without data on how they were recorded so every display falls back to the user's current time zone. Travel across time zones relabeled history.

How did you implement the solution?

Add two columns to sleep_entries, record_timezone (IANA) and record_utc_offset_minutes. When importing the system will fill in the timezone based on the data provided and the priority: IANA timezone, then the offset, and finally use the users saved time zone preference. Entry date bucketing is untouched and there's no backfill. This will only apply to new sleep entries.

Known limitations to be addressed in a follow up PR:

  • fixed offset can't follow daylight savings time inside a session. So in november when clocks fall back things will be 1hr off
  • Sleep timeline editor still shows browser local times.

Linked Issue: Closes #2033

How to Test

  1. Check out branch
  2. Import or manually add sleep data
  3. Change time zones
  4. Verify the times still display correctly

PR Type

  • Issue (bug fix)
  • New Feature
  • Refactor
  • Documentation

Checklist

All PRs:

  • [MANDATORY - ALL] Integrity & License: I certify this is my own work, free of malicious code, and I agree to the License terms.

New features only:

  • [MANDATORY for new feature] Alignment: I have raised a GitHub issue and it was reviewed/approved by maintainers or it was approved on Discord.

Frontend changes (SparkyFitnessFrontend/):

  • [MANDATORY for Frontend changes] Quality: I have run pnpm run validate and it passes.
  • [MANDATORY for Frontend changes] Translations: I have only updated the English (en) translation file.

Backend changes (SparkyFitnessServer/):

  • [MANDATORY for Backend changes] Code Quality: I have run typecheck, lint, and tests. New files use TypeScript, new endpoints have Zod schemas, and new endpoints include tests.
  • [MANDATORY for Backend changes] Database Security: I have updated rls_policies.sql for any new user-specific tables.

UI changes (components, screens, pages):

  • [MANDATORY for UI changes] Screenshots: I have attached Before/After screenshots below.

Mobile changes (SparkyFitnessMobile/):

  • [MANDATORY for Mobile changes] Tested on device or emulator: I have verified the changes work on iOS or Android.

Screenshots

Click to expand

Before

before

After

after

Notes for Reviewers

Optional — use this for anything that doesn't fit above: known tradeoffs, areas you'd like specific feedback on, qustions you have or context that helps reviewers.

Summary by CodeRabbit

  • New Features

    • Sleep records now preserve their original timezone or UTC offset when available.
    • Imported and manually entered sleep data retains recording-time metadata.
    • Sleep reports, charts, timelines, and analytics display times in the recorded timezone, with profile timezone fallback.
    • Sleep imports support optional recording timezone and UTC offset fields.
  • Bug Fixes

    • Improved daylight-saving and cross-midnight sleep handling.
    • Prevented missing timestamps from appearing as incorrect chart points.
    • Invalid fractional UTC offsets are now rejected during import.

@github-actions github-actions Bot added backend bug Something isn't working enhancement New feature or request frontend labels Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

PR Validation Results

Change Detection

  • 🖥️ Frontend changes detected
  • ⚙️ Backend changes detected

✅ All checks passed. Thank you!

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 78ace3e9-dc9e-4478-8ff5-21d68c7eb47c

📥 Commits

Reviewing files that changed from the base of the PR and between e45bde4 and d8b6a32.

📒 Files selected for processing (3)
  • SparkyFitnessServer/tests/measurementImportRoutes.test.ts
  • SparkyFitnessServer/tests/timezone.test.ts
  • shared/src/utils/timezone.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • SparkyFitnessServer/tests/timezone.test.ts
  • shared/src/utils/timezone.ts

📝 Walkthrough

Walkthrough

Sleep entries now preserve recording timezone metadata across imports, manual entry, integrations, storage, analytics, and frontend displays. Shared utilities provide IANA and fixed-offset fallback behavior.

Changes

Sleep timezone support

Layer / File(s) Summary
Timezone contracts and conversion utilities
shared/src/schemas/database/SleepEntries.zod.ts, shared/src/utils/timezone.ts, SparkyFitnessFrontend/src/types.ts, SparkyFitnessFrontend/src/utils/timeFormatters.ts, SparkyFitnessServer/schemas/measurementSchemas.ts
Schemas and types include recording timezone fields. Shared utilities resolve IANA zones or UTC offsets, convert instants, and parse explicit ISO offsets.
Server storage and source ingestion
SparkyFitnessServer/db/..., SparkyFitnessServer/models/..., SparkyFitnessServer/routes/..., SparkyFitnessServer/integrations/..., SparkyFitnessServer/services/..., SparkyFitnessGarmin/routes.py
Sleep sources derive or accept recording-zone metadata. Repositories persist and return it. Omitted update fields preserve existing values.
Frontend capture and reporting
SparkyFitnessFrontend/src/pages/CheckIn/..., SparkyFitnessFrontend/src/pages/Reports/..., SparkyFitnessFrontend/src/utils/..., SparkyFitnessFrontend/src/constants/...
New entries record the browser timezone. Edited times replace stale metadata. Reports and charts format values using each entry’s recording zone with profile fallback.
Validation coverage
SparkyFitnessFrontend/src/tests/..., SparkyFitnessServer/tests/...
Tests cover imports, timezone conversion, DST gaps, source integrations, persistence bindings, route validation, metadata preservation, and chronotype calculations.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SleepSource
  participant SleepProcessing
  participant SleepRepository
  participant Database
  participant SleepReports
  SleepSource->>SleepProcessing: provide timestamps and recording-zone data
  SleepProcessing->>SleepRepository: persist recording timezone or UTC offset
  SleepRepository->>Database: insert or update sleep metadata
  Database-->>SleepRepository: return stored metadata
  SleepRepository-->>SleepReports: provide entries with recording-zone fields
  SleepReports->>SleepReports: resolve zone and format sleep times
Loading

Possibly related PRs

Suggested reviewers: codewithcj

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: adding recording timezone and UTC offset metadata to sleep entries.
Description check ✅ Passed The description covers the problem, implementation, testing steps, linked issue, limitations, type, and checklist; screenshot placeholders remain incomplete.
Linked Issues check ✅ Passed The changes address issue #2033 by preserving recording timezone metadata and using it for sleep displays, charts, and chronotype calculations.
Out of Scope Changes check ✅ Passed The database, integrations, backend services, frontend displays, utilities, and tests are directly related to recording timezone support for sleep data.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@apedley

apedley commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeWithCJ I was unable to verify this will work for Withings. Do you have mock data for them?

@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: 8

🧹 Nitpick comments (1)
SparkyFitnessServer/services/sleepScienceService.ts (1)

87-107: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace any with a sleep-history input type.

getWakeHour and getSleepHour accept entry: any. Define a narrow type for sleepStartTimestampGMT, sleepEndTimestampGMT, record_timezone, and record_utc_offset_minutes. This preserves the new recording-zone behavior and prevents unchecked property access.

As per coding guidelines, SparkyFitnessServer/{models,integrations,services}/**/*.ts must avoid any declarations in services.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessServer/services/sleepScienceService.ts` around lines 87 - 107,
Replace the any parameter types in getWakeHour and getSleepHour with a shared
narrow sleep-history input type containing sleepStartTimestampGMT,
sleepEndTimestampGMT, record_timezone, and record_utc_offset_minutes with
appropriate nullable or optional value types. Keep the existing
resolveRecordZone and timestamp validation behavior unchanged, and remove the
explicit-any suppression once both helpers are typed.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shared/src/schemas/database/SleepEntries.zod.ts`:
- Around line 42-43: Update all three record_utc_offset_minutes fields across
the write schemas to validate whole-minute values with z.number().int(),
including a dedicated integer validator for
ImportHealthDataItemSchema.record_utc_offset_minutes. Leave the shared
nullableOptionalLegacyNumber unchanged because it supports fractional
measurements.

In `@shared/src/utils/timezone.ts`:
- Around line 257-261: Restrict offset metadata in the shared timezone resolver
to integer minute values from -840 through 840 before returning the "offset"
result; otherwise fall back to the existing profile-timezone behavior. Also
update the calculated-offset acceptance condition in
SparkyFitnessGarmin/routes.py lines 579-584 to require -840 <= offset_min <=
840, preserving the existing handling for invalid offsets.
- Around line 286-294: Update the offset parsing logic in the timezone utility
to validate the complete datetime structure before extracting the trailing
offset, so malformed inputs such as “T+05:30” return null. Preserve valid offset
handling and add regression coverage for malformed strings containing +HH:MM
suffixes.

In `@SparkyFitnessFrontend/src/tests/components/SleepEntrySection.test.tsx`:
- Around line 1-3: Move the SleepEntrySection test into the
src/tests/pages/CheckIn/ subtree, preserving the existing test coverage. Replace
the direct `@testing-library/react` render import with the shared render exported
by test-utils.tsx, while retaining the other required testing imports.

In `@SparkyFitnessFrontend/src/tests/utils/formatTimeInZoneDstGap.test.ts`:
- Around line 1-4: Update the test suite setup around the process.env['TZ']
assignment to capture the existing TZ value, set America/New_York in beforeAll,
and restore the captured value or delete the variable in afterAll. Ensure
cleanup runs after the suite so later Jest tests do not inherit the forced
timezone.

In `@SparkyFitnessFrontend/src/utils/healthDataImport.ts`:
- Around line 216-224: Validate recording-zone metadata before persistence
across SparkyFitnessFrontend/src/utils/healthDataImport.ts:216-224,
SparkyFitnessServer/routes/sleepRoutes.ts:135-152 and :360-376, and the import
schema; require record_timezone to be a valid IANA zone and
record_utc_offset_minutes to be an integer whole-minute value within the
supported range. Update readNumberCell and nullableOptionalLegacyNumber
validation as needed, and ensure both sleep create/update handlers and
SparkyFitnessServer/integrations/healthData/healthDataRoutes.ts reject invalid
raw zone fields before forwarding them to persistence.

In `@SparkyFitnessServer/tests/chatbotToolsCheckin.test.ts`:
- Around line 645-661: Extend the test “leaves the stamp unset for Z-suffixed
and generated timestamps” with a separate invocation of
sparky_manage_checkin.execute for log_sleep that omits both bedtime and
wake_time, then assert the resulting processSleepEntry payload has
record_utc_offset_minutes undefined. Keep the existing Z-suffixed timestamp
assertion intact.

In `@SparkyFitnessServer/tests/sleepRepository.placeholders.test.ts`:
- Around line 73-79: Replace the any-typed mockClient, queryCalls call
arguments, and all corresponding values types in sleep repository tests with
explicit mock/query types and unknown where values are not known. Remove every
`@typescript-eslint/no-explicit-any` suppression at the referenced locations while
preserving the existing mock behavior and query-call assertions.

---

Nitpick comments:
In `@SparkyFitnessServer/services/sleepScienceService.ts`:
- Around line 87-107: Replace the any parameter types in getWakeHour and
getSleepHour with a shared narrow sleep-history input type containing
sleepStartTimestampGMT, sleepEndTimestampGMT, record_timezone, and
record_utc_offset_minutes with appropriate nullable or optional value types.
Keep the existing resolveRecordZone and timestamp validation behavior unchanged,
and remove the explicit-any suppression once both helpers are typed.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ac6a6c31-80e2-4ab1-b27e-82a4ea63409b

📥 Commits

Reviewing files that changed from the base of the PR and between 2241581 and d352896.

📒 Files selected for processing (40)
  • SparkyFitnessFrontend/src/constants/healthDataImport.ts
  • SparkyFitnessFrontend/src/pages/CheckIn/SleepEntrySection.tsx
  • SparkyFitnessFrontend/src/pages/Reports/SleepAnalyticsCharts.tsx
  • SparkyFitnessFrontend/src/pages/Reports/SleepAnalyticsTable.tsx
  • SparkyFitnessFrontend/src/pages/Reports/SleepReport.tsx
  • SparkyFitnessFrontend/src/pages/Reports/SleepStageChart.tsx
  • SparkyFitnessFrontend/src/tests/components/SleepEntrySection.test.tsx
  • SparkyFitnessFrontend/src/tests/utils/formatTimeInZoneDstGap.test.ts
  • SparkyFitnessFrontend/src/tests/utils/healthDataImport.test.ts
  • SparkyFitnessFrontend/src/tests/utils/timeFormatters.test.ts
  • SparkyFitnessFrontend/src/types.ts
  • SparkyFitnessFrontend/src/utils/healthDataImport.ts
  • SparkyFitnessFrontend/src/utils/timeFormatters.ts
  • SparkyFitnessGarmin/routes.py
  • SparkyFitnessServer/ai/tools/checkinTools.ts
  • SparkyFitnessServer/config/swagger.ts
  • SparkyFitnessServer/db/migrations/20260807000000_add_record_timezone_to_sleep_entries.sql
  • SparkyFitnessServer/integrations/fitbit/fitbitDataProcessor.ts
  • SparkyFitnessServer/integrations/googlehealth/googleHealthDataProcessor.ts
  • SparkyFitnessServer/integrations/healthData/healthDataRoutes.ts
  • SparkyFitnessServer/integrations/oura/ouraDataProcessor.ts
  • SparkyFitnessServer/integrations/polar/polarDataProcessor.ts
  • SparkyFitnessServer/integrations/withings/withingsDataProcessor.ts
  • SparkyFitnessServer/models/sleepRepository.ts
  • SparkyFitnessServer/models/sleepScienceRepository.ts
  • SparkyFitnessServer/routes/sleepRoutes.ts
  • SparkyFitnessServer/services/fitbitService.ts
  • SparkyFitnessServer/services/healthDataHandlers.ts
  • SparkyFitnessServer/services/sleepScienceService.ts
  • SparkyFitnessServer/tests/chatbotToolsCheckin.test.ts
  • SparkyFitnessServer/tests/fitbitDataProcessor.test.ts
  • SparkyFitnessServer/tests/googleHealthDataProcessor.test.ts
  • SparkyFitnessServer/tests/measurementService.healthConnectSleepStages.test.ts
  • SparkyFitnessServer/tests/ouraDataProcessor.test.ts
  • SparkyFitnessServer/tests/polarDataProcessor.test.ts
  • SparkyFitnessServer/tests/sleepRepository.placeholders.test.ts
  • SparkyFitnessServer/tests/sleepScienceService.recordZone.test.ts
  • SparkyFitnessServer/tests/timezone.test.ts
  • shared/src/schemas/database/SleepEntries.zod.ts
  • shared/src/utils/timezone.ts

Comment thread shared/src/schemas/database/SleepEntries.zod.ts
Comment thread shared/src/utils/timezone.ts
Comment thread shared/src/utils/timezone.ts Outdated
Comment thread SparkyFitnessFrontend/src/utils/healthDataImport.ts
Comment thread SparkyFitnessServer/tests/chatbotToolsCheckin.test.ts
Comment thread SparkyFitnessServer/tests/sleepRepository.placeholders.test.ts Outdated

@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

🧹 Nitpick comments (1)
SparkyFitnessServer/schemas/measurementSchemas.ts (1)

58-72: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add regression tests for legacy integer parsing.

Cover whole-number numbers and strings, null, empty strings, and fractional numbers and numeric strings. This verifies that legacy coercion remains compatible while fractional UTC offsets fail validation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@SparkyFitnessServer/schemas/measurementSchemas.ts` around lines 58 - 72, Add
regression tests for nullableOptionalLegacyInteger covering whole-number numeric
and string inputs, null, and empty strings as accepted values, plus fractional
numeric and numeric-string inputs as rejected values. Exercise the schema
directly and verify fractional UTC offsets fail validation while preserving
legacy coercion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shared/src/utils/timezone.ts`:
- Around line 299-303: Update the offset parsing logic near the hours/minutes
validation to return null when the parsed sign is "-" and total equals zero,
covering both -00:00 and -0000 while preserving valid nonzero offsets. Add
regression tests for both input forms and verify resolveRecordZone uses the
profile-timezone fallback.

In `@SparkyFitnessServer/tests/measurementImportRoutes.test.ts`:
- Around line 204-218: The SleepSession import test does not verify that
record_utc_offset_minutes is forwarded unchanged. Extend the assertions after
the processHealthData call in the post test to inspect its argument and confirm
the forwarded item retains record_utc_offset_minutes: -300, while preserving the
existing status and call-count checks.

---

Nitpick comments:
In `@SparkyFitnessServer/schemas/measurementSchemas.ts`:
- Around line 58-72: Add regression tests for nullableOptionalLegacyInteger
covering whole-number numeric and string inputs, null, and empty strings as
accepted values, plus fractional numeric and numeric-string inputs as rejected
values. Exercise the schema directly and verify fractional UTC offsets fail
validation while preserving legacy coercion behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7fb223d5-7349-4717-bce3-f5ecd5e07625

📥 Commits

Reviewing files that changed from the base of the PR and between d352896 and e45bde4.

📒 Files selected for processing (8)
  • SparkyFitnessFrontend/src/tests/utils/formatTimeInZoneDstGap.test.ts
  • SparkyFitnessGarmin/routes.py
  • SparkyFitnessServer/schemas/measurementSchemas.ts
  • SparkyFitnessServer/services/sleepScienceService.ts
  • SparkyFitnessServer/tests/measurementImportRoutes.test.ts
  • SparkyFitnessServer/tests/sleepRepository.placeholders.test.ts
  • SparkyFitnessServer/tests/timezone.test.ts
  • shared/src/utils/timezone.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • SparkyFitnessFrontend/src/tests/utils/formatTimeInZoneDstGap.test.ts
  • SparkyFitnessServer/tests/timezone.test.ts
  • SparkyFitnessServer/services/sleepScienceService.ts
  • SparkyFitnessGarmin/routes.py
  • SparkyFitnessServer/tests/sleepRepository.placeholders.test.ts

Comment thread shared/src/utils/timezone.ts
Comment thread SparkyFitnessServer/tests/measurementImportRoutes.test.ts
@CodeWithCJ
CodeWithCJ merged commit 9495962 into CodeWithCJ:main Aug 10, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend bug Something isn't working enhancement New feature or request frontend

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Sleep data doesn't take into account the different timezones

2 participants