Skip to content

healthsync hardcodes America/Los_Angeles in 3 places: every reading near local midnight is filed under the wrong day for non-Pacific operators #1762

Description

@jacobo-ortiz

LIFEOS/TOOLS/healthsync/ pins every day-boundary calculation to America/Los_Angeles:

File What it pins
store.ts (dayKeyLA()) which day-file a reading lands in
store.ts (isoNowLA()) the fetched_at / lastSuccess stamp
eightsleep.ts the tz= query param sent to the Eight Sleep trends API

For any operator outside Pacific time the day key is wrong near local midnight, so readings are silently filed under the previous or next day. Nothing errors. The data is just in the wrong bucket, and HealthSync.ts then builds trend windows off those mis-keyed files.

Reproduced on a UTC-5 host on 2026-07-21: an export at 01:13 local was written to the previous day file, because 01:13 at UTC-5 is 23:13 the day before in Los Angeles. Two hours of every day land in the wrong file; for UTC+ operators the window is larger.

eightsleep.ts compounds it: the provider is asked to bucket by LA while the files are keyed by LA too, so the two agree only for LA users and the from/to window silently covers the wrong span for everyone else.

Fix

A resolveTimeZone() in store.ts taking the first usable IANA zone from:

  1. LIFEOS_HEALTH_TZ (explicit override for this subsystem)
  2. TZ (the POSIX standard, which launchd/cron operators already set)
  3. the host zone via Intl.DateTimeFormat().resolvedOptions().timeZone
  4. America/Los_Angeles (the current value, so behaviour is unchanged when nothing else resolves)

Candidates validated by constructing an Intl.DateTimeFormat and falling through on throw, so a typo in TZ degrades instead of crashing the sync. Memoised, with a reset seam for tests.

Compatibility: no call site changes

dayKeyLA and isoNowLA stay as @deprecated aliases of a renamed dayKey / isoNow. There are 12 call sites across HealthSync.ts, apple.ts, oura.ts, function.ts and eightsleep.ts; none need to change. They can be migrated whenever, or never.

The behaviour change for an operator who sets nothing is "always LA" becoming "the machine own zone", which is the intended semantics of a personal health log. Anyone who genuinely wants LA sets TZ or LIFEOS_HEALTH_TZ.

Verification

Same instant (2026-07-21T06:13:00Z) through dayKey():

LIFEOS_HEALTH_TZ day key
America/Bogota 2026-07-21
America/Los_Angeles 2026-07-20 (old behaviour preserved)
Europe/Madrid 2026-07-21
Asia/Tokyo 2026-07-21
unset 2026-07-21 (host zone)
an invalid zone string 2026-07-21 (falls through, no throw)

HealthSync.ts pull runs clean against all four providers after the change.

Related, and what this does NOT cover

Same class as the timezone work in #762 / #780 / #806, which fixed hooks/lib/time.ts and statusline-command.sh but did not reach LIFEOS/TOOLS/. Tools/FailureCapture.ts and Tools/RelationshipReflect.ts still carry the same hardcode and are not covered here.

Diff is written and tested locally (+54/-6 in store.ts, +4/-1 in eightsleep.ts). Happy to open it as a PR if that is easier than porting from the description.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions