Repository navigation
fix(frontend): seed Purchase History dates from the local calendar day - #1321
Conversation
The Purchase History From/To inputs were seeded (and snapped to a plan's
purchases) with toISOString().split('T')[0], i.e. the UTC calendar day,
while <input type="date"> shows the user's local day. Evening users west
of UTC saw tomorrow in To; morning users east of UTC saw yesterday.
Move toLocalDateInputValue and parseLocalDateInputValue out of apikeys.ts
into a shared lib/local-date.ts and use them for all four history.ts
sites. Because /api/history filters on whole UTC days, loadHistory now
widens the local From/To pair to the UTC days that cover it
(localDayRangeToUtcDays), so a local range never silently drops a
purchase made near either edge (e.g. a UTC+12 user filtering on today
previously lost this morning's purchases).
Refs LeanerCloud#51
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): REQUEST_CHANGES Blocking:
|
…y cap Widening the local From/To days to the UTC days that cover them adds a day, so any off-UTC user picking a non-leap one-year range (e.g. 2025-10-24..2026-10-24 in Los Angeles) sent a 367-day span and got "date range too large" for a range the server accepts as typed. When the widened span would exceed the cap, the From side now gives the extra day back: the request keeps the user's whole To day (usually today, the newest rows) and loses only the hours of the oldest day that fall outside the server's window. A range already over the cap, or with From after To, passes through unchanged so the server reports the same error as before. The jest sandbox cannot switch zones, so the new range tests run the module in a child node under America/Los_Angeles, Pacific/Auckland and UTC, checking one-year, leap-year and cap-boundary ranges plus a sweep of To days around DST changes and Feb 29 against the server's rule. Refs LeanerCloud#51 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Independent review (IAmJSD's agent, fresh context) of the original head: REQUEST_CHANGES Blocking:
Fixed in
|
|
Independent review (IAmJSD's agent, fresh context, given only the diff, the issue and the PR text): APPROVE Blocking:
|
|
Head |
|
Local verification at
|
|
Marked ready and merged by IAmJSD's agent, at IAmJSD's request (IAmJSD reviewed the before/after screenshots; cristim is traveling), after the independent review and local verification above, all at |
The Purchase History From/To inputs were seeded with the UTC calendar day, while
<input type="date">shows the local day, so evening users west of UTC saw tomorrow in To and morning users east of UTC saw yesterday. Because/api/historyfilters on whole UTC days, a user east of UTC filtering on today silently lost this morning's purchases. This PR seeds the inputs from the local day through a shared helper and widens the request to the UTC days that cover the local range, so no purchase near either edge is dropped.Closes #51
What changed
frontend/src/lib/local-date.ts:toLocalDateInputValueandparseLocalDateInputValuemoved out ofapikeys.ts(which now imports them), plus a newlocalDayRangeToUtcDays.frontend/src/history.ts: all four sites usetoLocalDateInputValue(initHistoryDateRangeFrom/To andsnapDateInputsToPurchasesmin/max).loadHistorypasses the inputs throughlocalDayRangeToUtcDays, so the request carries the UTC days that cover local From midnight through the end of the local To day. Empty or malformed values pass through unchanged.localDayRangeToUtcDayskeeps every range the server accepts as typed within its 366-day cap (MaxHistoryDateRangeDays,internal/api/handler_history.go; the two dates may be at most 365 days apart). When widening would push such a range one day over, the From side gives the extra day back, so the request keeps the user's whole To day (usually today) and drops only the hours of the oldest day outside the server window. A range already over the cap, or with From after To, is sent exactly as typed so the server reports the same error as on main. The cap is exported asMAX_HISTORY_RANGE_DAYS.src/__tests__/local-date.test.ts, updatedsrc/__tests__/history.test.ts, new Playwright spectests-e2e/history-local-dates.spec.ts.grep -rE "toISOString\(\)\.split\('T'\)\[0\]" frontend/srcnow only matches a doc comment in the new helper.Why this approach
Switching only the seeding to local days would have caused a regression: a UTC-8 user at 20:00 would get To = today, and the backend would end that filter at UTC midnight, hiding this evening's purchases. Widening the request on the client keeps the inputs honest and never drops rows, with no backend change. The trade-off is that results can include a few hours past one edge of the picked range, which is a visible superset rather than a silent gap.
The other
toISOString().slice(0, 10)sites (PlansearliestStartDate, calendar-date, plan-draft-dates, ladder timeline) are left alone on purpose: they model server-side UTC days and are labeled "(UTC)" in the UI.Screenshots
Before is main (
a14dc8f2), after is this branch. Both are headless Chromium at 1440x900 against the repo's mocked e2e API, with a fixed clock, the browser time zone set, and a mock that applies the backend's UTC-day filter.Verification
Run on this branch's head
a7b65a64on top of maina14dc8f2:npm run lint: 0 errors (142 warnings already on main). Typecheck and build pass.local-date.test.tscovers local vs UTC day, padding, parse round-trip, range widening, the DST-end day and pass-through, plus the 366-day cap. The jest sandbox cannot switch zones, so the cap tests run the module in a childnodeunder America/Los_Angeles, Pacific/Auckland and UTC: a non-leap one-year range (2025-10-24..2026-10-24), a leap-year range (2028-01-01..2028-12-31), a 365-day range that has room to widen fully, a one-over-cap range across Feb 29 (sent unchanged, still rejected), and a sweep of To days around DST changes in both hemispheres, Feb 29 and today with gaps of 0, 7, 364 and 365 days. Each request is checked against the server's rule (end+ 23:59:59 minusstartat most 366 x 24h) and must cover the user's whole To day. Without the cap handling, six of these fail (Los Angeles and Auckland); UTC passes either way.history.test.tspins the clock to 23:30 EDT and checks that the seeded range is local, that the request covers a purchase from earlier the same local day (the acceptance test asked for in the issue comments), and that the snap uses the local day. TheloadHistoryexpectation now uses end2024-03-02.--randomize, the touched files pass on 3 seeds. One seed failed an existingloadHistorystatus-chip test through test-order dependence; the same flake appears on maina14dc8f2with seeds 1 and 7.history-local-dates.spec.tsruns under Pacific/Auckland and America/Los_Angeles with a fixed clock. Full e2e: 159 passed.scripts/test-git-secrets-allowlist.sh: 137 PASS.Notes for review
end=2026-07-28), so edge rows can appear just outside the picked local range. The table and summary are not trimmed on the client, since trimming would make the server-computed summary disagree with the table./api/history; that would be a separate issue.apikeys.ts(commit62984e0e). This PR only moves those helpers into the shared lib.new Date(...input)found no other bare-date parses of date inputs.Overlaps
history.tsimport block: fix(frontend): status badges use four shapes and conflicting colors per status #737, feat(frontend): de-duplicate the Purchases approval surface (Approval queue repeats Purchase History rows) #183, feat(frontend): Load more on the History view, Approval Queue from all loaded pages #676 and fix(frontend): impossible/placeholder values surfaced for scheduled and pending purchases #181 also touch these imports (trivial conflicts).loadHistoryclose to the filter change here.apikeys.ts: fix(frontend): confirm dialog focuses destructive button, lacks a11y, native dialogs remain #705 and fix(frontend): raw transport and developer errors reach users instead of friendly messages #748 touch other parts of the file (fix(frontend): raw transport and developer errors reach users instead of friendly messages #748 also edits the imports). This branch removes the local helpers that were further down the file.Note for the UI owner
This changes UI code that @cristim owns. IAmJSD reviewed the before/after screenshots and asked for it to be merged; any design point you disagree with can be a follow-up.
🤖 Generated with Claude Code