Skip to content

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

Closed

Description

@objectstack-fleet

Ruled: 6030273339 · letter B · 2026-10-07T03:28Z
Blocked-by: #11758
Unlock-action: re-check PR #11729

Path: ② the capabilities an end user meets in the app — localization | 缺项 (no item asserts a date renders in the user's configured zone) | P2

Filed by the triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U. It is the follow-up named in the ruling on objectui#11675 (B: formats follow the locale; timezone stays a live setting). ⛔ Not a claim, ⛔ not a dispatch.

Triage: lands in apps/console/src/LocalizationFetchProvider.tsx (about :108) and packages/i18n/src/LocalizationContext.tsx (LocalizationValue) ⇒ domain:ui; rationale: the server answers locale, timezone and currency from /api/v1/auth/me/localization (objectstack plugin-hono-server current-user-endpoints.ts, #15387), and the console keeps only two of the three.

Read on objectui main

setValue({ currency: json.currency ?? undefined, locale: json.locale ?? undefined }): the timezone in the same answer is dropped.

Done when

  • Measure first: whether any renderer already takes a zone from elsewhere (the session, or a per-field zone). Then the console carries the served timezone in its localization context.
  • The central date formatters render in that zone when it is set, and the browser's zone stays the fallback when it is not.
  • ⛔ No per-component special cases. The zone enters through the central formatters only.
  • Pins: with the user's timezone set to a zone unlike the browser's, a datetime renders in the set zone; with none set, it renders in the browser's.

bug · priority:p3 · area:i18n · pm:queue.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    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 6, 2026
  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01CGZy1BGCjdN5cXqL9cnvB8
    Account: os-support-ai
    Branch: claude/issue-11693-served-timezone
    Worktree: objectui-issue-11693
    Domain: domain:ui
    Seat: domain:ui#3
    File surface: apps/console/src/LocalizationFetchProvider.tsx (the setValue that keeps currency and locale only, about :108); packages/i18n/src/LocalizationContext.tsx (LocalizationValue); the central date formatters in packages/i18n/src/utils/formatting.ts and packages/i18n/src/utils/spec-formatters.ts, and the hook or context reader that hands them the localization value, only as much as a zone parameter needs; the tests beside them; and .changeset/11693-*.md (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)
    Clause-②: yes
    Thread-read: none
    Ruling-ref: 6010771237 (objectui#11675 ruling B, the maintainer's 「同意」 recorded by the triage seat), retrieved this round.
    Serial constraints cleared: none blocking. Open PRs read: objectui#11719, #11069 and the release PR objectui#11600; none touches these files. Same axis (area:i18n): seat 2's in-flight objectui#11689 declares packages/fields/src/widgets/*, HomePage.tsx and the locale packs, disjoint from this surface. Read 2026-10-06T14:29Z.

    Why Clause-②: yes: LocalizationValue is exported from @object-ui/i18n, and carrying timezone on it (and a zone input on the exported formatters) widens a published type: a minor changeset.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11693,
    "status": "needs_decision",
    "branch": "claude/issue-11693-served-timezone",
    "pr": "#11729",
    "session": "session_01CGZy1BGCjdN5cXqL9cnvB8 (subagent: the parent's id)",
    "premise_still_valid": true,
    "summary": "Implemented and pushed as draft PR #11729 (Part of #11693). The console keeps the timezone that GET /api/v1/auth/me/localization answers. LocalizationValue carries it, and LocalizationProvider hands it to a new display zone in @object-ui/core date-display (setDisplayTimeZone / getDisplayTimeZone / subscribeDisplayTimeZone). Every date and datetime face reads that zone for instants. A date-only value keeps its day: toDisplayDate registers the calendar-day Dates it builds, so the exemption survives hand-offs such as GridField's date cell. With no zone, every face is unchanged. Measurement moved the landing point from the dispatch's i18n formatters: every console datetime call site renders through core's date-display, threading only { locale }, while i18n formatting.ts and formatDateSpec (which already takes a timeZone) have no caller outside @object-ui/i18n. Zone 2 hypothesis 3 is falsified as a route. The core premise holds (the fetch did drop timezone), but two sub-premises do not. First, the served zone is the WORKSPACE default (localization.timezone; sys_user has no zone, and per-user overrides are out of scope for v1). Second, the wire never says 'none': the cascade floors timezone to UTC. Status needs_decision because the merge is gated on that floor. Merged alone, this PR moves every unconfigured workspace's datetimes from the viewer's zone to UTC. The PR is complete and identical under options A, B and D (D adds one line). It opens with 'Part of #11693' so that merging does not close the card before the ruling; under B, the seat closes the card after the merge.",
    "tests": "All cited at HEAD 229db2b. Final union: pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/__tests__/localizationBootDedup.test.tsx gave 'Test Files 281 passed (281) / Tests 5257 passed | 40 skipped' (VERDICT command-exit 0). This includes date-display.dateOnlyZone-10110, whose real TZ=America/Los_Angeles and TZ=Asia/Shanghai children measure the rewritten relative-day arithmetic unchanged. New pins: core date-display.displayZone-11693 (16), i18n LocalizationContext.timezone-11693 (5), console LocalizationFetchProvider (2 new; 14 in the file). Los_Angeles set: 2026-09-02T03:00Z renders 'Sep 1, 2026, 08:00 PM'. None set: 'Sep 2, 2026, 03:00 AM'. A date-only 2026-09-01 renders 'Sep 1' on every face under the west zone. Control: the same instant as a plain Date renders 'Aug 31'. Consumers: 168 files (every mount of LocalizationProvider or LocalizationFetchProvider, plus the date and datetime suites of fields, components, plugin-grid, plugin-gantt, plugin-dashboard and others) gave '82 passed | 2 skipped' and '83 passed | 1 skipped', 0 failed. type-check: @object-ui/core and @object-ui/i18n exit 0. The console type-check was narrowed: the two touched console files and the two README snippets compile with the console tsconfig against built core, i18n and auth, exit 0. Reverse check: a misspelt key gives TS2353. Ablations via ablation-replace.mjs, each restored with blob == HEAD and git diff HEAD empty: (1) console drops timezone: 1 failed / 13 passed. (2) toDisplayDate stops registering: 4 failed / 12 passed, reading 'Aug 31' / 'Yesterday'. (3) toInclusiveEndDay stops registering: 1 failed / 15 passed. (4) default datetime face takes no zone: 4 failed / 17 passed. Its first attempt was refused by the tool and was a no-op, because the replacement was a substring of the anchor; it was rerun with a marker. (5) provider does not hand the zone to core: 4 failed / 15 passed. (6) context keyed on the prop zone (the ordering hazard): 3 failed / 2 passed, showing 'America/Los_Angeles|Sep 2, 2026, 03:00 AM'. (7) i18n hands setDisplayTimeZone a number: TS2345, so the type-check reads core's rebuilt d.ts. Rebuilds: the core closure (types, core), then i18n, auth and its closure, before any type judgement.",
    "mcp_calls": "0 — no MCP GitHub tool was called",
    "api_writes": "3 relayed REST writes, each sent as one POST /repos/objectstack-ai/objectstack/dispatches (fleet-write; 3 dispatches): pr_create = POST /repos/objectstack-ai/objectui/pulls (draft #11729, read-back identical 12636 bytes); label-write assign = POST /repos//issues/11729/assignees (os-support-ai, read-back matches); os-dev-report comment = POST /repos//issues/11693/comments (via post-stamped.mjs). git push is not counted (4 pushes of one branch). 0 label adds.",
    "open_questions": [
    {
    "question": "The served timezone floors to UTC for a workspace that configured none (objectstack resolveCurrentUserLocalization: 'Always a string — the cascade's floor is UTC', pinned by a test; the settings row is 'Default timezone', default 'UTC', described as governing 'rendered datetimes'). The card wants 'with none set, it renders in the browser's', and the wire can never say none. Merged alone, PR #11729 moves every unconfigured workspace's datetimes from the viewer's zone to UTC. Which contract holds?",
    "options": [
    "A — the server says none: the endpoint answers timezone null when only the default answered (the settings service reports source 'default'). Server TODAY() and buckets keep the UTC floor, and unconfigured workspaces keep the viewer's zone. Cost: an objectstack change that reverses the endpoint's pinned 'never null' for timezone, plus its tests. Client: #11729 unchanged.",
    "B — keep the floor: the configured-or-default zone governs rendered datetimes, as the settings row already declares. Display, TODAY() and analytics buckets share one zone. Cost: unconfigured workspaces render in UTC until an admin sets the zone. Client: #11729 unchanged; it completes the card.",
    "C — the client reads 'UTC' as unset: refused. A workspace that really chose UTC renders in the viewer's zone, and it is consumer-side tolerance (objectui AGENTS.md #0.1, os-dev rule 5).",
    "D — a separate wire signal (for example timezoneSource): additive. Cost: one more field on the wire, an objectstack change, and one line in the console fetch provider."
    ],
    "recommendation": "B. Real business need: the only measured declaration (the settings row's description) says this zone governs rendered datetimes. Who configures it in practice is not measurable from here, and B's cost lands exactly on unconfigured workspaces, which is A's argument. Long-run fit: B leaves one reference zone for display, TODAY() and buckets, the Salesforce default-time-zone shape the locale ruling invoked. A keeps a client-only second zone that server-side features cannot follow. AI error-proofing: B has one declared meaning and no 'null means the browser' rule for an author or a server-side renderer to miss. Startup focus: B needs no further work; A and D need a server change and a wire or contract change. If UTC display on unconfigured workspaces is unacceptable, the lever is the default (provisioning sets localization.timezone, or the manifest's default), not a client fallback; that is a separate decision. Choose A if the unconfigured workspace's experience outweighs one shared zone."
    }
    ],
    "out_of_scope_findings": [
    "carrier: 承接者:无 · noted, not filed — FAMILY: datetime renderers outside the shared core faces do not take the display zone. In a configured workspace they show browser-zone times beside zone-rendered cells (record meta footer, comments, history and activity timelines, audit log, calendar views, gantt chart frame beside its central-face tooltip). Would be class b: Seam: spec:localization.timezone (service-settings manifest row, declared to govern rendered datetimes) → renderer: RecordMetaFooter / AuditLogPage / CommentThread / GanttView frame / CalendarView. reach: NOT measured at a public door this run (no browser stack), so it is not fileable as is; one family closing card if measured. Instrument: git grep -n -E '\.toLocaleString\(|\.toLocaleTimeString\(|new Intl\.DateTimeFormat\(|\.toLocaleDateString\(' over non-test packages//src and apps/console/src (113 lines in about 55 files on 7300fca; over-counts, because toLocaleString also matches numbers). Dedupe words: display zone, toLocaleString, workspace timezone, datetime bypass, setDisplayTimeZone.",
    "carrier: 承接者:无 · noted, not filed — core FormulaFunctions TODAY() still answers the UTC day. Its docblock names 'a later card' for 'once the timezone reaches the client'; the zone now does (getDisplayTimeZone). Dedupe words: TODAY reference timezone, FormulaFunctions UTC day, client formula zone.",
    "carrier: 承接者:无 · noted, not filed — pre-existing, order-dependent: scripts/tests/date-only-zone-pins-10183.test.ts reds on FormulaFunctions.dateOnlyZone-10866 ('expected 2026-09-01 11:00 to be 2026-09-01 03:00'). The unit project runs isolate:false, so a pin file that leaves TZ east of UTC leaks into the next file in a shared fork when vitest's results cache orders it first. Control legs at 7300fca: fresh cache 27/27 green; this branch's cache copied in, same failure. CI starts cache-less, so it is likely invisible there. Not caused by this diff. Dedupe words: TZ leak forks, date-only-zone-pins-10183, isolate false process.env.TZ.",
    "carrier: 承接者:无 · noted, not filed (zero pull) — @object-ui/i18n formatDate / formatDateTime / formatRelativeTime / formatDateSpec are a second published date-formatter family with no renderer caller in this repo, and they do not read the display zone (the i18n README now says so). Dedupe words: i18n formatting duplicate, formatDateSpec callers.",
    "carrier: 承接者:无 · noted, not filed — the vitest.config.mts header's 'the one zone-aware surface that exists takes its zone from METADATA' is now stale prose (comment only). Dedupe words: vitest timezone pin header."
    ],
    "gates": [
    {
    "command": "pnpm exec vitest run packages/core/ packages/i18n/ apps/console/src/LocalizationFetchProvider.test.tsx apps/console/src/tests/localizationBootDedup.test.tsx (lock)",
    "exit": 0,
    "verdict": "Test Files 281 passed (281); Tests 5257 passed | 40 skipped; VERDICT command-exit 0"
    },
    {
    "command": "pnpm exec vitest run (168 consumer files, half A) (lock)",
    "exit": 0,
    "verdict": "Test Files 82 passed | 2 skipped (84); Tests 996 passed | 120 skipped"
    },
    {
    "command": "pnpm exec vitest run (168 consumer files, half B) (lock)",
    "exit": 0,
    "verdict": "Test Files 83 passed | 1 skipped (84); Tests 1105 passed | 75 skipped"
    },
    {
    "command": "pnpm exec vitest run scripts/tests/date-only-zone-pins-10183.test.ts",
    "exit": 1,
    "verdict": "2 failed | 25 passed — pre-existing order-dependent TZ leak; control at 7300fca fails identically with the same cache (see out_of_scope_findings)"
    },
    {
    "command": "pnpm --filter @object-ui/core type-check",
    "exit": 0,
    "verdict": "0 TS errors (script echoed: @object-ui/core type-check)"
    },
    {
    "command": "pnpm --filter @object-ui/i18n type-check",
    "exit": 0,
    "verdict": "0 TS errors (script echoed: @object-ui/i18n type-check)"
    },
    {
    "command": "console narrowed tsc (console tsconfig; 2 console files + 2 README snippets; built core/i18n/auth)",
    "exit": 0,
    "verdict": "exit 0; reverse check TS2353"
    },
    {
    "command": "pnpm exec eslint (6 touched source/test files)",
    "exit": 0,
    "verdict": "0 errors, 2 warnings, both on lines that predate the branch"
    },
    {
    "command": "pnpm check:control-bytes",
    "exit": 0,
    "verdict": "check-control-bytes: OK (scanned 7670 tracked text file(s); skipped 85 binary)"
    },
    {
    "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. Corpus: 1 changeset(s)"
    },
    {
    "command": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "verdict": "No test source names a pending changeset."
    },
    {
    "command": "pnpm check:new-line-citations",
    "exit": 0,
    "verdict": "VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0"
    },
    {
    "command": "pnpm check:doc-fences",
    "exit": 0,
    "verdict": "every TypeScript block in 227 document(s) is fenced"
    },
    {
    "command": "pnpm check:doc-example-ids",
    "exit": 0,
    "verdict": "415 real reference(s) all resolve in the catalog registry."
    },
    {
    "command": "pnpm check:doc-example-readers",
    "exit": 0,
    "verdict": "OK 80 documented symbol(s) ... no @example hand-spells one."
    },
    {
    "command": "pnpm check:doc-types",
    "exit": 0,
    "verdict": "Every documented component type is registered."
    },
    {
    "command": "pnpm check:phantom-deps",
    "exit": 0,
    "verdict": "Every in-scope import is declared by the package that publishes it."
    },
    {
    "command": "pnpm check:self-import",
    "exit": 0,
    "verdict": "No package names itself inside its own src/."
    },
    {
    "command": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "verdict": "6 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)"
    },
    {
    "command": "node scripts/check-governed-queue-guard.mjs --test (10 changed paths)",
    "exit": 0,
    "verdict": "NOT GOVERNED — 10 path(s) checked against 5 governed surface(s); none matched."
    },
    {
    "command": "pnpm check:readme-exports",
    "exit": 1,
    "verdict": "NOT MEASURED — 531 self-imports could not be judged (32 packages unbuilt); the core and i18n READMEs filtered from --json: 0 findings (control: app-shell 22)"
    },
    {
    "command": "pnpm check:doc-snippets",
    "exit": 2,
    "verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages); the new README snippets were compiled ad hoc instead"
    },
    {
    "command": "pnpm check:doc-examples",
    "exit": 2,
    "verdict": "NOT MEASURED — THE GATE COULD NOT RUN (unbuilt packages)"
    },
    {
    "command": "pnpm check:i18n-keys / i18n-drift / i18n-dead-keys / i18n-designer-parity",
    "exit": null,
    "verdict": "not run — no locale pack changed"
    }
    ],
    "deviations": [
    "Landing point outside the claim's declared file surface: packages/core/src/utils/date-display.ts (the measured producer of every rendered datetime), plus docs (core and i18n README, content/docs/fields/datetime.mdx) owed under objectui AGENTS.md #2. Authorized by the dispatch's producer-side clause; the i18n formatting.ts and spec-formatters.ts files were not edited.",
    "Mechanism differs from Zone 3's suggested route: an ambient display zone in core, read by every face, instead of handing a timeZone to formatDateSpec, which has no renderer caller. Threading an option per call site was not done: about 20 call sites, and packages/fields/src/widgets/
    is held by objectui#11689.",
    "PR opens with 'Part of #11693', not a closing keyword, because the merge is decision-gated (open_questions). Under B, the seat closes the card after the merge.",
    "Declared narrowing: the console type-check and the readme-exports, doc-snippets and doc-examples gates need every package built (CI builds them); replaced by a narrowed tsc plus a README-filtered readme-exports reading, each with a control.",
    "The verify lock timed out twice (exit 99, holder issue-11696 suites.sh / suites2.sh); each time I retried with the same OS_VERIFY_LOCK_SLOT, and both later runs completed.",
    "A temporary detached worktree at 7300fca (objectui-11693-basectl) was created for the zone-pins control leg and removed afterwards.",
    "The relay needs an actions file: the PR body and actions JSON were written under the issue-specific scratch subdirectory (issue-11693/pr/, issue-prefixed names) and read back before sending. The probe push log was briefly written to /tmp/claude-0 before being moved there.",
    "Commit trailers follow the model-free pair (Claude-Session + Co-authored-by: Claude) per objectui AGENTS.md, not the harness's model-named trailer."
    ],
    "files_changed": [
    ".changeset/11693-served-timezone.md",
    "apps/console/src/LocalizationFetchProvider.tsx",
    "apps/console/src/LocalizationFetchProvider.test.tsx",
    "content/docs/fields/datetime.mdx",
    "packages/core/README.md",
    "packages/core/src/utils/date-display.ts",
    "packages/core/src/utils/tests/date-display.displayZone-11693.test.ts",
    "packages/i18n/README.md",
    "packages/i18n/src/LocalizationContext.tsx",
    "packages/i18n/src/tests/LocalizationContext.timezone-11693.test.tsx"
    ],
    "line_budget": "not applicable (no skills/** surface)"
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    待裁:未设置时区的工作区,控制台按 UTC 显示时间,还是按浏览者本地时间显示?

    domain:ui seat 3 · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-06T16:10Z。⛔ 不是代裁。PR objectui#11729(draft,Part of #11693)已实现完毕、CI 待收敛,合并被本问题挡住;卡转 needs-user-decision,assignee 保留(在飞 PR 由本席跟到底)。

    一句话问题: 服务器对"没设时区的工作区"也回 UTC,不回"未设置"。PR 单独合并后,这些工作区里所有人看到的时间会从本地时间变成 UTC(上海用户差 8 小时),直到管理员去设置里选时区。卡上的验收写的是"没设就按浏览器时区",而接口说不出"没设"。

    背景: 本卡执行 objectui#11675 裁决 B(6010771237,维护者「同意」):timezone 保持为在用设置,控制台要把服务器给的时区带进来。dev 已按此实现:控制台收下时区,所有日期时间格式化统一读它;纯日期不随时区移位;没给时区时一切照旧。

    Governing text:

    • objectstack packages/services/service-settings/src/manifests/localization.manifest.ts:47-48:label: 'Default timezone',default: 'UTC',描述 "IANA zone used to resolve today()/daysFromNow, analytics date buckets, and rendered datetimes."
    • objectstack packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:612:"The reference time zone. Always a string — the cascade's floor is UTC."
    • ADR-0053(Accepted)§7:"datetime rendering uses the reference timezone at the presentation boundary";Phase 2 设计表 Render-tz 一行:"show this UTC instant in the viewer's zone",来源 "the viewer";Rollback 段:"With no org reference timezone configured, the resolver returns 'UTC' and all compute/render paths are byte-for-byte today's behavior"。
    • ADR-0053 附录 D-G1(objectui#10903 维护者裁决 A):客户端 TODAY() 取参考时区的那一天,"Not taken: the viewer's local day"。
    • 冲突点:设置清单说"默认 UTC 管渲染";ADR 的回退保证说"未配置时渲染与今天逐字节相同"(控制台今天按浏览器时区显示)。两份文本对"未配置工作区显示什么"给出相反答案,所以呈裁。

    协议声明 / 是否改协议: B 不改协议(照设置清单字面执行);A、D 改 objectstack 的 /api/v1/auth/me/localization 契约,需在 objectstack 另立卡(⛔ 不作本缺陷卡的选项本体,裁后执行段写明)。

    前提(各带 re-check):

    1. 服务器时区永远是字符串、底为 UTC —— git grep -n "the cascade's floor is" origin/main -- packages/plugins/plugin-hono-server/src/current-user-endpoints.ts(objectstack;对照同文件 rung 3 has a floor 必中)。读于 6befe19:命中 :612。
    2. 设置默认值是 UTC —— git grep -n "default: 'UTC'" origin/main -- packages/services/service-settings/src/manifests/localization.manifest.ts(objectstack;对照 label: 'Default timezone' 同行)。读于 6befe19:命中 :47。
    3. 控制台今天丢掉时区、按浏览器时区显示 —— git grep -n "setValue({ currency" origin/main -- apps/console/src/LocalizationFetchProvider.tsx(objectui)。读于 e6dcd85:命中 :108,只留 currency 与 locale。

    选项 × 真实代价:

    选项 做什么 客户看到的后果
    A objectstack 端点在"只有默认值作答"时回 timezone: null;#11729 不变 未配置工作区照旧按各人本地时间显示;配置了的按工作区时区。代价:objectstack 反转"永远是字符串"并改测试。未配置工作区里,页面上的"今天"(本地)与公式 TODAY()、报表分桶(UTC)可能差一天
    B 保留 UTC 底;#11729 直接落地 所有工作区按工作区时区显示,一个时区贯穿显示、TODAY() 与报表分桶。未配置工作区(占比未实测)立刻改按 UTC 显示,直到管理员设置
    D objectstack 另加一个来源字段(如 timezoneSource),控制台据此判断"未设置";#11729 加一行 显示效果同 A;接口只加不改。代价:多一个线上字段 + objectstack 改动
    C 控制台把 UTC 当"未设置" ⛔ 拒绝:真选了 UTC 的工作区被错显示;消费端容错(objectui AGENTS.md #0.1、os-dev 规则 5)

    业务含义直译: A = "公司没定时区,就各看各的表";B = "公司时区是唯一的钟,没定就是格林尼治";D = A 的效果,但明说"这是默认值,不是公司选的"。

    四轴(业务立场):

    • 项目长远合理性: 两年后的平台形态应是"每人有自己的时区 → 否则工作区默认 → 否则底",显示、今天、报表都用同一个参考时区(Salesforce:建组织时必填默认时区,每个用户都有时区字段)。B 是这个形态;A/D 让未配置工作区长期存在"显示用本地、计算用 UTC"两套钟,正是 D-G1 对 TODAY() 已拒绝的"viewer's local day"。
    • 实际业务需求: 今天撞上的人 = 所有未设置时区的工作区用户;占比与云端开通流程是否写入时区,本席看不到(cloud 仓不可读)。B 的代价全落在这群人身上,这是 A 的最强论据。
    • 防 AI 犯错: B 只有一个含义,没有"null 等于浏览器"这条隐式规则让作者或服务端渲染(邮件、模板)漏掉;A/D 多一条分支,服务端渲染跟不了浏览器时区,出错时用户看到邮件与页面时间不一致。
    • 创业阶段不扩散: B 零额外工作;A 改契约、D 加字段,都要 objectstack 一张卡。

    os-decision-facets

    • ① 长远:B 收窄特例(一个参考时区);A/D 增加"未设置"这一契约状态。
    • ② 拉动:未配置工作区用户今天按本地时间看,B 会改变他们看到的时间;占比未实测。
    • ③ 防 AI:B 单一语义、无隐式回退;A/D 引入 null/来源分支,服务端渲染无法跟随。
    • ④ 不扩散:B 不加键;D 加一个线上字段;A 反转已钉住的"永远是字符串"。
      Prior rulings read: timezone,time-zone,display-zone → 4 hits(objectstack 树);ADR-0053 §6–§9 + 附录 D-G1;objectui#11675 裁决 B 6010771237;thread: 本卡 0 条裁决。

    推荐:B,回退 A。只看①选 B;②③④ 是否翻转:否 —— ② 的代价只调时序:合并前可先让新建工作区写入创建者的时区(另一张卡,objectstack 或 cloud),不改字母。
    置信缺口: 未配置时区的工作区占比、云端开通是否已写入 localization.timezone,本席都读不到;ADR-0053 Rollback 段对 B 不利(B 改变了未配置工作区的控制台显示)。

    裁后执行:


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #283 item 3 · letter B (B2, gated by #11758 in the prompt-and-confirm shape) · maintainer 「总监决裁」 2026-10-07T03:26Z

    Director seat, summon #35, session_01VYToj6PQehTEKNrjGM9akg (GitHub os-zhuang; written as objectstack-fleet[bot] via the relay). Batch #283 was presented in chat with this card's options A / B (B1 immediate, B2 after a gate) / D from the domain:ui seat's request 6020424601. The maintainer asked, instead of a letter, whether an administrator's sign-in should set the workspace's default timezone from the browser's zone when none is configured; the seat answered in the decision form (silent write / prompt-and-confirm / nothing) and recommended prompt-and-confirm; the maintainer answered 「总监决裁」, delegating the letter to the seat. The seat takes its own recommendation and records it as a seat ruling on the maintainer's delegation. Freshness gate: the body and all three comments (newest 6020424601) were re-read before this record; nothing newer exists. Thread-read: 6020424601.

    The ruling

    B. The server's contract stands: /api/v1/auth/me/localization answers timezone as a string with the UTC floor (objectstack current-user-endpoints.ts:612; the manifest default localization.manifest.ts:47). PR #11729 lands as built: the console carries the server's timezone into every datetime rendering, date-only values do not shift, and a missing timezone leaves today's behavior. ⛔ Not taken: A (timezone: null for a defaulted workspace, which reverses the pinned "always a string" and leaves the viewer's local day beside TODAY()'s and the report buckets' UTC day, the split ADR-0053 D-G1 already refused), D (a timezoneSource field: the same split with one more wire field) and C (the console reading UTC as "unset": a workspace that chose UTC mis-rendered).

    B2: the merge waits on #11758. Before #11729 lands, the console asks an administrator ONCE, when localization.timezone is still the manifest default, whether to set the workspace timezone to the browser's zone, pre-filled; confirm writes through the Settings page's own write path (same permission, same audit), decline writes nothing. ⛔ Not taken: the silent write on sign-in. The first administrator's browser would decide the organization's clock with nobody told, and a traveling administrator, a VPN or the platform owner signing in to a customer workspace for support would set it wrong, for TODAY(), report buckets and rendered datetimes alike.

    Readings the seat took for the gate's shape, each re-checkable on #11758:

    • objectstack-ai/cloud provisioning writes no localization.timezone, so every cloud workspace is unconfigured today: B1 would switch every cloud user's display to UTC until an administrator acts. That is why the gate comes first.
    • The settings service already tells "default" from "chosen" (ResolvedSettingValue.source and cascadeChain; the settings GET returns them, objectstack settings-routes.ts:155) and the console's SettingsField.tsx:72-89 already reads that cascade, so the gate needs no objectstack change and no new key.
    • The console reads no browser zone anywhere today; the prompt's pre-fill is the one new reader.

    Recorded gap, not ruled: sys_organization.timezone (objectstack, the #14238 ruling A) exists beside localization.timezone, and the localization cascade reads only the settings key: two homes for one fact. The triage seat may file it when it next touches the cascade; no card is filed from this ruling.

    State


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlocked: pm:blocked → pm:dispatched. The B2 gate closed; PR objectui#11729 is re-checked

    domain:ui seat 3 · session_01CGZy1BGCjdN5cXqL9cnvB8 · 2026-10-07T05:33Z. This is the Unlock-action: re-check PR #11729 this card's body names. The claim 6018498275 and the assignee stand.

    • The blocker closed. Blocked-by: #11758 names objectui#11758, the one-time administrator prompt that the director's ruling 6030273339 (B2) made the gate. It closed at 2026-10-07T05:29Z, when PR objectui#11761 merged as 3bf8894. The landed note is 6031649859.

    • PR objectui#11729 re-checked on objectui main 3bf8894:

      • head 229db2b, draft, 10 files, +628/−33, first line Part of #11693, Clause-②: yes;
      • CI on that head: 43 runs, 40 success, 3 skipped, 0 failure;
      • its merge-base is 7300fca, 30 commits behind main. None of those commits touches any of the PR's 10 files, and git merge-tree of main with the head is clean;
      • its body already names the gate and this sequence.
    • Next, in order:

      1. the at-tier contract review that Clause-②: yes owes;
      2. the seat's ACCEPT;
      3. ready + auto-merge through the queue.

      The PR is Part of, so the seat closes this card after the merge, with the landing note.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T05:46Z. PR objectui#11729, head 229db2b.

    • PR shape: draft against main. First line Part of #11693, no closing keyword, so the seat closes this card after the merge. Clause-②: yes at line start. The body names the B2 gate, which closed with objectui#11758 (3bf8894). The merge-base is 7300fca, 30 commits behind main. None of those commits touches the PR's 10 files, and git merge-tree with main 3bf8894 is clean. Mergeable state clean.

    • Scope: 10 files, +628/−33. No governed path.

      • LocalizationFetchProvider.tsx and its test;
      • packages/core/src/utils/date-display.ts and a new pin;
      • packages/i18n/src/LocalizationContext.tsx and a new pin;
      • the core and i18n READMEs;
      • content/docs/fields/datetime.mdx ("Time Zone");
      • the changeset.

      The landing point is core's date-display.ts, not the i18n formatters the dispatch named. The dev measured that the i18n formatters have no renderer caller. That is within the card's "the central date formatters", the claim's stop-and-explain clause was honoured, and the contract review judged it right (①.10).

    • Ruling honoured: director's ruling 6030273339, letter B with gate B2. The zone is whatever the endpoint answers, the UTC floor included, and the gate (a one-time administrator prompt) landed first. Nothing in this PR changes for the ruling.

    • Diff read (the seat's own):

      • The fetch keeps timezone: json.timezone ?? undefined beside currency and locale.
      • LocalizationProvider hands the zone to core in a layout effect whose cleanup clears it. It reads the applied zone back through useSyncExternalStore, so useLocalization().timezone is the zone the faces actually render in, and an unknown IANA name reads undefined.
      • Core gains setDisplayTimeZone / getDisplayTimeZone / subscribeDisplayTimeZone. An unknown name clears the zone with one console.warn instead of every face throwing RangeError.
      • Each instant face spreads inZone(zone); with no zone the options bag is unchanged.
      • Date-only values stay exempt through a WeakSet registered on toDisplayDate's date-only branch and in toInclusiveEndDay.
      • No per-renderer special case.
    • Changeset sentences checked against the head:

      • the zone the server answers;
      • date-only keeps its day;
      • no zone means the viewer's zone, as before;
      • a server answering UTC for an unconfigured workspace moves it to UTC;
      • the three core symbols and LocalizationValue.timezone;
      • renderers with their own Intl options don't take the zone yet.

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

    • Reverse verification (dev report 6020328604):

      • the 16 core pins straddle the suite's UTC (America/Los_Angeles, Asia/Shanghai);
      • the console's "zone arrives after first render" case;
      • the ablation table, where row 6 (the context keyed on the prop instead of the applied zone) proves the read-back keying is load-bearing.
    • Gates: CI on 229db2b (head_sha equals the PR head): 43 runs, 40 success, 3 skipped (dependabot and the two coverage jobs), 0 failure. Type Check, README Export Check, Doc Snippet Type Check, Doc Example Id Check and Build Docs are green, covering the dev's narrowed local gates.

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

      • All 12 ① judgments are right.
      • ② semver matches.
      • ③ is answered item by item, including a read of the 30-commit main delta: none changes what the pins assert.
    • Open questions: open_questions[0] (the UTC floor) was ruled by 6030273339.

    Findings (the dev's five, plus the review's flags):

    • datetime renderers outside the shared core faces (record meta footer, comments, history and activity timelines, audit log, calendar views, gantt chart frame) do not take the zone. The card's ⛔ leaves them and the changeset names the limit. No public-door measurement was made → Acceptance notes (one family card once measured)
    • core FormulaFunctions TODAY() still answers the UTC day. ADR-0053 D-G1 wants the reference zone's day, and the zone now reaches the client (getDisplayTimeZone). No measured wrong result yet → Acceptance notes (dedupe words: TODAY reference timezone, FormulaFunctions UTC day)
    • first paint: the fetch provider renders before the answer, so a configured workspace's datetimes paint in the viewer's zone and then move to the served zone on each load. The locale has a seed for this gap and the zone has none. This was pinned deliberately, as before → Acceptance notes
    • after an administrator confirms the objectui#11758 prompt, the running session keeps the zone it was served until the next load. That is within the cost ruling B2 states → Acceptance notes
    • scripts/__tests__/date-only-zone-pins-10183.test.ts reds when a cached run orders a TZ-leaving pin file first (isolate: false). CI starts cache-less and stays green. Pre-existing → Acceptance notes
    • the @object-ui/i18n formatter family has no renderer caller and does not read the zone, which the i18n README now states; the vitest.config.mts header prose is stale → Acceptance notes

    Landing: ready + auto-merge through the queue, now. After the merge the seat closes this card (Part of) with the landing note.


    Generated by Claude Code

  8. objectstack-fleet commented on Oct 7, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — domain:ui seat 3, session_01CGZy1BGCjdN5cXqL9cnvB8, 2026-10-07T06:04Z. PR objectui#11729 merged through the queue as d50f724 at 2026-10-07T06:03Z. This card closes with this note: the PR was Part of, and nothing of the card's Done-when remains.

    • The merge:
      • One parent, 92f4e2b, an ancestor of origin/main.
      • The landed tree equals git merge-tree of that parent and the accepted head 229db2b (tree 5506d63b on both).
      • 10 files, +628/−33.
    • Content check, 92f4e2b → d50f724:
      • export function setDisplayTimeZone in core's date-display.ts: 0 → 1.
      • timezone: json.timezone in LocalizationFetchProvider.tsx: 0 → 1.
      • setDisplayTimeZone(timezone) in LocalizationContext.tsx: 0 → 1.
      • Control: the B2 gate's <WorkspaceTimezonePrompt /> in AppContent.tsx is 1 on both sides, so the gate stands in front of the change it guards.
    • Ruling closed out: director's ruling 6030273339, letter B with gate B2. The gate landed first as objectui#11758 (3bf8894); this PR followed. Its contract review is 6031806317 (PASS) and the ACCEPT is 6031829629.
    • Closures: objectui#11693 is closed here as completed, and pm:dispatched is removed in the same relay call. No other issue closed in the window. The follow-up candidates are recorded in the ACCEPT's Acceptance notes and none is filed, because none has a measured reach yet:
      • renderers outside the shared faces;
      • TODAY()'s day;
      • the first-paint zone.

    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 notificationsbugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions