Skip to content

Sweep toISOString().split('T')[0] -> toLocalDateInputValue across frontend date inputs #51

Open

Description

@cristim

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 / 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.
  • grep -rE "toISOString\\(\\)\\.split\\('T'\\)\\[0\\]" frontend/src returns nothing outside tests.
  • No behaviour change for users in UTC; values match local calendar for everyone else.

Related

Activity

  1. cristim commented on Jul 28, 2026

    @cristim
    MemberAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions