Repository navigation
Description
Problem
Several places use someDate.toISOString().split('T')[0] to produce a YYYY-MM-DD value for <input type="date"> (or for date filters). toISOString() returns the UTC calendar date, but <input type="date"> is rendered in the user's local calendar. The two diverge by a day for any user whose local clock has crossed midnight UTC (so e.g. a US west-coast user after ~4-5pm local sees defaults / mins resolve to "tomorrow" instead of "today").
PR LeanerCloud/cloud-commitments-cli#1258 fixed the two occurrences in frontend/src/plans.ts::openAddPurchasesModal via a new toLocalDateInputValue(Date) helper. The same pattern survives in:
frontend/src/history.ts:179—startInput.value = start.toISOString().split('T')[0]frontend/src/history.ts:182—endInput.value = end.toISOString().split('T')[0]frontend/src/history.ts:243-244—minDate/maxDateof the analytics rangefrontend/src/apikeys.ts:359— default expiry date for new API keys
Impact
- Analytics filter defaults shift by a day for evening-hours users west of UTC. Visible as "I clicked default ‘last 30 days’ at 11pm and got 31 days back."
- API-key default expiry is off by a day on creation.
Severity is low — values are pre-fills, the user can fix them — but the inconsistency with the plans.ts fix invites future contributors to copy the wrong pattern.
Suggested fix
Promote toLocalDateInputValue from plans.ts to frontend/src/utils.ts (or a small dates.ts neighbour) and replace the four call sites above. Regression tests should mirror the QA 5.6 test in plans-range-validation.test.ts.
Acceptance criteria
- Single shared helper exists in
frontend/src/utils.ts(or similar). - All four call sites use it;
plans.tsswitches from the local helper to the shared one. -
grep -rE "toISOString\\(\\)\\.split\\('T'\\)\\[0\\]" frontend/srcreturns nothing outside tests. - No behaviour change for users in UTC; values match local calendar for everyone else.
Related
- Same root cause as the in-scope timezone fix on PR fix(ui): enforce future-only planned start date + widen Configure Purchase Term field cloud-commitments-cli#1258 (
fix/qa437-purchase-ui-polish, commit43c395440).
Activity
cristim commentedon Jul 28, 2026
Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have been fixed or moved.The 2026-07-28 full-repo frontend review re-derived this sweep independently. Adding evidence rather than filing duplicates. Three things to fold in: updated line numbers, a worse failure mode than the issue currently records, and one call site that is outside this issue's stated scope.
Line numbers have moved
On
3e9660d06, thehistory.tssites listed in this issue are now:frontend/src/history.ts:202and:205(initHistoryDateRange), was:179/:182frontend/src/history.ts:266-267(the analyticsminDate/maxDate), was:243-244frontend/src/apikeys.ts:359(unchanged)The acceptance criterion
grep -rE "toISOString\(\)\.split\('T'\)\[0\]" frontend/srcis still the right check; use it rather than the line numbers.The impact is worse than "pre-fills the user can fix"
The issue currently rates this low on the grounds that the values are pre-fills. For
history.tsthat under-states it, because the pre-fill is a filter bound on a table, and a wrong bound silently removes rows rather than displaying something visibly odd:The symmetric case for UTC-11 users loses a day at the From bound instead. Either way the user is looking at a table that is silently incomplete on the exact screen they use to confirm whether money was spent. That is a different failure class from "I asked for 30 days and got 31" - it is a false negative on a purchase audit, and the natural next action is to re-run the purchase.
Suggest raising this from
severity/lowtoseverity/mediumand re-checking the priority accordingly; theapikeys.tshalf genuinely is low.New: a call site outside this issue's current scope
This issue covers the four output sites (
toISOString().split('T')[0]). The API-key flow has a matching input bug that the sweep as written will not catch:frontend/src/apikeys.ts:191-195: submit doesnew Date(expiresAtInput)on the bareYYYY-MM-DDstring from the picker. JS parses a bare date string as UTC midnight, not local.utils.ts:parseDateInput(:41-74) exists specifically to parse bare calendar dates as local midnight, and is not used here. Its docstring describes exactly this bug class.Failure scenario: a user in UTC-8 picks today's date in the picker. UTC midnight of that day is roughly 16 hours in the past, so the
expiresAt <= new Date()validation at:192rejects it with "Expiration date must be in the future" for a date the user reads as still ahead of them. Conversely a UTC+13 user successfully creates a key that expires most of a day before the calendar date they chose, with no warning.So the API-key flow is wrong on both ends: the default is seeded from the UTC calendar day (
:359, already in this issue) and the submitted value is parsed as UTC midnight (:191-195, new). Fixing only the first leaves the rejection bug live.Suggested amendment to the acceptance criteria
Add alongside the existing four:
apikeys.tssubmit parses the picker value withparseDateInput, not barenew Date(...).grep -rE "new Date\([^)]*[Ii]nput" frontend/srcreviewed for other bare-date parses of<input type="date">values.history.tsasserts the seeded range includes a purchase executed earlier the same local day, run under a non-UTCTZ. A test that only asserts the string value passes for a user in UTC and proves nothing.Context
From a review of the frontend view modules (
recommendations.ts,settings.ts,plans.ts,riexchange.ts,history.ts,dashboard.ts,apikeys.ts,users/*) that traced every date-to-string and string-to-date conversion in scope. No othertoISOString-for-a-date-input sites were found beyond the ones already listed here.IAmJSD commentedon Oct 24, 2026
Claiming: working on this
fix(frontend): keep widened history ranges within the server's 366-da…
fix(frontend): seed Purchase History dates from the local calendar day (