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:
constparsed=newDate(s);if(Number.isNaN(parsed.getTime()))returnundefined;
...
returnparsed.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
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()inpackages/rest/src/import-coerce.tsends with:A cell like
2026-08-01 06:00:00carries no offset, sonew 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 theYYYY-MM-DDfast path above it is fine). The import path therefore stores a different instant depending on the deployment host'sTZ, and never consults the caller's business timezone, whichresolveExecCtxhas already resolved on this route.Measured:
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.tsopens with "This is the inverse ofexport-format.ts"), and the export writes a naiveYYYY-MM-DD HH:mm:sscell with no offset. So the natural workflow — export, edit in a spreadsheet, re-import — is only lossless when the hostTZhappens to equal the clock the export wrote in:TZ=Asia/Shanghaire-imported it as +08 and shifted every datetime by 8 hours.TZequals the business timezone (the reported deployment's case) and wrong by the difference otherwise.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.timezoneinto the import coercion and, for a naive datetime, interpret the wall clock in that zone.zonedDateStartToUtcMsinpackages/core/src/utils/datetime.tsalready does the wall-clock-to-instant direction DST-safely (Intl.DateTimeFormatoffsets, 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 trailingZ) keeps being honoured as written — it should, and the change should only affect naive cells.Scope
packages/rest/src/import-coerce.ts—parseDateCelland itscoerceValuecallerpackages/rest/src/rest-server.ts/import-prepare.tspackages/rest(a cross-day case, not a mid-day one — this shifts rows across month boundaries the same way 【缺陷】数据导出(CSV/XLSX)日期时间列硬编码按 UTC 渲染,与界面时区不一致(@objectstack/rest export-format.ts formatDate) #8373 did)Related: #8373.
Generated by Claude Code