Skip to content

feat(app-shell): each read-only base card on the Studio landing says how many items are customized and switched off - #28634

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9g21fi
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9g21fi

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #28566

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:Studio 首页「已安装(只读)」区的每张基础包卡片多一行「已定制 n 项,已停用 m 项」(英文 "n customized, m switched off")。数字来自服务器的 GET /api/v1/packages/:id/customizations,每张卡读一次;n = 服务器给的覆盖数 + 扩展数,m = 服务器给的停用数。有定制包(BASE.custom)时「已定制」链到它的 Studio,「已停用」链到基础包的自动化页。
  • 为什么改:管理员升级前、或平时想一眼看出改过哪些已安装应用、停用了什么(ADR-0146 D3)。之前首页卡片上没有任何计数,全局视图只有 Setup 的 Packaged automation 页,且只覆盖流程和动作。
  • 风险与回滚:低。只读展示一次已有接口;读取中不显示数字,读取失败显示灰色「无法读取定制」(悬停看服务器原话),绝不显示 0;可写包和定制包本身不画这一行、也不发请求。回滚即还原本 PR。注意:main red: Dogfood Regression Gate — GET /packages/:id/customizations is ledgered but not mounted (#28595), so the route 404s at runtime #28622 记录该接口在 main 上尚未挂载,挂载前这一行在真实环境里会显示「无法读取定制」。
  • 先看:ui/packages/app-shell/src/views/studio-design/packageCustomizations-io.ts(读取与按 spec 校验)、BuilderLanding.tsx 的 renderCustomizationLine、BuilderLanding.customizationCounts-28566.test.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/views/studio-design/BuilderLanding.customizationCounts-28566.test.tsx,预期 1 个文件、6 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • A small reader beside packages-io. packageCustomizations-io.ts exports fetchPackageCustomizationCounts(baseId). It sends GET /api/v1/packages/:id/customizations and judges the dispatcher envelope's data by GetPackageCustomizationsResponseSchema from @objectstack/spec/api. It returns customized = served counts.overrides + served counts.extensions, and switchedOff = served counts.switchedOff. It counts nothing itself. A failed read rejects with the server's sentence through readEnvelopeFailureText, the rule every package reader in that folder uses, else HTTP STATUS. An answer the schema refuses rejects too, so no number is ever read out of an off-contract body. A pair id (isCustomizationPackage) is refused before any request, because the route refuses it by shape.
  • The landing line. BuilderLanding reads once per read-only base card drawn, after the package list answers. The effect is keyed on the joined target ids, a primitive (ui/AGENTS.md chore: version packages #10). The page does not wait for these reads. The line has three states:
    • pending: "Reading customizations…" with no number;
    • failed: a muted "Couldn’t read customizations", with the server's sentence as its title;
    • read: "n customized, m switched off".
      When the package list holds BASE.custom (from customizationPackageId), "n customized" links to /studio/BASE.custom/data. Otherwise it is plain text. "m switched off" links to /studio/BASE/automations, the pillar where the switch lives.
  • No line on a writable package or on a pair. A pair served read-only is still a pair, so it gets no line either.
  • Strings. Seven engine.studio.landing.customizations.* keys go into @object-ui/i18n's studio namespace, in en and zh. The zh line reads 「已定制 n 项,已停用 m 项」, as the card asks. The other languages fall back to en, as the whole namespace does by design. Placeholders are customized, switchedOff and pair, single-brace tFormat tokens. There is no count token, so the i18next count-family gate does not apply.

Tests

BuilderLanding.customizationCounts-28566.test.tsx runs the real landing and the real reader under a real router. Only the package list and fetch are stubbed, with answers shaped by the route's schema inside the dispatcher envelope. The six cases are the card's five plus one:

  • 3 overrides, 1 extension and 2 switched off read "4 customized, 2 switched off". The pair link goes to /studio/hotcrm.custom/data and the switched-off link to /studio/hotcrm/automations. One request is sent, for the base only.
  • A base with no pair reads "0 customized, 0 switched off" with no pair link.
  • A failed read (503 with an error envelope) shows the failed state, no digit, and the server's sentence in the title.
  • An answer the schema refuses (no counts) shows the failed state with no digit. This is the extra case.
  • A writable package, a writable pair and a read-only pair draw no line and send no request.
  • The landing paints while both reads stay pending, and both lines show no number.

Four existing landing tests (accessibleNames-12510, orgScope-11553, recentPackages-11863, setupExit-12513) draw a read-only card. The new read reached a real socket from them, and the ui network-escape guard refused it. They now mock the reader with a pending promise, the way they already mock packages-io. No assertion changed.

Acceptance notes

Verification

New test file · 6/6

pnpm --dir ui exec vitest run --maxWorkers=2 packages/app-shell/src/views/studio-design/BuilderLanding.customizationCounts-28566.test.tsx under os-verify-lock: Test Files 1 passed (1), Tests 6 passed (6).

Ablations · two red, one green explained

All three ran through scripts/ablation-replace.mjs against the committed fix. The tree was restored after each run, and git diff HEAD was empty.

  1. customized: counts.overrides (the extensions term dropped): 1 failed, the "4 customized, 2 switched off" case.
  2. The landing's pair filter dropped alone: 6/6 green. The reader's own pair guard still refuses the id before a request, and the line renderer draws nothing for a pair.
  3. The reader's pair guard and the landing's pair filter dropped together: 1 failed, the "no line and no request for a writable package or a pair" case.
Every test naming the landing or the reader · 22 files

git grep -l "BuilderLanding\|packageCustomizations-io" over ui test files (22 files): Test Files 22 passed (22), Tests 120 passed (120). Before the four stubs: 4 failed, all with the network-escape refusal. The tests that reach the landing through routes (registerStudioComponents.builderLazy-11798, StudioRoute.landingI18n, AppContent.studioRootLanding-12113, UnifiedSidebar.studioNoHome-12113): 4/4 files, 16/16 tests. This is a declared narrowing. CI's Test UI runs the rest of app-shell.

Type check and i18n
  • pnpm --filter @object-ui/app-shell run type-check: exit 0, after turbo run build --filter='@object-ui/app-shell^...' (64 tasks).
  • pnpm --dir ui exec vitest run packages/i18n/: Test Files 94 passed (94), Tests 1438 passed, 13 skipped. This includes the studio namespace parity test.
Derived gates · 34 of 34 exit 0 at 7b29406

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 34 commands against merge base 0e78922ee. All 34 ran and exited 0. That count includes check-sdui-manifest.mjs and its --self-test, check:type-check-debt under the lock, check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias and pnpm --dir ui exec vitest run scripts/ (Test Files 169 passed, 2 skipped; Tests 4900 passed). dispatch-gates --ran reconciled 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN. pnpm --dir ui run check:new-line-citations found 0 new citations. The head then took one merge of main (d77dc07a, two ui/e2e/live files, no overlap with this diff).


Generated by Claude Code

…tomized, m switched off"

Each read-only base's card reads GET /packages/:id/customizations once and
draws the served counts: customized = overrides + extensions, switched off as
served. A pair draws no line and is never sent; loading shows no number; a
failed read says so, with the server's sentence as its title.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jRL4qcnwHDD4M1e4wx
…tests; add the changeset

The landing now reads each read-only base's customizations. Four landing
tests that are not about it would send that read to a real socket, so they
stub the reader with a pending promise.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jRL4qcnwHDD4M1e4wx
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 1c9ccf15982869ee086d152d51230eaced795114
Local-runs: none

① Derived judgments

  • Face: the Clause-②: yes (widening) declaration alone. The PR's 10 files (.changeset/, ui/packages/app-shell/src/views/studio-design/**, ui/packages/i18n/src/namespaces/studio/{en,zh}.ts) touch no packages/spec/src/** and no governed path (check-governed-merges --pr 28634).
  • Published surface moved: @object-ui/i18n's studio namespace gains seven runtime entries, engine.studio.landing.customizations.{customized,separator,switchedOff,loading,failed,pairTitle,switchedOffTitle}, in en and zh. The table's type is Record<string, string> (const STUDIO_EN), so no published literal key type widens; the move is additive catalogue content only. No key is removed or renamed and no existing value changes. Declaring it yes (widening) is the conservative reading and is kept; it changes no level.
  • @object-ui/app-shell is private: true; packageCustomizations-io.ts is not re-exported. The read consumes the already-published GetPackageCustomizationsResponseSchema (@objectstack/spec/api, workspace:^) and customizationPackageId / isCustomizationPackage (@objectstack/spec/kernel) without changing them. No accept set moves.

② Semver level

  • Changeset: @object-ui/app-shell patch, @object-ui/i18n patch, Clause-②: yes (widening) and Metadata-protocol: none line-initial in the changeset and the PR body. Additive with no type change takes patch: consistent.

③ Boundary flags

Implemented-by: claude/project-thread-9g21fi
Reviewed-by: session_01QsR2jR7ZFkpqmm4U8S1f9y

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · zhuangjianguo · session_01QsR2jR7ZFkpqmm4U8S1f9y

席位核对 · ACCEPT · 2026-11-01T11:09Z

Head-sha: 1c9ccf15982869ee086d152d51230eaced795114

  • Shape: draft → main, first content line Fixes #28566, no other card number beside a closing keyword. 10 files, +450 / −3 (453 lines ≤ 3000), no governed path (check-governed-merges --pr 28634). Author objectstack-fleet[bot]. Clause-②: yes (widening) / Metadata-protocol: none line-initial in body and changeset, as claimed. Contract review TIER_DEFAULT PASS 6557127021 at this head.
  • Wanted, item by item: (1) each read-only base card carries "n customized, m switched off" / 「已定制 n 项,已停用 m 项」, with n = served overrides + extensions and m = served switchedOff, judged by GetPackageCustomizationsResponseSchema; (2) pending shows no number, failed or off-contract shows the muted "Couldn't read customizations" with the server's sentence as title, never "0"; (3) "n customized" links the pair's Studio Data only when the list holds <base>.custom, and "m switched off" links the base's Automations; (4) no line and no request on a writable package or a pair, and the reader refuses a pair id before any request; (5) one read per read-only base card, and the landing paints first. All hold in the diff (packageCustomizations-io.ts, BuilderLanding.tsx).
  • Live probe (dev, 1c9ccf15, examples/app-crm, built Console): GET /api/v1/packages/com.example.crm/customizations → 200, 0/0/0 on a fresh DB. After switching off crm_convert_lead_wizard → switchedOff: 1. The pair id → 400 naming its base. The card reads "0 customized, 1 switched off" from exactly one request.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending, all ten required contexts included (Build Core skipped by its path filter on a ui-only diff). The head moved from 3d90f3d4 by a merge of main at 6e11a929 only. The diff against main is unchanged (10 files), and the dev re-ran the landing tests at the new head: 24 files, 128/128.
  • PR body correction: the body's "main red: Dogfood Regression Gate — GET /packages/:id/customizations is ledgered but not mounted (#28595), so the route 404s at runtime #28622: the route is not mounted on main yet" Acceptance note, and the matching 风险与回滚 sentence, are stale. main red: Dogfood Regression Gate — GET /packages/:id/customizations is ledgered but not mounted (#28595), so the route 404s at runtime #28622 landed as 28ef35e0, and the live probe above drew the counts. This record supersedes both lines.
  • Tests: new BuilderLanding.customizationCounts-28566 (6 cases: 3 overrides + 1 extension + 2 off reads "4 customized, 2 switched off" with the pair link; a base with no pair reads 0/0 with no link; failed read shows no number; writable and pair send nothing; landing paints before the counts). Three ablations, two red as expected; the third stays green because the reader's own pair guard covers it. Four older landing tests mock the new read as pending.
  • Out of scope / NOT MEASURED: other languages fall back to en for the seven new keys, as the whole studio namespace does. Skills-docs: not owed.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #28566 closes the card on merge. Serial neighbour PR feat(studio): an All metadata area in the builder, per package and across packages #28623 also edits BuilderLanding.tsx (a different hunk) and the studio packs. The queue rebuilds whichever lands second.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 11:11
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 11:11
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 5e879ea Nov 1, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9g21fi branch November 1, 2026 11:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio: the landing card of each installed base says "n customized, m switched off", read from the per-package customization read

1 participant