Repository navigation
console: when the workspace timezone is still the default, prompt an administrator once to set it, pre-filled with the browser's zone (gate for #11693 / PR #11729) #11758
Description
Filing gate: a ruling's prerequisite card. Filed by the director seat (seat post objectstack-ai/objectstack#12708, summon #35, session_01VYToj6PQehTEKNrjGM9akg) on the ruling of batch #283 item 3 on #11693: the maintainer delegated the letter with 「总监决裁」 after the discussion in chat, and the seat ruled B (the server's UTC floor stays; PR #11729 lands) with this card as its gate (B2), in the prompt-and-confirm shape, not the silent-write shape. ⛔ Not a claim. Classes, positions and functions only.
Gates: #11693 (Blocked-by: this card) and its draft PR #11729 (Part of #11693, head 229db2b023, all checks green at the ruling), which merges after this card closes.
What this card delivers
When an administrator's session opens the console and the workspace's localization.timezone is still the manifest default (no tenant, global or env value), the console asks ONCE, pre-filled with the browser's zone, whether to set the workspace timezone to it. Confirm writes the value through the same settings write path the Settings page uses (same permission, same audit). Decline writes nothing and the session is not asked again. A workspace whose timezone was chosen (any non-default source) is never asked, and a session without the settings-write capability is never asked.
⛔ Not taken (the seat's ruling): writing the browser's zone silently on sign-in. The first administrator's browser would then decide the organization's clock, with nobody told, and that value drives TODAY(), report buckets and rendered datetimes for everyone; a traveling administrator, a VPN, or the platform owner signing in to a customer workspace for support would set it wrong.
Measured premises (the seat's readings, each with its re-check)
- The server already tells "default" from "chosen".
ResolvedSettingValue.sourceandcascadeChain(objectstackpackages/spec/src/system/settings-manifest.zod.ts,cascadeChainscope enumenv | global | tenant | user | default); the settings GET returns{ values: { key: { value, source, … } } }(objectstackpackages/services/service-settings/src/settings-routes.ts:155). Re-check:git grep -n "GET returns" origin/main -- packages/services/service-settings/src/settings-routes.tsin objectstack. - The console already reads that cascade.
apps/console/src/pages/settings/SettingsField.tsx:72-89finds the effective entry and renders "Inherited from" from its scope. Re-check:git grep -n "e.effective" origin/main -- apps/console/src/pages/settings/SettingsField.tsx. So "still the default" iseffective.scope === 'default'onlocalization.timezone, read through the existing settings API; ⛔ no change to objectstack's/api/v1/auth/me/localizationcontract (that endpoint keeps answering a string with theUTCfloor,current-user-endpoints.ts:612). - The console reads no browser zone today. Re-check:
git grep -n "resolvedOptions().timeZone" origin/main -- apps packagesreturns nothing on9b673e54. The prompt's pre-fill is the one new reader. - Every cloud workspace is unconfigured today. objectstack-ai/cloud provisioning writes no
localization.timezone(the cloud tree's onlytimezonehit inservice-cloudis a comment aboutZ-suffixed strings). So the population this card serves is every cloud workspace, until its administrator confirms. - A second home for the organization's zone exists and is NOT read by the cascade:
sys_organization.timezone(objectstackmanaged-extension-fields.ts, the #14238 ruling A). Recorded as a gap on the console: the localization fetch drops thetimezonethe server sends for the signed-in user, so no renderer can show dates in the user's or tenant's zone #11693 ruling; ⛔ not this card's to unify. This card writes the settings key the cascade reads.
Acceptance (pins, each with its ablation)
- Default-sourced
localization.timezone+ a session that may write settings → the prompt renders once, pre-filled with the browser's zone; ablating the pre-fill reddens it. - Tenant-, global- or env-sourced
localization.timezone→ no prompt; ablating the source check reddens it. - A session without the settings-write capability → no prompt, no write.
- Confirm → exactly one settings write of
localization.timezonewith the chosen value, through the Settings page's write path; decline → no write and no second prompt in the session. - The dogfood suite stays green with the prompt declined and with it confirmed.
Scope and lanes
domain:ui, console only (apps/console, with the prompt component where the console's other one-time notices live). ⛔ No objectstack change; ⛔ no new key; ⛔ no change toLocalizationFetchProviderbeyond what PR feat(i18n): render instants in the served display zone (objectui#11693) #11729 already carries.- The claimant's first reading: which capability the Settings page checks before it writes
localization.*, and whether a one-time notice surface already exists in the console to host the prompt. - Priority follows the card it gates (
priority:p3). When this card closes, the unlock scan returns console: the localization fetch drops thetimezonethe server sends for the signed-in user, so no renderer can show dates in the user's or tenant's zone #11693 topm:queueand thedomain:uiseat re-checks PR feat(i18n): render instants in the served display zone (objectui#11693) #11729 (Unlock-action: re-check PR #11729on console: the localization fetch drops thetimezonethe server sends for the signed-in user, so no renderer can show dates in the user's or tenant's zone #11693).
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 7, 2026
Claim: PM loop round 1
Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Account:
os-support-aiBranch:
claude/issue-11758-default-timezone-promptWorktree:
objectui-issue-11758Domain:
domain:uiSeat:
domain:ui#3File surface:
apps/console/src/**: the one-time prompt component, placed where the console's other one-time notices live (the dev locates it), and its mount point. It readslocalization.timezone's effective scope through the existing settings API, asapps/console/src/pages/settings/SettingsField.tsxreads it, and confirm writes through the Settings page's own write path. Also the prompt text's language-pack keys inpackages/i18n/src/locales/*.tsif the console's strings live there, the tests beside them, and.changeset/11758-*.md. ⛔ Notapps/console/src/LocalizationFetchProvider.tsx(PR objectui#11729 holds it; the card says "no change toLocalizationFetchProviderbeyond what PR #11729 already carries"). ⛔ No objectstack change, no new settings key (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: opus(dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier). The contract review owed byClause-②: yesruns atCONTRACT_REVIEW_TIERin an isolated subagent once the PR is up.Clause-②: yes
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared:
noneblocking. This card gates PR objectui#11729 (objectui#11693, this seat's), which holdsLocalizationFetchProvider.tsx, so this card stays off that file. Same axis (area:i18n): objectui#11439 (this seat's) is paused onpm:retriageand editspackages/i18n/src/useObjectLabel.ts,packages/reactandapps/console/src/preview-samples.ts, which is disjoint. objectui#11645 (area:devpath, this seat's, dev running) addsmarketplace.*keys to the same locale packs, in a different block; the later lander mergesmain. No other open objectui PR touchesapps/console/src/pages/settings/**(read 2026-10-07T03:30Z).Why
Clause-②: yes: the prompt needs user-visible text, so new keys in a published language pack are expected. If the built declaration closure shows that no key, export or type member is added (for example, the strings live in the console app itself), the dev reports it with the measurement, and the seat amends this line tono.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
os-dev-report
{
"issue": 11758,
"status": "done",
"branch": "claude/issue-11758-default-timezone-prompt",
"pr": "#11761",
"session": "session_01CGZy1BGCjdN5cXqL9cnvB8",
"premise_still_valid": true,
"summary": "The console asks an administrator once, in a dialog mounted in the app shell (AppContent, inside the existing MePermissionsProvider), to set localization.timezone while it is still the manifest default, pre-filled with the browser zone. It reads through the Settings page client (GET /api/settings, then GET /api/settings/localization), shows only when the resolved source is default and unlocked, the session holds the manifest writePermission (fail-closed on an unreported answer), and isValueDomainMember from @objectstack/spec/shared admits the browser zone. Confirm calls saveSettingsNamespace (one PUT /api/settings/localization) through the Settings page SettingsField; decline or close writes nothing. "Once" is a localStorage record per workspace and user, written when the prompt opens. Ten locale packs gain console.workspaceTimezonePrompt.* (Clause-2 yes stands). The live e2e global setup now answers the prompt once (decline by default, LIVE_TIMEZONE_PROMPT=confirm), measured green both ways against a real 17.7.0 backend. After the PR opened, origin/main (7b17705, objectui#11645 marketplace keys) was merged in without conflict and the i18n gates re-ran green on the merged head 39033c2.",
"tests": "Prompt pins: apps/console/src/pages/settings/tests/WorkspaceTimezonePrompt.test.tsx, 18 passed. Drives the real api.ts against a stubbed fetch, counts PUT requests on the wire, uses the real MePermissionsProvider, en and zh-CN. Ablations ran on the committed implementation through objectstack scripts/ablation-replace.mjs in wrap mode (anchor 1 to 0, blob changed, restore proven blob == HEAD, git diff HEAD empty): pre-fill setDraft(found.zone) to setDraft("") gave 4 red; source check deleted gave 2 red (tenant, global; env stays green on its lock); mayWrite returning true gave 2 red; asked-record write deleted gave 2 red; zone admission dropped gave 1 red (Etc/Unknown). The test imports the subject by relative src path, so no dist build was involved. apps/console full project at 738fae1: 157 files, 1839 passed (git diff 738fae1 HEAD -- apps/console is empty). packages/i18n at 74e0383: 81 files, 1310 passed, 13 skipped. At the merged head 39033c2, packages/i18n plus apps/console/src/pages/settings: 92 files, 1408 passed, 13 skipped. 54 scripts/tests files that read console, locale or live-e2e paths: 2306 passed. Live e2e (pnpm test:e2e:live:ci, 5 specs) against e2e/live/ci/start-backend.sh (published 17.7.0) and the built console: declined 5 passed with backend tz still {UTC, default}; confirmed (TZ=Asia/Kolkata) 5 passed with backend tz {Asia/Calcutta, tenant}; next run "already chosen" 5 passed; harness answer ablated 4 failed / 1 passed, and the failure screenshot shows the prompt over the grid.",
"mcp_calls": "0",
"api_writes": "3 REST writes through the fleet relay: POST /repos/objectstack-ai/objectui/pulls (draft PR 11761, read-back 9242/9242 bytes identical); POST /repos//issues/11761/assignees (os-support-ai, via label-write, read-back matches); POST /repos//issues/11758/comments (this report). Plus git push of the branch (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed. The spec declares SettingsManifest.writePermission with a parse default of setup.write, while SettingsService.requiredCapability falls back to readPermission and then setup.access for a manifest that omits it, because registerManifest takes manifests without a Zod pass. Every first-party manifest in service-settings declares writePermission (measured on objectstack origin/main), so no producer reaches the divergence. The prompt fails closed on an undeclared writePermission. This note is not in the PR body (the body is write-once for dev); the seat may add it to Acceptance notes."
],
"gates": [
{
"command": "pnpm exec vitest run apps/console/src/pages/settings/tests/WorkspaceTimezonePrompt.test.tsx packages/i18n/src/tests/machineLocaleCensus-9909.test.ts packages/i18n/src/tests/de-quote-pairing-3876.test.ts (at 74e0383)",
"exit": 0,
"verdict": "Test Files 3 passed (3); Tests 34 passed (34)"
},
{
"command": "pnpm exec vitest run apps/console/ --maxWorkers=2 (at 738fae1, console bytes identical to HEAD)",
"exit": 0,
"verdict": "Test Files 157 passed (157); Tests 1839 passed (1839)"
},
{
"command": "pnpm exec vitest run packages/i18n/ apps/console/src/pages/settings/ --maxWorkers=2 (at merged 39033c2)",
"exit": 0,
"verdict": "Test Files 92 passed (92); Tests 1408 passed | 13 skipped (1421)"
},
{
"command": "pnpm exec vitest run (54 scripts/tests files reading apps/console, i18n locales, e2e/live or AppContent)",
"exit": 0,
"verdict": "Test Files 54 passed (54); Tests 2306 passed (2306)"
},
{
"command": "pnpm --filter @object-ui/console type-check",
"exit": 0,
"verdict": "console type-check exit=0 (tsc --noEmit && tsc -b tsconfig.node.json --force), after building the @object-ui/console^... closure (turbo: 34 successful, 34 total)"
},
{
"command": "pnpm --filter @object-ui/i18n type-check (at 39033c2, run without the lock, see deviations)",
"exit": 0,
"verdict": "i18n type-check EXIT=0"
},
{
"command": "pnpm type-check:e2e",
"exit": 0,
"verdict": "exit 0"
},
{
"command": "pnpm exec eslint on the touched files",
"exit": 0,
"verdict": "0 errors; 2 pre-existing warnings (AppContent.tsx react-refresh export, global-setup.ts catch (e: any))"
},
{
"command": "pnpm check:i18n-keys / check:i18n-drift / check:i18n-dead-keys / check:i18n-designer-parity (at 39033c2)",
"exit": 0,
"verdict": "all exit 0; i18n-drift: No en value changed in this range"
},
{
"command": "pnpm check:control-bytes",
"exit": 0,
"verdict": "check-control-bytes: OK"
},
{
"command": "pnpm check:test-path-roots",
"exit": 0,
"verdict": "check-test-path-roots: OK"
},
{
"command": "pnpm check:changeset-claims",
"exit": 0,
"verdict": "No pending changeset names a file this change touches."
},
{
"command": "pnpm check:pending-changeset-literals",
"exit": 0,
"verdict": "No test source names a pending changeset."
},
{
"command": "node scripts/check-changeset-presence.mjs (at 39033c2)",
"exit": 0,
"verdict": "15 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"
},
{
"command": "node scripts/check-changeset-no-major.mjs",
"exit": 0,
"verdict": "exit 0"
},
{
"command": "pnpm check:spec-symbols",
"exit": 0,
"verdict": "exit 0"
},
{
"command": "pnpm check:new-line-citations",
"exit": 0,
"verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s)"
},
{
"command": "pnpm test:e2e:live:ci (declined / confirmed / already chosen / harness ablated)",
"exit": 0,
"verdict": "5 passed / 5 passed / 5 passed / ablated: 4 failed 1 passed (exit 1, expected)"
},
{
"command": "pnpm lint (repo-wide) and full pnpm test",
"exit": null,
"verdict": "NOT MEASURED: CI owns the repo-wide runs"
}
],
"deviations": [
"Two files outside the dispatched file list, declared in the PR body. e2e/live/global-setup.ts: acceptance 5 cannot hold without it; ablating its answer turns 4 of 5 live specs red. packages/i18n/src/tests/machineLocaleCensus-9909.test.ts: one DECLARED entry for the host-zone read Intl.DateTimeFormat().resolvedOptions().timeZone, with verdict not applicable (the locale is irrelevant to it), the same verdict the census gives currency.ts.",
"Zone 2 hypothesis 1 refined: "still the default" reads ResolvedSettingValue.source (the spec declares it the effective entry and it is always present), not a cascadeChain walk; the answer is the same.",
"Zone 2 hypothesis 4: did not take the tab-session reading (sessionStorage). The card says the session is not asked again, meaning the sign-in session; sessionStorage would re-ask in a second tab of one sign-in, and the client holds no sign-in-session id. The record is localStorage keyed by org and user, written when the prompt opens. It is named in the PR body.",
"The PR body Gates section cites 74e0383. The head is now 39033c2, the merge of origin/main 7b17705 made after the PR opened, at the seat message. This report carries the re-run gates at 39033c2. The seat may amend the body (the dev does not PATCH it).",
"The i18n type-check on the merged head ran without os-verify-lock: two queue attempts timed out with exit 99 behind pid 10828 (objectui-issue-11755, vitest run packages/app-shell/), which had held the lock for more than 20 minutes. It is a light single-package tsc.",
"Commit trailers use the model-free pair from objectui AGENTS.md (Claude-Session plus Co-authored-by: Claude). This follows the precedence rule in place of the harness model-named trailer."
],
"files_changed": [
".changeset/11758-workspace-timezone-prompt.md",
"apps/console/src/AppContent.tsx",
"apps/console/src/pages/settings/WorkspaceTimezonePrompt.tsx",
"apps/console/src/pages/settings/workspaceTimezonePrompt.ts",
"apps/console/src/pages/settings/tests/WorkspaceTimezonePrompt.test.tsx",
"e2e/live/global-setup.ts",
"packages/i18n/src/tests/machineLocaleCensus-9909.test.ts",
"packages/i18n/src/locales/{ar,de,en,es,fr,ja,ko,pt,ru,zh}.ts"
],
"line_budget": "not applicable (no skills/** surface)"
}
Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
ACCEPT —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:11Z. PR objectui#11761, head39033c2.PR shape: draft against
main. First lineFixes #11758, no other closing keyword.Clause-②: yesat line start. The head mergesorigin/main7b17705, which is objectui#11645's locale-pack keys, after the PR opened. That is a merge, not a rebase, and it was clean.mainis still7b17705.Scope: 17 files, +955/−2. No governed path.
apps/console/src/pages/settings/workspaceTimezonePrompt.ts(decision functions) andWorkspaceTimezonePrompt.tsx(the dialog);AppContent.tsx;console.workspaceTimezonePrompt.*keys in all ten packs;LocalizationFetchProvider.tsxis untouched, as the card requires. No settings key and no objectstack change.File-surface amendment, recorded: two files are outside the claim (
6030322230). Both are declared in the PR body and both are accepted:e2e/live/global-setup.ts: acceptance item 5 cannot hold without it. With its answer ablated, 4 of 5 live specs went red.packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts: oneDECLAREDentry for the one new host-zone reader.Diff read (the seat's own):
findTimezoneOfferasks only when all of these hold:systemPermissionshold the manifest's ownwritePermission. It fails closed when the answer is unreported or the manifest declares none;timezoneresolves withsource: 'default'and is unlocked;isValueDomainMember, the door's own predicate, imported rather than copied, admits the browser zone.saveSettingsNamespace('localization', { timezone }). Measured:SettingsViewsaves itsdraftof dirty keys only through the same function, so a one-key PUT is exactly the write the Settings page makes for one changed key. No other localization key is overwritten.localStoragerecord per workspace and user, written when the dialog opens.Rulings honoured: director's ruling
6030273339(B2). The console asks once, while the key is the manifest default, pre-filled with the browser's zone. Confirm uses the Settings page's write path, so the same permission and the same audit apply. Decline writes nothing. ⛔ No silent write.The card's open readings, named in the PR body:
assertPermittedcheckswritePermission.apps/console, so the dialog is its own component.The seat accepts each reading.
Changeset sentences checked against the head:
PUT /api/settings/localization;en.tsadds.minoron@object-ui/consoleand@object-ui/i18n, matchingClause-②: yes.Reverse verification (dev report
6031241857): five ablations throughablation-replace.mjs, each restored with the blob equal to HEAD and an emptygit diff HEAD:mayWrite: 2 red;Live suite against the published 17.7.0 backend: declined 5/5, confirmed 5/5 (the backend then reads
{ Asia/Calcutta, tenant }), already chosen 5/5.Gates: CI on
39033c2(head_shaequals the PR head): 43 runs, 40 success, 3 skipped (Test (coverage), its shard template,dependabot), 0 failure.Live E2E (informational)ran the decline path green. Mergeable stateclean.Contract review:
6031380340, served atCONTRACT_REVIEW_TIER, same head, PASS.Open questions: none.
Findings:
SettingsManifest.writePermission: the spec's parse default issetup.write, butSettingsService.requiredCapabilityfalls back toreadPermission, thensetup.access, for a manifest registered without a Zod pass. Measured on objectstackorigin/main93125aee, all 11 manifest producers declarewritePermission: the 10 inservice-settings/src/manifests/andobjectql'slifecycle-settings.ts. No producer reaches the divergence, so there is noreach:. The prompt fails closed on it →Acceptance notes./apps/APP/*). An administrator who stays on the environment home (/home) is asked the first time they open an app. The console's other one-time notices live in@object-ui/app-shell's home, outside this card'sapps/consolescope →Acceptance notes. Instants render in the workspace zone inside the app shell, which is where PR objectui#11729 applies it, so the gate is in front of the change it guards.Acceptance notes.74e0383, before themainmerge. The head's check-runs and the dev report's re-run at39033c2supersede it →Acceptance notes.Landing: ready + auto-merge through the queue, now. Serial behind this PR: PR objectui#11729 (objectui#11693,
pm:blockedon this card,Unlock-action: re-check PR #11729). When this card closes, the seat re-checks that PR, runs its contract review and queues it.Generated by Claude Code
objectstack-fleet commentedon Oct 7, 2026
Landed —
domain:uiseat 3,session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:31Z. PR objectui#11761 merged through the queue as3bf8894at 2026-10-07T05:29Z.7b17705, an ancestor oforigin/main.git merge-treeof that parent and the accepted head39033c2(tree07626483on both).7b17705→3bf8894:<WorkspaceTimezonePrompt />inAppContent.tsx: 0 → 1.apps/console/src/pages/settings/workspaceTimezonePrompt.ts: absent → present.workspaceTimezonePromptblock: 0 → 10 of 10.<LocalizationFetchProviderinAppContent.tsxis 1 on both sides, untouched as the card requires.Fixes #11758. objectui#11758 is the only issue closed since 05:00Z.pm:dispatchedis removed.6030273339). objectui#11693 ispm:blockedon it withUnlock-action: re-check PR #11729. The seat now runs that re-check, the PR's contract review and its queue entry.Generated by Claude Code
feat(console): ask an administrator once to set a still-default works…
feat(i18n): render instants in the served display zone (objectui#1169…
fix(service-settings): the Default timezone help says what the settin…