Skip to content

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

Closed

Description

@objectstack-fleet

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)

  1. The server already tells "default" from "chosen". ResolvedSettingValue.source and cascadeChain (objectstack packages/spec/src/system/settings-manifest.zod.ts, cascadeChain scope enum env | global | tenant | user | default); the settings GET returns { values: { key: { value, source, … } } } (objectstack packages/services/service-settings/src/settings-routes.ts:155). Re-check: git grep -n "GET returns" origin/main -- packages/services/service-settings/src/settings-routes.ts in objectstack.
  2. The console already reads that cascade. apps/console/src/pages/settings/SettingsField.tsx:72-89 finds 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" is effective.scope === 'default' on localization.timezone, read through the existing settings API; ⛔ no change to objectstack's /api/v1/auth/me/localization contract (that endpoint keeps answering a string with the UTC floor, current-user-endpoints.ts:612).
  3. The console reads no browser zone today. Re-check: git grep -n "resolvedOptions().timeZone" origin/main -- apps packages returns nothing on 9b673e54. The prompt's pre-fill is the one new reader.
  4. Every cloud workspace is unconfigured today. objectstack-ai/cloud provisioning writes no localization.timezone (the cloud tree's only timezone hit in service-cloud is a comment about Z-suffixed strings). So the population this card serves is every cloud workspace, until its administrator confirms.
  5. A second home for the organization's zone exists and is NOT read by the cascade: sys_organization.timezone (objectstack managed-extension-fields.ts, the #14238 ruling A). Recorded as a gap on the console: the localization fetch drops the timezone the 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.timezone with 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


Generated by Claude Code

Activity

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:i18nThe customer's own language, across UI, metadata and notifications
    and removed on Oct 7, 2026
  2. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-11758-default-timezone-prompt
    Worktree: objectui-issue-11758
    Domain: domain:ui
    Seat: domain:ui#3
    File 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 reads localization.timezone's effective scope through the existing settings API, as apps/console/src/pages/settings/SettingsField.tsx reads it, and confirm writes through the Settings page's own write path. Also the prompt text's language-pack keys in packages/i18n/src/locales/*.ts if the console's strings live there, the tests beside them, and .changeset/11758-*.md. ⛔ Not apps/console/src/LocalizationFetchProvider.tsx (PR objectui#11729 holds it; the card says "no change to LocalizationFetchProvider beyond 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 by Clause-②: yes runs at CONTRACT_REVIEW_TIER in an isolated subagent once the PR is up.
    Clause-②: yes
    Responsibility: n/a — not a defect card
    Thread-read: none
    Serial constraints cleared: none blocking. This card gates PR objectui#11729 (objectui#11693, this seat's), which holds LocalizationFetchProvider.tsx, so this card stays off that file. Same axis (area:i18n): objectui#11439 (this seat's) is paused on pm:retriage and edits packages/i18n/src/useObjectLabel.ts, packages/react and apps/console/src/preview-samples.ts, which is disjoint. objectui#11645 (area:devpath, this seat's, dev running) adds marketplace.* keys to the same locale packs, in a different block; the later lander merges main. No other open objectui PR touches apps/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 to no.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:11Z. PR objectui#11761, head 39033c2.

    • PR shape: draft against main. First line Fixes #11758, no other closing keyword. Clause-②: yes at line start. The head merges origin/main 7b17705, which is objectui#11645's locale-pack keys, after the PR opened. That is a merge, not a rebase, and it was clean. main is still 7b17705.

    • Scope: 17 files, +955/−2. No governed path.

      • apps/console/src/pages/settings/workspaceTimezonePrompt.ts (decision functions) and WorkspaceTimezonePrompt.tsx (the dialog);
      • the mount in AppContent.tsx;
      • the pin file;
      • six console.workspaceTimezonePrompt.* keys in all ten packs;
      • the changeset.

      LocalizationFetchProvider.tsx is 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: one DECLARED entry for the one new host-zone reader.
    • Diff read (the seat's own):

      • findTimezoneOffer asks only when all of these hold:
        • the session's reported systemPermissions hold the manifest's own writePermission. It fails closed when the answer is unreported or the manifest declares none;
        • timezone resolves with source: 'default' and is unlocked;
        • isValueDomainMember, the door's own predicate, imported rather than copied, admits the browser zone.
      • Confirm is one saveSettingsNamespace('localization', { timezone }). Measured: SettingsView saves its draft of 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.
      • Decline, Escape and the overlay write nothing.
      • "Once" is a localStorage record 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:

      • Capability checked: none on the client; the server's assertPermitted checks writePermission.
      • One-time notice surface: none in apps/console, so the dialog is its own component.
      • "Session": read as the sign-in session. The narrowest scope that covers every tab of one sign-in is the device, and a server flag is ruled out because it would be a new key.

      The seat accepts each reading.

    • Changeset sentences checked against the head:

      • "on opening an app";
      • the three gate conditions;
      • "a session that may not write settings is never asked, and reads nothing beyond the settings list";
      • once per device, administrator and workspace, "at the moment it opens";
      • confirm goes through PUT /api/settings/localization;
      • decline or close writes nothing;
      • the six keys listed are exactly the six en.ts adds.

      minor on @object-ui/console and @object-ui/i18n, matching Clause-②: yes.

    • Reverse verification (dev report 6031241857): five ablations through ablation-replace.mjs, each restored with the blob equal to HEAD and an empty git diff HEAD:

      • pre-fill: 4 red;
      • source check: 2 red;
      • mayWrite: 2 red;
      • asked-record write: 2 red;
      • zone admission: 1 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_sha equals 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 state clean.

    • Contract review: 6031380340, served at CONTRACT_REVIEW_TIER, same head, PASS.

      • All 13 ① judgments are right.
      • ② semver matches.
      • Every ③ flag is answered or escalated. The escalations are taken below.
    • Open questions: none.

    Findings:

    • objectstack SettingsManifest.writePermission: the spec's parse default is setup.write, but SettingsService.requiredCapability falls back to readPermission, then setup.access, for a manifest registered without a Zod pass. Measured on objectstack origin/main 93125aee, all 11 manifest producers declare writePermission: the 10 in service-settings/src/manifests/ and objectql's lifecycle-settings.ts. No producer reaches the divergence, so there is no reach:. The prompt fails closed on it → Acceptance notes.
    • the dialog mounts in the app shell (/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's apps/console scope → 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.
    • a session that is not a localization writer, or a writer whose workspace zone is already chosen, pays one or two settings reads per app-shell load, because the record is written only when the dialog opens. The changeset discloses this, and it is keyed on the capability list's content → Acceptance notes.
    • the PR body's Gates section cites 74e0383, before the main merge. The head's check-runs and the dev report's re-run at 39033c2 supersede it → Acceptance notes.

    Landing: ready + auto-merge through the queue, now. Serial behind this PR: PR objectui#11729 (objectui#11693, pm:blocked on 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

  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:31Z. PR objectui#11761 merged through the queue as 3bf8894 at 2026-10-07T05:29Z.

    • The merge:
      • One parent, 7b17705, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head 39033c2 (tree 07626483 on both).
      • 17 files, +955/−2.
    • Content check, 7b17705 → 3bf8894:
      • <WorkspaceTimezonePrompt /> in AppContent.tsx: 0 → 1.
      • apps/console/src/pages/settings/workspaceTimezonePrompt.ts: absent → present.
      • Locale packs carrying the workspaceTimezonePrompt block: 0 → 10 of 10.
      • Control: <LocalizationFetchProvider in AppContent.tsx is 1 on both sides, untouched as the card requires.
    • Closures:
      • The PR's only closing keyword was Fixes #11758. objectui#11758 is the only issue closed since 05:00Z. pm:dispatched is removed.
    • The gate is open. This card was the B2 gate the director's ruling put on PR objectui#11729 (6030273339). objectui#11693 is pm:blocked on it with Unlock-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:i18nThe customer's own language, across UI, metadata and notificationsdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions