3.3 KiB
The day boundary is resolved from the User's timezone
Day-scoped data (DailyMetric) needs a single, unambiguous notion of "which day". We resolve it by converting an instant into the User's IANA timezone and taking the local date, rather than using the offset carried on logged_at or a UTC date.
Considered Options
- Bare local date from
logged_at— whatget_calendardid originally. No server-side authority: two clients in different offsets disagree about the same instant's date. - UTC date — unambiguous but wrong for most humans; evening entries in eastern offsets land on the following day and fail to line up with the health data describing them.
Consequences
User.timezone stops being optional in practice. Clients must set it from the platform at register/login, and requests that need a day boundary are rejected when it is unset rather than silently defaulting — silently misfiled data produces analytics that look correct and are not. Timezone must therefore validate against the real IANA database, not just check for a /.
All day grouping in the system flows through this one rule, including get_calendar and streak calculation, which previously used two different and mutually inconsistent definitions.
An imported wall clock time is the User's own wall clock
Daylio's export records 2026-08-25 and 8:00 PM and says nothing about which zone that was. The importer originally read it as UTC, so an entry logged at 8 PM in Warsaw was stored as 20:00Z and displayed at 10 PM — every imported entry off by the User's offset, silently, and worse the further from UTC they live.
A wall clock time with no zone is the User's own wall clock. It is now placed in their Timezone using the offset in force on that entry's own date, not a single offset taken from today, so a year of history spanning a clock change gets both offsets. A Warsaw import shows +01:00 in winter and +02:00 in summer, and every entry reads as the 8 PM it was.
This makes the Timezone a precondition for importing, exactly as it already is for the calendar and for DailyMetrics: an account with none is refused rather than filed an hour or ten out of place. That is worth an error in the one place a User can act on it.
Two edge cases the clocks create:
- An hour that never happened. In Warsaw the clocks jump from 02:00 to 03:00 on the last Sunday in March, so a Daylio row reading 2:30 AM that day names a time that did not exist. The entry lands an hour later rather than being dropped, with a warning — a mood that was logged is not made up, and losing it to arithmetic is worse than moving it sixty minutes.
- An hour that happened twice. In autumn 02:30 occurs twice; the earlier of the two is used. Either is defensible and the difference is an hour once a year, so the choice is recorded rather than agonised over.
A second bug the same file exposed
The Daylio adapter read its columns by position, and index 7 is note_title, not note. Every imported note was silently dropped and the always-empty title kept in its place. Columns are now looked up by header name, so a column added, removed or reordered upstream cannot quietly shift the meaning of the data — and an export missing full_date, time or mood is refused by name rather than parsed into nonsense. Where a row has both a title and a note, both are kept.