Uh oh!
There was an error while loading. Please reload this page.
fix(rest): stamp the export download filename in the business timezone - #8729
Conversation
#8484) exportContentDisposition built the -YYYYMMDD-HHMMSS stamp from process-local getters, which read the deployment host's TZ rather than the caller's business timezone. After #8373 moved the export's contents onto the business timezone, the filename was the last export surface still on the host clock, so the name and the rows inside disagreed whenever TZ was not the business zone. The route had already resolved ExecutionContext.timezone one frame up for the cells; it is now threaded to the filename too, so both read one clock. The no-timezone fallback stays PROCESS-LOCAL, deliberately not UTC: each surface keeps its own historical output, and defaulting to UTC would silently re-time the filename of every deployment with a host TZ set. zonedWallClock now returns null when no usable zone resolves so each caller supplies its own fallback; the fallback pins own a skewed process TZ so they cannot pass vacuously on a UTC runner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 9 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 3 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8484
exportContentDispositionbuilt the-YYYYMMDD-HHMMSShalf of the export download's suggested filename from process-local getters (now.getFullYear()/getHours()/ …), which read the deployment host'sTZ— a hosting fact, not the caller's business timezone. After #8373 moved the export's contents onto the business timezone, the filename was the last export surface still on the host clock, so the name and the rows inside it disagreed exactly whenTZwas not the business zone: a container atTZ=UTCserving an Asia/Shanghai tenant downloadedorders-20260731-220000.csvwhose first row read2026-08-01 06:00:00— off by a day, and across a month boundary by a month.The fix
The route had already resolved the business timezone 42 lines above the call —
const timezone = …offExecutionContext.timezone(the platform-default to global to tenant cascade), introduced by #8373 for the cells. It is now passed toexportContentDispositionas well, so the name and the contents read one clock. No new resolution path, and nothing else inrest-server.tstouched.The shared clock helper is split rather than given a default:
zonedWallClocknow returnsnullwhen no usable zone resolves, and each of the two callers supplies its own fallback at the call site.wallClock(the cell path) keeps its UTC fallback; the filename keeps process-local. Baking either into the shared helper would silently re-time the other surface.This inverts the intuitive answer and is the whole risk on this card. Each fallback preserves the historical output of its own surface, and the two have different histories: the cells were hardcoded to UTC before #8373; this filename has always used the process clock. "UTC is the safe default" would in fact re-time the filename of every deployment that sets a host
TZbut resolves no business timezone — a silent change to a user-visible name, for zero correctness gain. An explicitly resolved'UTC'is a resolved zone, not a missing one, and does produce a UTC stamp regardless of the host.Tests
The five existing pins keep their exact stamps, and that is deliberate rather than an oversight:
NOWis built from local calendar components, so the local getters read the same components back under any host zone — pinning20260714-153045is a host-zone-independent statement of the process-local fallback. What moved is the arity (timezone sits betweenextand the injected clock), so a caller that forgets it gets a type error rather than a silently re-timed filename.New cases cover the zone being read, the day/month boundary crossing (the issue's own scenario), agreement with the datetime cells, an unresolvable zone, and explicitly-resolved
'UTC'.The fallback pins own a skewed process
TZfor their duration. Without that they would be vacuously green:packages/restis not in CI's skewed-zone job, so on GitHub's UTC runners process-local and UTC are the same stamp and flipping the fallback would pass. One case asserts the zone actually took, mirroring the same assert-the-axis-is-real discipline CI applies to its own skewed-zone job.Reverse verification (fix committed first, then ablated, then restored byte-identically): flipping the fallback to UTC turns exactly the two process-local pins red —
expected '20260801-020000' to be '20260801-100000'— while the explicit-zone, resolved-'UTC'and five naming pins stay green. Predicted direction before running; observed direction matched.Verification
All at head
d82240dd4, tree clean:pnpm --filter @objectstack/rest test— 115 files, 1910 tests passed (includesrest.test.ts's route-levelContent-Dispositionassertions, which match\d{8}-\d{6}and are zone-agnostic)pnpm --filter @objectstack/rest typecheck— cleanpnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build, then the full workspace closure (70 tasks) for the ratchetcheck:cross-package-test-inputs(+ the.mjsform),check:query-options-erasure,check:type-check-coverage,check:type-check-debt(--re-measure, 33 entries, none above ceiling),check:nul-bytesscripts/pm/dispatch-gates.mjs):check:authz-resolverandcheck:route-envelope(pulled in by therest-server.tsline), pluscheck:changeset-gate-self-tests,check:objectui-changeset,check-adr-0087-registration.mjs,check-changeset-no-major.mjs,check-empty-changeset.mjs(pulled in by the new changeset, which did not exist when the dispatch list was taken). All green.route-envelopereports a pre-existing ticks-down-only ratchet onrest-server.ts; this change adds no response site and the gate passes. The@objectstack/lintTEST_DEBT surplus it prints is pre-existing, unrelated, and explicitly not an error — left alone as out of scope.Scope held: the filename stamp only. The file's contents are already correct after #8373 and are untouched; the import half (#8485) is not addressed here and remains open.
Generated by Claude Code