Repository navigation
feat(auth,console): a new workspace takes its creator's browser timezone (objectui#11908) - #11953
Conversation
…eate query (objectui#11908)
The create route reads the creator's IANA zone from `?timezone=` and keeps
the body `{ name, slug }`. `createOrganization`'s input gains an optional
`timezone` on `AuthClient` and `AuthContextValue`; the client lifts it onto
the query through better-auth's `fetchOptions.query`. An absent or empty
zone sends no parameter.
Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…browser zone (objectui#11908) `CreateWorkspaceDialog` (every slug attempt) and `SetupPage`'s create branch pass the browser's `Intl.DateTimeFormat().resolvedOptions().timeZone` as `createOrganization`'s `timezone`. A missing, empty or throwing read sends no key at all. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…bjectui#11908) `@object-ui/auth` minor (an additive optional member on two published declarations); `@object-ui/app-shell` and `@object-ui/console` patch. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
…chine-locale census (objectui#11908) `CreateWorkspaceDialog` reads `Intl.DateTimeFormat().resolvedOptions().timeZone` to seed a new workspace's zone. That is the host zone, the one member of `resolvedOptions()` the locale does not decide, and nothing is formatted: the same verdict the census already gives the console's prompt reader, which `packages/app-shell` cannot import. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
❌ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. Which half objected:
📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#11908 (body; comments 6055427516, 6056548803, 6057204057, 6057250297, 6057425342, 6057502960), PR objectui#11953 (body, 11-file list, net diff ① Derived judgmentsPublished surface,
Wire, the accept-set of
Call sites:
Prompt, card scope item 3: no code change. The guard at
② Semver levelChangeset
③ Boundary flags
Implemented-by: VERDICT: PASS |
Takes the eager-closure rollback (objectui#11937, #11956) so Bundle Analysis re-runs against the rolled-back base. No conflicts; no file of this branch is touched by the merged range. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card objectui#11908 (body; comments 6055427516, 6056548803, 6057204057, 6057250297, 6057425342, 6057502960, 6057960053, 6058437332), PR objectui#11953 (body, 11-file list, net diff against Re-record context, verified rather than inherited: this head is ① Derived judgmentsPublished surface,
Wire, the accept-set of
Call sites:
Prompt, card scope item 3: no code change. The guard at Eager closure, ② Semver levelChangeset
③ Boundary flags
Implemented-by: VERDICT: PASS |
Fixes #11908
Clause-②: yes
A new workspace now takes its creator's browser timezone when it is created, so its first administrator is no longer asked for a zone the browser already knows. Existing workspaces are untouched (「存量不动」).
The wire shape is the one in triage's unlock on the card (comment 6055427516):
POST /api/v1/auth/organization/create?timezone=IANA_ZONE, with the body staying{ name, slug }. The zone rides the query, never the body.What changed
@object-ui/auth:createOrganizationtakes an optionaltimezone.createAuthClient) puts it on the create route's query through better-auth'sfetchOptions.query.AuthProviderpasses its input through to the client unchanged.@object-ui/app-shell:CreateWorkspaceDialogreadsIntl.DateTimeFormat().resolvedOptions().timeZoneonce per submit, through a module-privatebrowserTimeZone().timezone.timezonekey.@object-ui/console:SetupPage's create branch sends the same zone, read through the console's existingbrowserTimeZone()inpages/settings/workspaceTimezonePrompt.ts, so that module stays the console app's one reader.@object-ui/authminor,@object-ui/app-shellpatch,@object-ui/consolepatch.@object-ui/i18n(test only):machineLocaleCensus-9909.test.tsgains oneDECLAREDentry for the dialog's host time-zone read (count: 1, verdictnot applicable), the twin of the console prompt reader's entry. Nothing is published.Published declarations that change (for the contract review)
Measured on the built declaration closure:
@object-ui/authbuilt at BASEf3a0488and at this branch, thendiff -roverdistwith*.jsand*.mapexcluded. Exactly two.d.tsfiles differ, and each gains one optional member plus doc text. Nothing else indist/*.d.tsmoves.dist/index.d.tsbydataparameter beforedataparameter afterAuthClient.createOrganization(dist/types.d.ts)export type { AuthClient }; return type ofcreateAuthClient(){ name: string; slug: string; logo?: string }{ name: string; slug: string; logo?: string; timezone?: string }AuthContextValue.createOrganization(dist/AuthContext.d.ts)export type { AuthContextValue }; return type ofuseAuth(){ name: string; slug: string; logo?: string }{ name: string; slug: string; logo?: string; timezone?: string }AuthOrganization).check:readme-exportsis not implicated.timezonecompiles and sends exactly what it sent before;pnpm --filter @object-ui/app-shell type-checkandpnpm --filter @object-ui/console type-checkboth compile every test double in the tree that is typed asAuthClient, and both are green.Measurements (dispatch Zone 2)
better-auth1.7.2: its client proxy splitsfetchOptionsandqueryoff the argument before building the body.createOrganization-timezone-11908.test.tsdrives the real client through a stubbedfetch. It readsurl.search === '?timezone=Asia%2FShanghai'and body keys exactlyname, slug.url.search === ''and the same two body keys./organization/createendpoint declares a body schema and no query schema. So a server without the cloud half ignores the parameter, which matches triage's "harmless before production" reading.Clause-②: yesstands.browserTimeZone().packages/app-shellcannot importapps/console, so the dialog carries its own module-private copy, and no new export was added for it.source, andfindTimezoneOfferreturnsnullfor any non-defaultsource.WorkspaceTimezonePrompt.test.tsxpasses "tenant-sourced timezone → no prompt, no write", along with its global and env variants.Tests (all at HEAD
5bf108f)New pins:
packages/auth/src/__tests__/createOrganization-timezone-11908.test.ts: the wire. Query?timezone=Asia%2FShanghai, body keys exactlyname, slug, a logo still in the body, and the controls for an omitted and an empty zone.packages/app-shell/src/console/organizations/__tests__/CreateWorkspaceDialog.test.tsx: the zone is stubbed atIntl.DateTimeFormat.prototype.resolvedOptions, so the suite no longer depends on the machine's zone.{ name, slug, timezone }, and the collision retry carries the same zone.timezonekey.apps/console/src/pages/auth/__tests__/setupCreateTimezone-11908.test.tsx: the wizard's create branch, with the same pins and controls. FakesetTimeoutcovers the poll's pauses.Runs (repo root, through the shared verify lock, exit codes captured before any pipe):
pnpm exec vitest run packages/auth/pnpm exec vitest run packages/app-shell/src/console/organizations/ packages/i18n/src/__tests__/residue-namespaces-3546.test.tsxpnpm exec vitest run apps/console/src/pages/auth/ apps/console/src/pages/settings/ apps/console/src/components/SetupRoute.test.tsxpnpm --filter @object-ui/auth type-check--listFilesincludes the new testpnpm --filter @object-ui/app-shell type-check--listFilesincludes the dialog testpnpm --filter @object-ui/console type-check--listFilesincludesSetupPage.tsxand the new testpnpm exec eslinton the 9 touched.ts/.tsxfilescreateAuthClient.ts21 → 21, all pre-existingno-explicit-anywarnings)pnpm check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:new-line-citations,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:phantom-depsnew-line-citationsreads "0 new citation(s)"node scripts/check-changeset-presence.mjs,node scripts/check-changeset-no-major.mjsmajorbump."Declared narrowings:
CreateWorkspaceDialog, enumerated bygit grep -l CreateWorkspaceDialog -- '**/*.test.*'. app-shell's published surface is unchanged: the dialog's props are the same and the new helper is module-private. That enumeration is blind to tree-wide disk scanners such asmachineLocaleCensus-9909, which read app-shell's source frompackages/i18n; CI caught it, and round 2 declares the site.pages/auth), the prompt module's folder (pages/settings) andSetupRoute, which mountsSetupPage.**/*.{ts,tsx}pereslint.config.js. 9 files were linted, a count read from--format json. Type-aware linting is not enabled (noparserOptions.projectorprojectService), so this diff cannot move a verdict on any untouched file.Round 2 at HEAD
7f443ba: the census plus the three pins (4 files, 26 tests) pass, andpnpm --filter @object-ui/i18n type-checkexits 0. Removing the new census entry throughablation-replaceturns exactly the census's 'no call site passes nothing…' test red, with the dialog's line as the only received entry. The restore was proven: blobccd30a3ae311== HEAD.Reverse verification
Each leg was run through
ablation-replacefrom the committed state. Each mutation was proven on disk by its anchor count and a blob change, and each restore was proven by blob equal to HEAD withgit diff HEADempty.fetchOptions.querywiring631e7ae6c6e7== HEADundefinedinstead ofbrowserTimeZone()f102cb5d504c== HEADSetupPagedrops thetimezonespread18ccaaa34618== HEADAll three mutated files are read from
srcby their pins (relative imports;@object-ui/authis mocked at the call sites), so nodistrebuild was involved.Eager closure (the console's first-load budget)
Bundle Analysisis red onmainalready: the console's eager closure is over its 3307.0 KB ceiling there, tracked by the P0 card objectui#11937. This PR does not touch the budget script or any ceiling. This change's own delta, measured locally with a consolevite buildpluspnpm check:eager-closureon BASEf3a0488and on this branch:eagerGzipBytesf3a0488The delta lands mainly in
index(+86) andinfrastructure(+26). The other chunks move by 1 to 3 bytes, from hashed chunk-name references.After
maintook the rollback (3c888c6, objectui#11956) and this branch merged it (9c77741d), the budget bot reads 3306.7 KB across 336 chunks against a 3312.0 KB budget: PASS (comment 6058417056). The first paragraph's 'red onmainalready' describes the base before the rollback.Acceptance notes
SetupPage's usual path does not create a workspace, so it sends no zone. It renames the organization the server bootstrapped at sign-up (updateOrganization). Only when no bootstrap organization appears does it reach the create branch this PR changes. So on a fresh self-hosted deployment, the first owner's bootstrap organization keeps the server default, and the prompt still asks there. That organization is created server-side with no client request this card covers. Whether the ruling reaches it is left to the seat (reportopen_questions). ⛔ Not changed here.browserTimeZone()is "the console's ONE reader of the browser's zone". That stays true ofapps/console:SetupPagereuses it.packages/app-shell's dialog now holds its own module-private reader, because it cannot import the console. The sentence is narrower than it may read. Carrier: none (the prompt module is not on this claim's surface). Noted, not carded.ExpressionProvidermentionssys_organization.create_organization, a metadata action owned outside this repository. If that action creates organizations, it does not carry the zone. Not measured from here. Noted, not carded.Asia/Shanghaicreates a workspace, and the environment readslocalization.timezone = Asia/Shanghaiwith no prompt) needs the cloud half's server, which is not installable here.Session:
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8(os-dev fordomain:ui#3, dispatched under that session).Generated by Claude Code