Skip to content

Bulk import reads a naive datetime cell in the process-local timezone, so an export/edit/re-import round trip shifts the instant #8485

Description

@os-zhuang

Found while implementing #8373 (export renders datetime cells in the business timezone); filed rather than fixed there — it is the import side of the same seam and outside that card's declared surface.

What

parseDateCell() in packages/rest/src/import-coerce.ts ends with:

const parsed = new Date(s);
if (Number.isNaN(parsed.getTime())) return undefined;
...
return parsed.toISOString();

A cell like 2026-08-01 06:00:00 carries no offset, so new Date(s) interprets it in the process timezone (ECMAScript: a date-time form with no offset is local time; date-only forms are UTC — which is why the YYYY-MM-DD fast path above it is fine). The import path therefore stores a different instant depending on the deployment host's TZ, and never consults the caller's business timezone, which resolveExecCtx has already resolved on this route.

Measured:

TZ=Asia/Shanghai node -e "console.log(new Date('2026-08-01 06:00:00').toISOString())"
# 2026-07-31T22:00:00.000Z
TZ=UTC           node -e "console.log(new Date('2026-08-01 06:00:00').toISOString())"
# 2026-08-01T06:00:00.000Z

Same file, same tenant, same cell — eight hours apart, decided by a host setting nobody authoring the spreadsheet can see.

Why it matters

Export and import are advertised as inverses (import-coerce.ts opens with "This is the inverse of export-format.ts"), and the export writes a naive YYYY-MM-DD HH:mm:ss cell with no offset. So the natural workflow — export, edit in a spreadsheet, re-import — is only lossless when the host TZ happens to equal the clock the export wrote in:

Either way the correctness of the round trip rests on a host env var rather than on the tenant's configured timezone. The defect predates #8373; #8373 changes which deployments happen to be lucky.

Suggested direction

Read the cell in the business timezone: thread ExecutionContext.timezone into the import coercion and, for a naive datetime, interpret the wall clock in that zone. zonedDateStartToUtcMs in packages/core/src/utils/datetime.ts already does the wall-clock-to-instant direction DST-safely (Intl.DateTimeFormat offsets, never hand-rolled arithmetic) and is the natural primitive to generalise from date-only to date-time. Fall back to the current behaviour when no timezone is resolved.

Worth deciding explicitly as part of the fix: whether a cell that does carry an offset (2026-08-01T06:00:00+08:00, or a trailing Z) keeps being honoured as written — it should, and the change should only affect naive cells.

Scope

Related: #8373.


Generated by Claude Code

Activity

  1. added theissue type on Aug 13, 2026
  2. hotlong commented on Aug 13, 2026

    @hotlong
    Contributor

    Triage: lands in packages/rest (import-coerce.ts + import route call sites) ⇒ domain:cli. Queued as Bug: export and import are advertised as inverses, and the naive-datetime read is decided by the host TZ rather than the tenant's resolved business timezone — measured 8-hour shift on a round trip. The suggested primitive (zonedDateStartToUtcMs) and the offset-carrying-cell carve-out are in the body. Sibling of #8484 (filename half, same seam, same lane) — small enough to batch if the seat confirms disjoint-or-serial file handling, since both touch packages/rest.

    Size/model suggestion: M.

    本评论来自分诊座位 Routine。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions