You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Sweep toISOString().split('T')[0] -> toLocalDateInputValue across frontend date inputs #51
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:243-244 — minDate / maxDate of the analytics range
frontend/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.ts switches from the local helper to the shared one.
Reviewed commit: be11bdcb5. Note: origin/main moved to 3e9660d06 during the review; re-verify against current main before 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, the history.ts sites listed in this issue are now:
frontend/src/history.ts:202 and :205 (initHistoryDateRange), was :179 / :182
frontend/src/history.ts:266-267 (the analytics minDate / maxDate), was :243-244
frontend/src/apikeys.ts:359 (unchanged)
The acceptance criterion grep -rE "toISOString\(\)\.split\('T'\)\[0\]" frontend/src is 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.ts that 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:
A user in UTC+13 (NZDT) opens Purchase History at 10:00 local on 2026-07-28. UTC is still 2026-07-27T21:00, so the To field is seeded 2026-07-27. Purchases executed earlier that same morning fall outside the default range and simply do not appear. The operator concludes the scheduled purchase never ran, and there is nothing on screen indicating a filter excluded it.
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/low to severity/medium and re-checking the priority accordingly; the apikeys.ts half 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 does new Date(expiresAtInput) on the bare YYYY-MM-DD string 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 :192 rejects 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.ts submit parses the picker value with parseDateInput, not bare new Date(...).
grep -rE "new Date\([^)]*[Ii]nput" frontend/src reviewed for other bare-date parses of <input type="date"> values.
Regression test for history.ts asserts the seeded range includes a purchase executed earlier the same local day, run under a non-UTC TZ. 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 other toISOString-for-a-date-input sites were found beyond the ones already listed here.
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.