fix(home): give the calendar widget and the header one day boundary - #1953
Merged
Conversation
The Home board built "today" twice. `todayCalendarRange` took the local date components and pinned them to `T00:00:00.000Z`; the header's `todayRangeIso` used a local `setHours(0, 0, 0, 0)`. At every non-zero UTC offset a band of hours landed in one window and not the other, so the header counted events the widget had no row for. Both now call `localDayRange`, one helper over the `YYYY-MM-DD` string `useToday` already hands them. Local midnight is the right boundary: the main process projects every date-only source at local midnight and reads the window back as local dates, so a UTC-pinned window made `getLocalDueDateRange` span two local days. The ranges being identical makes the query key identical, so the board fetches the day once instead of twice.
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
This was referenced Sep 2, 2026
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The Home board built "today" twice and the two answers disagreed.
todayCalendarRangetook the local date components and pinned them toT00:00:00.000Z; the header'stodayRangeIsoused a localsetHours(0, 0, 0, 0)with an inclusive23:59:59.999end. At any non-zero UTC offset a band of hours fell inside one window and outside the other, so the header advertised a count the widget below it had no row for. At UTC-7 the gap is the local evening, at UTC+3 it is local 00:00 to 03:00.Both sides now call one helper,
localDayRange(date)inapps/desktop/src/renderer/src/lib/local-day-range.ts, over theYYYY-MM-DDstringuseTodayalready hands them. The two old implementations are deleted rather than adjusted to agree, which is the point of the ticket: two agreeing copies drift again. The header also moves fromnew Date()touseToday(), so its count rolls over at midnight alongside the widget instead of waiting for the next re-render.Local midnight is the correct boundary, and not only because "today" is the user's wall clock. The main process already treats the window as local:
toLocalInstantandtoLocalAllDayEnd(apps/desktop/src/main/calendar/projection.ts:34-43) project every date-only source at local midnight, andgetLocalDueDateRange(:45-59) collapses the requested instants back to local calendar dates to matchtasks.due_date. Under the UTC-pinned window that collapse spanned two local days: at UTC-7 today's widget asked for yesterday's and today's task due dates, at UTC+9 for today's and tomorrow's. Local midnight makes it exact. The end is built asnew Date(y, m, d + 1)rather thanstart + 24h, so a 23- or 25-hour DST day resolves through the platform; that is the same constructionuse-today.tsandtoLocalAllDayEndalready use.The ranges being identical makes the react-query key identical, so the board now fetches the day once instead of twice, and one
useCalendarChangeEventsinvalidation covers both. The stale comment claiming a shared key is gone with the function that carried it; the E2E spec's "pinned to UTC hours" note is corrected too.Two behaviour changes worth a reviewer's eye, both in negative-offset zones, both concerning all-day rows stored at UTC midnight:
NULLend_atwas included before and is dropped now, because the events predicate falls back tocoalesce(end_at, start_at)(projection.ts:139,:441) and00:00Zsorts before a local-midnight window start. Google's API effectively always sendsend.date, whose exclusive next-day value keeps normal all-day events in, so this is a narrow tail. Filed separately rather than widening the projection SQL here.toLocalDateKey.No schema, IPC contract, settings shape or vault file format changed.
packages/contractsis untouched, so noipc:generate/ipc:checkrun was needed. Nothing on disk is read or written differently and existing installs need no migration; the change is which instants the renderer asks for.Closes #1920
Release note
The Home board's event count and its calendar widget now agree on which day is today, so a late-evening or just-after-midnight event is no longer counted in the header while missing from the widget.
Test plan
The bug reproduces first. Against
origin/main's four source files, with the new tests in place:The three-hour split is this machine at UTC+3. The local-evening case passes here and fails west of UTC, which is the asymmetry the issue describes from the other side. After the fix all four pass.
Timezone coverage.
process.env.TZis inert in the renderer vitest project: it runs on thethreadspool, whereprocess.envis a worker copy that never reachestzset. A probe across four zones read back the same-180offset every time, anduse-today.test.tsxdocuments the same wall. Because a CI runner sits at UTC, where the two old windows differ only in the final millisecond, an ambient-zone test alone would go green on main in CI. Solocal-day-range.test.tsruns the helper in a childnodewith a realTZ(Node strips the types on import, which is why the module is deliberately import-free, with a test guarding that). That covers UTC,America/Los_Angeles(-7),Pacific/Marquesas(-9:30),Asia/Kolkata(+5:30),Australia/Eucla(+8:45) andPacific/Kiritimati(+14), plus three DST transition days asserted by day length: 23h and 25h in Los Angeles, 24.5h inAustralia/Lord_Howe. Host-zone property tests sit alongside it so the file still says something on a machine the matrix cannot enumerate.Commands run from the worktree on Node v24.16.0 (
process.versions.modules137):pnpm lint— exit 0.pnpm --filter @memry/desktop typecheck:web/typecheck:node/typecheck:test— all exit 0. No file was added to anyexcludelist; both new test files compile.pnpm check:architecture— passed.pnpm check:contracts— passed.vitest --project rendererovercomponents/home,components/calendar,components/journal,lib,hooks,pages— 313 files, 4854 passed, 0 failed.git diff --check— clean.npx react-doctor@latest .— the one hit in the new file isanti-slop/no-known-value-wideningon the declared return type, a repo-wide pattern with 640 existing instances and the same signature the deleted function had. Net neutral.pnpm docs:impact --base origin/main --strict—docs changed on this branch.pnpm docs:build— build complete.pnpm --filter @memry/desktop i18n:check— not run; no user-facing string changed, only a docs paragraph and comments.Tests added.
apps/desktop/src/renderer/src/lib/local-day-range.test.ts(17 cases, the zone matrix and the host-zone properties) andapps/desktop/src/renderer/src/components/home/home-board-day-range.test.tsx(4 cases, mounting the realHomeHeaderand the realCalendarWidget: the two request an identical window, one client fetches the day once, and a local-evening and a local-00:30 event each appear in the widget and in the header's "1 event").calendar-widget.test.tsxhad three cases pinning the old UTC literals; they now assert throughlocalDayRange, which keeps their real subject, the midnight rollover, and makes them zone-independent.E2E, not run here.
apps/desktop/tests/e2e/home-calendar-widget-refresh.e2e.tsis edited but not executed; E2E needs an Electron native rebuild, which runs serially under the coordinator. Its seed helper wastodayUtcAt, a UTC hour on the local date, which only landed inside the widget's window because the window was UTC-pinned too. It is nowtodayLocalAt, a local wall-clock hour, which is inside the local day at every offset. All three cases should pass unchanged: the rows are the same, only the seeded instants moved.