Repository navigation
feat(console): ask an administrator once to set a still-default workspace timezone (objectui#11758) - #11761
Conversation
…pace timezone (objectui#11758) The prompt reads `localization.timezone` through the Settings page's own client, shows only while the value is the manifest default and the session holds the manifest's write permission, pre-fills the browser's zone, and confirms through `saveSettingsNamespace`. Decline writes nothing. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…ui#11758) Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Also reads a refused save without an `any`. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…al setup (objectui#11758) A fresh live backend leaves `localization.timezone` at the manifest default and the seeded admin may write settings, so the console's new one-time modal would stand in front of every spec. Global setup answers it once in a real browser (decline by default, `LIVE_TIMEZONE_PROMPT=confirm` to confirm) and keeps the outcome in the shared storageState. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…ctui#11758) Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
…s zone read (objectui#11758) The de copy now says what it means without German quotes, so the pinned quote-pair count stands. The machine-locale census declares the prompt's one `Intl.DateTimeFormat()` call: it reads the host time zone, formats nothing. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
Brings in objectui#11645's `marketplace.*` locale keys beside this branch's `console.workspaceTimezonePrompt` block; no conflict. 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
|
✅ 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: PR #11761 (draft, ① Derived judgmentsEach accept-set or public-surface change the diff implies, named right or wrong against the card and the ruling.
Nothing in the diff is wrong against the card or the ruling. ② Semver level
③ Boundary flagsEvery dev flag (os-dev-report 6031241857 deviations and findings, and the PR body's acceptance notes) and the
Implemented-by: VERDICT: PASS Reading time 2026-10-07T05:09Z: the card, the ruling, the PR body, its file list, the diff and the check-runs were read in this act, the check-runs last. Generated by Claude Code |
Fixes #11758
Clause-②: yes
The console now asks an administrator once to set the workspace timezone while
localization.timezoneis still the manifest default, pre-filled with the browser's zone. Confirm writes through the Settings page's own save; decline writes nothing. This is the B2 gate the director's ruling on objectui#11693 (comment 6030273339) put on PR objectui#11729; that PR's files are untouched here.What it does
apps/console/src/pages/settings/workspaceTimezonePrompt.tsdecides whether to ask and what to offer (pure functions).apps/console/src/pages/settings/WorkspaceTimezonePrompt.tsxis the dialog: the Settings page's ownSettingsFieldrendering the manifest'stimezonespecifier, andsaveSettingsNamespace('localization', { timezone })on confirm.apps/console/src/AppContent.tsxmounts it once, inside theMePermissionsProviderthe app shell already mounts.The prompt shows only when all of these hold:
GET /api/settingslists thelocalizationmanifest and the session's reportedsystemPermissionsinclude that manifest'swritePermission;GET /api/settings/localizationresolvestimezonewithsource: 'default', unlocked;isValueDomainMember(from@objectstack/spec/shared, the predicate the settings door itself calls) admits for the specifier's declaredvalueDomain;Readings the card asked for
localization.*: none on the client.SettingsViewrenders Save for anyone who can read the page, and the server refuses:SettingsService.assertPermittedrequires the manifest'swritePermission(setup.writeon the localization manifest) for an enforced caller. The prompt reads that samewritePermissionoff the manifest and checks it againstsystemPermissionsfrom/api/v1/auth/me/permissions, failing CLOSED on an unreported answer (unlikehasCapabilities, which fails open): an unprompted question to a non-writer is the worse error, and a holder who is not asked still has the Settings page.apps/console. The console's one-time notices (RecoveryPasswordReminder,CloudOnboardingNext) are banners inside@object-ui/app-shell's environment home page, not a host other code can mount into, and that package is outside this card's scope. So the prompt is its own component beside the Settings page it reuses, mounted in the app shell.source, not acascadeChainwalk. The spec declaressourcethe effective entry ("The first entry wherevalueis non-null is also the effectivesource"), the service sets it from that entry, andcascadeChainis optional on the wire. Same answer asSettingsField's walk, from a member that is always present.sessionStorageis per tab, so a second tab of one sign-in would ask again, and the client holds no sign-in-session id (AuthClientSessioncarries only the bearer token, the readingrecoveryReminderGate.tsrecords). The narrowest scope that covers every tab of one sign-in is the device, so the record is thelocalStoragekeyos:workspace-timezone-prompt:ORG_ID:USER_ID, written when the prompt OPENS: reloading or navigating away with it open does not ask again. No server-side flag, no new settings key.iana_time_zonedomain: the domain is theIntl.DateTimeFormatprobe (isValueDomainMember). Measured:Etc/Unknown(ICU's answer for an unknown host zone) andMars/Olympusare refused;undefinedwould pass the bare probe, so a non-string answer is refused before it. Without a declared domain theoptionstable is exhaustive, as on the server. The server judges with its own ICU, so a zone the browser admits can still be refused at the door: the refusal then lands in the field's error slot and nothing is written.Pins and ablations
apps/console/src/pages/settings/__tests__/WorkspaceTimezonePrompt.test.tsxdrives the realapi.tsagainst a stubbedfetch(writes are counted asPUT /api/settings/localizationrequests) and the realMePermissionsProvider; the browser zone is stubbed toAsia/Kolkataagainst aUTCdefault. 18 tests, en and zh-CN.Each ablation ran on the committed implementation through
ablation-replace.mjs(anchor hit 1 then 0, blob changed, restore proven: blob equals HEAD andgit diff HEADis empty):setDraft(found.zone)becomessetDraft('')mayWriteanswerstruemayWriteunit)Etc/Unknown)The live suite with the prompt declined and confirmed
A fresh live backend leaves the zone at the default and the seeded admin holds
setup.write, so the new modal stands in front of every live spec.e2e/live/global-setup.tsnow answers it once in a real browser, through the prompt's own buttons, and keeps the outcome in the shared storageState: decline by default,LIVE_TIMEZONE_PROMPT=confirmto confirm. It skips when the backend reports a chosen zone.Measured locally against
e2e/live/ci/start-backend.sh(published@objectstack/*17.7.0, the showcase app) and the built console,pnpm test:e2e:live:ci:answered: decline, 5 passed; the backend'stimezoneis still{ value: 'UTC', source: 'default' }.TZ=Asia/Kolkata LIVE_TIMEZONE_PROMPT=confirm):answered: confirm, 5 passed; the backend now answers{ value: 'Asia/Calcutta', source: 'tenant' }.already chosen; no prompt to answer, 5 passed.On the real backend the premises read: localization manifest
readPermission: setup.access,writePermission: setup.write; the seeded admin'ssystemPermissionsincludesetup.write;timezoneis{ value: 'UTC', source: 'default', locked: false }.Acceptance notes
Asia/CalcuttaforTZ=Asia/Kolkata(Chromium's ICU answers the legacy link name). The door admits it and it was stored as sent. The pre-fill is the browser's report, unedited; canonicalising it would be a second opinion on the zone, so it is not done./apps/APP/*). An administrator who stays on the environment home (/home) is asked the first time they open an app.LocalizationFetchProvider).Files outside the dispatched surface
e2e/live/global-setup.ts: acceptance item 5 ("the dogfood suite stays green with the prompt declined and with it confirmed") cannot hold without it, as the ablation above measures.packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts: oneDECLAREDentry forIntl.DateTimeFormat().resolvedOptions().timeZone, which reads the host zone and formats nothing (the verdict the census already givescurrency.ts's metadata probe).New published keys (
@object-ui/i18n, all ten packs)console.workspaceTimezonePrompt.title,.description({{current}}),.laterHint,.decline,.confirm,.saved({{zone}}). No export, prop or type member is added. Changeset:.changeset/11758-workspace-timezone-prompt.md,minorfor@object-ui/consoleand@object-ui/i18n.Gates (at
74e0383)pnpm exec vitest run apps/console/(run at738fae1;git diff 738fae1 HEAD -- apps/consoleis empty): 157 files, 1839 passed.pnpm exec vitest run packages/i18n/: 81 files, 1310 passed, 13 skipped.scripts/__tests__files that read console, locale or live-e2e paths: 2306 passed.pnpm --filter @object-ui/console type-check,pnpm --filter @object-ui/i18n type-check,pnpm type-check:e2e: exit 0.pnpm exec eslinton the touched files: 0 errors (the two warnings are pre-existing: theAppContent.tsxfast-refresh export andglobal-setup.ts'scatch (e: any)).check:control-bytes,check:test-path-roots,check:i18n-keys,check:i18n-drift,check:i18n-dead-keys,check:i18n-designer-parity,check:changeset-claims,check:pending-changeset-literals,check:spec-symbols,check:new-line-citations,check-changeset-presence.mjs,check-changeset-no-major.mjs: exit 0.pnpm lint(repo-wide, CI's run) and the fullpnpm testfarm (CI's run).Session:
session_01CGZy1BGCjdN5cXqL9cnvB8Generated by Claude Code