Skip to content

feat(console): ask an administrator once to set a still-default workspace timezone (objectui#11758) - #11761

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-11758-default-timezone-prompt
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-11758-default-timezone-prompt

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11758

Clause-②: yes

The console now asks an administrator once to set the workspace timezone while localization.timezone is 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.ts decides whether to ask and what to offer (pure functions).
  • apps/console/src/pages/settings/WorkspaceTimezonePrompt.tsx is the dialog: the Settings page's own SettingsField rendering the manifest's timezone specifier, and saveSettingsNamespace('localization', { timezone }) on confirm.
  • apps/console/src/AppContent.tsx mounts it once, inside the MePermissionsProvider the app shell already mounts.

The prompt shows only when all of these hold:

  1. GET /api/settings lists the localization manifest and the session's reported systemPermissions include that manifest's writePermission;
  2. GET /api/settings/localization resolves timezone with source: 'default', unlocked;
  3. the browser reports a zone that isValueDomainMember (from @objectstack/spec/shared, the predicate the settings door itself calls) admits for the specifier's declared valueDomain;
  4. this administrator has not been asked about this workspace on this device.

Readings the card asked for

  • Which capability the Settings page checks before writing localization.*: none on the client. SettingsView renders Save for anyone who can read the page, and the server refuses: SettingsService.assertPermitted requires the manifest's writePermission (setup.write on the localization manifest) for an enforced caller. The prompt reads that same writePermission off the manifest and checks it against systemPermissions from /api/v1/auth/me/permissions, failing CLOSED on an unreported answer (unlike hasCapabilities, 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.
  • Whether a one-time notice surface exists in the console: not in 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.
  • "Still the default": the resolved value's source, not a cascadeChain walk. The spec declares source the effective entry ("The first entry where value is non-null is also the effective source"), the service sets it from that entry, and cascadeChain is optional on the wire. Same answer as SettingsField's walk, from a member that is always present.
  • "Once" and "the session": read as the administrator's sign-in session (the card: "the session is not asked again"). sessionStorage is per tab, so a second tab of one sign-in would ask again, and the client holds no sign-in-session id (AuthClientSession carries only the bearer token, the reading recoveryReminderGate.ts records). The narrowest scope that covers every tab of one sign-in is the device, so the record is the localStorage key os: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.
  • The browser zone and the iana_time_zone domain: the domain is the Intl.DateTimeFormat probe (isValueDomainMember). Measured: Etc/Unknown (ICU's answer for an unknown host zone) and Mars/Olympus are refused; undefined would pass the bare probe, so a non-string answer is refused before it. Without a declared domain the options table 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.tsx drives the real api.ts against a stubbed fetch (writes are counted as PUT /api/settings/localization requests) and the real MePermissionsProvider; the browser zone is stubbed to Asia/Kolkata against a UTC default. 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 and git diff HEAD is empty):

Ablation Red
pre-fill: setDraft(found.zone) becomes setDraft('') 4 (en and zh pre-fill, confirm without editing, refusal slot)
source check deleted 2 (tenant, global; env stays green on its lock)
mayWrite answers true 2 (setup.access-only session, mayWrite unit)
the "asked" record write deleted 2 (record written, decline then remount)
zone admission dropped 1 (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.ts now 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=confirm to 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:

  • declined: answered: decline, 5 passed; the backend's timezone is still { value: 'UTC', source: 'default' }.
  • confirmed (TZ=Asia/Kolkata LIVE_TIMEZONE_PROMPT=confirm): answered: confirm, 5 passed; the backend now answers { value: 'Asia/Calcutta', source: 'tenant' }.
  • chosen (the next run): already chosen; no prompt to answer, 5 passed.
  • harness answer ablated (decline mode): 4 failed, 1 passed; the failure screenshot shows the prompt over the grid.

On the real backend the premises read: localization manifest readPermission: setup.access, writePermission: setup.write; the seeded admin's systemPermissions include setup.write; timezone is { value: 'UTC', source: 'default', locked: false }.

Acceptance notes

  • The browser reported Asia/Calcutta for TZ=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.
  • Mount scope: the app shell (/apps/APP/*). An administrator who stays on the environment home (/home) is asked the first time they open an app.
  • After confirm the display zone follows on the next load, the same as a save on the Settings page (this PR does not touch LocalizationFetchProvider).
  • No doc page covers the localization settings, so none is updated.

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: one DECLARED entry for Intl.DateTimeFormat().resolvedOptions().timeZone, which reads the host zone and formats nothing (the verdict the census already gives currency.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, minor for @object-ui/console and @object-ui/i18n.

Gates (at 74e0383)

  • pnpm exec vitest run apps/console/ (run at 738fae1; git diff 738fae1 HEAD -- apps/console is empty): 157 files, 1839 passed.
  • pnpm exec vitest run packages/i18n/: 81 files, 1310 passed, 13 skipped.
  • the prompt, census and de quote-pair test files at HEAD: 3 files, 34 passed.
  • the 54 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 eslint on the touched files: 0 errors (the two warnings are pre-existing: the AppContent.tsx fast-refresh export and global-setup.ts's catch (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.
  • NOT MEASURED: pnpm lint (repo-wide, CI's run) and the full pnpm test farm (CI's run).

Session: session_01CGZy1BGCjdN5cXqL9cnvB8


Generated by Claude Code

claude added 6 commits October 7, 2026 03:45
…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>
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>
…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>
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 334 chunks) 3513.9 KB 3551.8 KB
Main entry chunk (gzip) 156.3 KB 350 KB
Entry file index-DaedzY7P.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 578.94KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.64KB 22.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 334 chunks) 3513.9 KB 3551.8 KB
Main entry chunk (gzip) 156.3 KB 350 KB
Entry file index-DaedzY7P.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 578.94KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 37.51KB 10.04KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.50KB 11.82KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 90.64KB 22.85KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 39033c2167adfcd9065a1fa18d931f9a0370d3d5
Local-runs: none

PR #11761 (draft, claude/issue-11758-default-timezone-prompt into main; the merge-base is main's tip at the reading, 0 behind, so the PR diff is the net diff against main at the head) for card #11758, the B2 gate the ruling on #11693 (comment 6030273339) put on PR #11729. Inputs read: the card body and both of its comments (claim 6030322230, os-dev-report 6031241857), the ruling, the PR body, its 17-file list, the net diff at the head, and the head's 42 check-runs. Nothing built, run or re-run; isolated from the dispatching seat's conclusions.

① Derived judgments

Each accept-set or public-surface change the diff implies, named right or wrong against the card and the ruling.

  1. The prompt's gate, findTimezoneOffer — asks only when GET /api/settings lists localization with a writePermission the session's reported systemPermissions hold, GET /api/settings/localization resolves timezone with source: 'default' and locked: false, and the browser zone passes isAdmissibleZone. Right. This is the card's "default-sourced + may write" pin; source !== 'default' covers tenant, global, env and user sources (acceptance 2); the locked branch adds a refusal the card did not name but the door would enforce anyway.
  2. "Still the default" read off ResolvedSettingValue.source, not a cascadeChain walk (dev deviation 2). Right. The card's premise 2 located the fact in the cascade SettingsField walks; the spec text the dev quotes declares source the effective entry and cascadeChain optional on the wire, so the member that is always present is the better reader of the same fact. No objectstack contract is touched: /api/v1/auth/me/localization unchanged, LocalizationFetchProvider.tsx untouched.
  3. mayWrite fails CLOSED on an unreported systemPermissions and on a manifest without writePermission, diverging from hasCapabilities (fails open). Right for this surface: the predicate decides who is ASKED, never who may write (the server judges the PUT), and the card's acceptance 3 is "never asked", so the closed direction is the one that cannot breach it. Consequence named: a backend that does not report systemPermissions never prompts; the Settings page remains.
  4. Confirm is saveSettingsNamespace('localization', { timezone }), one PUT; the field is SettingsField on the manifest's own timezone specifier; decline, Escape and the overlay write nothing. Right. This is the card's "same write path, same permission, same audit" letter (acceptance 4), and a refusal lands in the field's error slot as on the Settings page. The ablation table (pre-fill 4 red, source check 2 red, mayWrite 2 red, record write 2 red, admission 1 red) covers each pin the card asked an ablation for; "exactly one write" is counted as PUT requests on a stubbed fetch through the real api.ts, not as mock calls.
  5. "Once" is a localStorage record os:workspace-timezone-prompt:ORG:USER, written when the prompt OPENS, device scope (dev deviation 3). Right, and wider than the letter in the safe direction. The card says "the session is not asked again"; the device record is a superset of every sign-in session on that device (every tab, every re-sign-in), and the only narrower durable scope would be a server-side flag, which the card's "no new key" forbids. Consequences named, not defects: a new device or cleared storage asks again; a reload with the dialog open counts as asked and is never answered; another administrator of the same workspace on the same device IS asked (keyed by user). No new settings key, so that ⛔ holds.
  6. The one new browser-zone reader, Intl.DateTimeFormat().resolvedOptions().timeZone in browserTimeZone(), with a DECLARED census entry (not applicable, mirroring currency.ts). Right. Premise 3 said the pre-fill is the one new reader; the diff has exactly one, and the census accept-set widens by exactly that one, with a reason that holds (the host zone is the one resolvedOptions() member the locale does not decide).
  7. Zone admission through isValueDomainMember from @objectstack/spec/shared (a new import edge into apps/console), the options table exhaustive when no domain is declared. Right. The door's own predicate is called, not copied; Etc/Unknown and a non-string answer are refused before any request. The import resolves and type-checks on the head (Type Check, Build & E2E, Bundle Analysis green), so no package-manifest change was owed.
  8. Mount: AppContent, inside MePermissionsProvider, i.e. the /apps/APP/* shell only; /home is not covered (dev acceptance note). Right within the card's lane, with the consequence named: the card placed the component "where the console's other one-time notices live", the dev measured those live in @object-ui/app-shell's environment home, which the card's scope lane (apps/console only) excludes; so an administrator parked on /home is first asked when they open an app. The card's letter ("opens the console") is wider than this mount; the seat decides whether a follow-up on @object-ui/app-shell is wanted. Not a breach.
  9. Request cost the card is silent on. Because the record is written only when the prompt opens, every app-shell load pays one GET /api/settings for any session with a non-empty systemPermissions that is not a localization writer, and two reads (/api/settings, /api/settings/localization) for a writer on a workspace whose zone is already chosen, indefinitely. Disclosed in the changeset ("reads nothing beyond the settings list"). Acceptable at p3, named for the seat; the effect is keyed on the capability LIST's content, so a provider refetch does not repeat it within one load.
  10. e2e/live/global-setup.ts: a real-browser answer to the prompt before the specs, LIVE_TIMEZONE_PROMPT=decline|confirm (decline is the default), skipped when the backend reports a chosen zone, warned rather than failed when the expected prompt does not show. Right. Acceptance 5 cannot hold without it (the dev's harness ablation: 4 of 5 live specs red). Harness-only knob, no published surface. Named: CI's Live E2E (informational) ran the default decline path; the confirmed path and the "already chosen" re-run are the dev's local measurements against the published 17.7.0 backend, not a check-run on the head.
  11. Asia/Calcutta stored as the browser reports it, not canonicalised to Asia/Kolkata. Right. The door admits it; canonicalising would be a second opinion on the zone that the card did not ask for. Named: the stored value follows the browser's ICU spelling.
  12. New language-pack keys console.workspaceTimezonePrompt.title, .description, .laterHint, .decline, .confirm, .saved in all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh); no export, prop or type member otherwise. Right, and it is what the claim's Clause-②: yes reason ("new keys in a published language pack are expected") foresaw. Each pack carries the same six keys and the same two interpolation names (current, zone); en and zh wording read against the tests' expectations.
  13. The dispatched ⛔s. No objectstack change (the PR is in objectui only); no new settings key; apps/console/src/LocalizationFetchProvider.tsx untouched (PR feat(i18n): render instants in the served display zone (objectui#11693) #11729 keeps it); AppContent.tsx gains one import and one mount beside the existing LocalizationFetchProvider element, within apps/console/src/**. All hold.

Nothing in the diff is wrong against the card or the ruling.

② Semver level

  • Changeset .changeset/11758-workspace-timezone-prompt.md: @object-ui/console: minor, @object-ui/i18n: minor. Matches the diff. @object-ui/i18n gains six keys per pack (additive; nothing renamed or removed); @object-ui/console gains a feature (the one-time prompt) and two modules under pages/settings/, nothing removed. No major is owed and none is declared; e2e/** and the census test publish nothing. The head's Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read and Changeset Overwrite Report are green.
  • Clause-②: yes — the PR body carries Clause-②: yes; the claim (6030322230) said yes with an amendment clause for the no-key case; keys WERE added to the published @object-ui/i18n packs, so yes stands, and this record is the review that line owes.

③ Boundary flags

Every dev flag (os-dev-report 6031241857 deviations and findings, and the PR body's acceptance notes) and the open_questions entry list (empty), each answered or escalated; then the gate state on the head.

  • Deviation 1, two files outside the dispatched surface. e2e/live/global-setup.ts: answered, accepted; acceptance 5 is unmeasurable without it, the dev ablated it (4 of 5 red) and declared it in the PR body. packages/i18n/src/__tests__/machineLocaleCensus-9909.test.ts: answered, accepted; the one new Intl reader must be declared or the census reddens, and the file sits in packages/i18n/src/__tests__/, within the claim's "tests beside them".
  • Deviation 2, source instead of a cascadeChain walk. Answered: right (① item 2).
  • Deviation 3, localStorage per org and user, written at open, not sessionStorage. Answered: accepted, with the device-scope consequences named (① item 5). Not escalated.
  • Deviation 4, the PR body's Gates section cites the pre-merge commit (the one before main was merged in) while the head is 39033c2. Answered: the head's check-runs are the gate verdicts for this record, and the report carries the re-run at 39033c2; the body's stale commit reference is cosmetic. The seat may amend the body (the dev does not PATCH it).
  • Deviation 5, the @object-ui/i18n type-check run locally without os-verify-lock. Answered: moot for this record; the head's Type Check check-run is green.
  • Deviation 6, model-free commit trailers. Answered: process, no contract bearing.
  • Out-of-scope finding: objectstack SettingsManifest.writePermission parse default (setup.write) versus SettingsService.requiredCapability's fallback (read permission, then setup.access) for a manifest registered without a Zod pass. Escalated to the seat as a note, not a blocker: no first-party manifest reaches it (dev measured on objectstack origin/main), the prompt fails closed on it (① item 3), and it is objectstack's, outside this card. Carrier: none filed by the dev; the seat decides whether to forward it to the objectstack triage lane or add it to Acceptance notes as the dev suggested.
  • Acceptance note, mount scope /apps/APP/* and not /home. Answered: accepted within the lane (① item 8); named to the seat as a letter-versus-mount gap for a possible follow-up on @object-ui/app-shell.
  • Acceptance note, Asia/Calcutta stored as reported. Answered: accepted (① item 11).
  • Acceptance note, the display zone follows on the next load; LocalizationFetchProvider untouched. Answered: that is the card's own ⛔; PR feat(i18n): render instants in the served display zone (objectui#11693) #11729 carries that half and re-checks after this card closes.
  • Acceptance note, no doc page updated. Answered: accepted on the dev's reading that no doc page covers localization settings; not re-measured here.
  • The claim's Clause-② amendment clause. Answered: not triggered; keys were added (②).
  • Acceptance 5, the "confirmed" mode. Named: CI's Live E2E (informational) is green on the default decline path; the confirmed and "already chosen" runs are the dev's local measurements (report and PR body), not a check-run on the head.
  • Check-runs on 39033c2 at this record's final reading: 43 in all; 40 success; 3 skipped by condition (Test (coverage), its shard template, dependabot; conditional skips, not gate failures); none pending, none failed. At this review's first reading, taken after every other input, one run was still in_progress: Test (shard 4/8). It was not counted green then; a second reading just before this record was written found it completed success, and the aggregate Test run had then appeared and completed success (the first reading's 42 became 43 by that aggregate). Green on the head: Type Check, Lint, Build & E2E, Live E2E (informational), Test, Test (dist pins), Test (shard 1/8) through Test (shard 8/8), Bundle Analysis, the five Changeset gates, Governed Surface Queue Guard, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check, Pre-Install Import Graph Check, Spec Main Shape Gate, Action Ref Convention, Shell Escape Residue Scan, README Export Check, and the doc and skill checks. The Vercel commit status reads Canceled by Ignored Build Step and is not a gate.

Implemented-by: claude/issue-11758-default-timezone-prompt
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

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

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 05:12
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 3bf8894 Oct 7, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11758-default-timezone-prompt branch October 7, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants