Skip to content

fix(console): 把已退场的 system/{users,organizations,roles,positions} 四条 URL 声明为重定向,直达框架系统对象 (#3655) - #3673

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3655-system-hub-routes
Aug 7, 2026
Merged

fix(console): 把已退场的 system/{users,organizations,roles,positions} 四条 URL 声明为重定向,直达框架系统对象 (#3655)#3673
yinlianghui merged 1 commit into
mainfrom
claude/issue-3655-system-hub-routes

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Fixes-part-of #3655(4/5:users/organizations/roles/positions 四条已交付;第五条 permissions 腿按有界授权停手,等维护者在 #3655 裁决 A/B/C 后另行落地 — 详见下文「为什么 permissions 留着不动」)

注:正文刻意不写 Route 的尖括号形式 —— GitHub 的正文消毒器会把「尖括号 + 字母」当成 HTML 标签在存储时剥掉。本 PR 首版正是在两处栽了这个跟头(存回来一读就发现是两个空反引号),现已改用无尖括号写法。

先复核:issue 结论成立,且比正文更细

issue 的结论来自静态路由表推导,派发单要求先做路由级实测复核。本 PR 的测试文件挂载 app-shell 真实的AppContent(DefaultAppContent)+ 本宿主真实的systemRoutes fragment,五条 URL 逐条渲染。这一点在这里比平时更要紧:缺陷本身就是「本 fragment 与 app-shell 尾部路由的相互作用」,手抄一份尾部路由等于让它自动同意 fragment 的任何行为。

实测落点(有应用分支,即 System Hub 卡片正常可达的那条):

URL修前落点(实测)修后落点(实测)
/apps/setup/system/usersRouteNotFound — "Page not found"一跳 -> /apps/setup/sys_user,ObjectView 渲染 sys_user
/apps/setup/system/organizations重写为 /apps/setup/system/record/organizations,RecordDetailView 收到 objectName: "system" / recordId: "organizations"一跳 -> /apps/setup/sys_organization
/apps/setup/system/rolesRouteNotFound — "Page not found"一跳 -> /apps/setup/sys_position
/apps/setup/system/positions重写为 …/system/record/positions,RecordDetailView 收到 objectName: "system" / recordId: "positions"一跳 -> /apps/setup/sys_position
/apps/setup/system/permissions重写为 …/system/record/permissions,RecordDetailView 收到 objectName: "system" / recordId: "permissions"未变(见下「为什么 permissions 留着」)

与 issue 正文的两处修正,均以实测为准:

  1. 零应用分支根本没有那条长度分叉。 issue 说「三个跳进不存在对象 system 的记录页」,这只在有应用分支成立。AppContent 的无 activeApp 路由表里既没有 :objectName/:maybeRecordId 也没有 :objectName/record/:recordId,所以 ShorthandRecordRedirect 压根不参与 —— 零应用下五条全部RouteNotFound。这条很关键,因为两个 sidebar 的 sys-*只在 activeApp 为假时渲染(AppSidebar.tsx 自己的注释),也就是说 sidebar 那三条入口平时走的正是这个分支。
  2. looksLikeRecordId 的过宽判定只作记录、不改动(派发单要求)。本 PR 一行都没碰它;它造成的长度分叉现象在测试里以 MEASUREMENT 形态钉住(system/teams 这类短词仍落 404),供后续单据引用。

处置分支:(b) 路由侧重定向,不动导航文件

派发单给了三个分支,实测把它定死在 (b):

目标对象名逐条取自框架源码,不是猜的:

入口目标对象依据
userssys_usersetup-nav.contributions.tsnav_users
organizationssys_organizationnav_organizations(列表那条)。另一条 nav_organizationrecordId: '{current_org_id}',需要运行期会话值,静态重定向解析不了,故取列表
rolessys_positionADR-0090 D3 把 sys_role 改名为 sys_position;sidebar 的 "Roles" 与 Hub 的 "Positions" 是同一个面的新旧两种叫法。全仓无 sys_role 对象
positionssys_positionplugin-security 的 nav_positions

为什么 permissions 留着不动

框架把本 console 叫作 "Permissions" 的东西拆成了两个 Setup 条目,证据两边都硬:

  • sys_capability(导航标签 "Capabilities")—— 其对象 docblock 明写「Named sys_capability (not sys_permission as the ADR loosely floats)」,而 sys_permission 正是被删掉的那个页面、以及 SystemHubPage 计数查询今天仍在用的名字。血缘指向它。
  • sys_permission_set(导航标签 "Permission Sets")—— 权限文档称其为「the only capability container」,是带管理员 CRUD 的授予容器,和卡片描述「Manage permission rules and assignments」以及它在 Positions 旁边的位置对得上。功能指向它。而 sys_capabilitymanagedBy: 'config' + protection.lock: 'no-overlay' 的平台锁定注册表,对它做管理员增删本就没有意义。

任选其一都会把此后每一次点击与每一个书签静默绑到一个维护者没有选过的面上 —— 这正是「消费端宽容」最典型的藏错处。派发单的 (b) 分支写明「redirect 目标以等价物实测为准」,而实测给出的是两个都能渲染的等价物,该条无法定案,故按「停手报告」处理:这一腿保持原样,并在测试里把它未变的落点钉住,让缺口显式可见、而不是读起来像漏掉了一条。裁决落地后,改的就是那条钉子。

零应用分支:测量,不是掩盖

重定向对两个分支同时生效(本宿主把同一个 fragment 传给 extraRoutesextraRoutesNoApp)。零应用下目标 /apps/setup/sys_user 不含 system / metadata 路径段,于是离开伪路由家族,落到「No Apps Configured」空状态。

这是诚实的终点而非被 stub 掩盖的回归:metadata 里一个 app 都没有的部署,同样没有 sys_user 可渲染。变的只是换了一个死胡同 —— 修前这里五条全是 RouteNotFound。测试里以专门一组用例钉住这个事实,而不是把它断言掉。

逆向验证(先预测,后运行)

预测:删掉那四行 path="system/…" 的路由声明后,四条一跳断言与四条零应用断言应转红,落点回到修前的两类;而三条 MEASUREMENT 断言(permissions 记录页、短词 404、零应用 permissions 404)不应受影响,因为它们不依赖这四条路由。

实测:13 passed -> 10 failed / 3 passed,失败清单与预测逐条吻合,失败现场直接打印出两类旧落点:

× /apps/setup/system/users reaches /apps/setup/sys_user in ONE hop
× /apps/setup/system/organizations reaches /apps/setup/sys_organization in ONE hop
× /apps/setup/system/roles reaches /apps/setup/sys_position in ONE hop
× /apps/setup/system/positions reaches /apps/setup/sys_position in ONE hop
× preserves the app prefix rather than stripping to the root
× an app segment spelled `system` survives the rewrite
× (4x) … still redirects, and the target is the no-apps empty state
TestingLibraryElementError: Unable to find an element by: [data-testid="object-view"]
data-testid="record-detail-view"
{"appName":"setup","*":"system/record/organizations","objectName":"system","recordId":"organizations"}
AssertionError: expected [ '/apps/my-app/system/users' ] to deeply equal [ '/apps/my-app/system/users', …(1) ]
- "/apps/my-app/sys_user",

测试

从仓库根跑,重活走共享 flock + --max-old-space-size=4096 + --maxWorkers=2:

  • pnpm exec vitest run --project '@object-ui/console' -> 25 files / 232 tests passed
  • pnpm exec vitest run packages/app-shell/src/console packages/app-shell/src/layout -> 46 files / 268 tests passed(消费半径清扫:systemRedirectTarget.test.tsx:62 是全仓唯一另一处提到 /system/users 的断言,它测的是 SystemRedirect改写产物,发生在本 PR 的路由之前,故不受影响 —— 已跑绿确认)
  • pnpm --workspace-concurrency=2 --filter @object-ui/console type-check -> 通过(新树里先 --filter '@object-ui/console^...' build 建好依赖)
  • pnpm --workspace-concurrency=2 --filter @object-ui/console lint -> 0 errors(190 warnings 全为既有 no-explicit-any;AppContent.tsx 唯一那条 react-refresh/only-export-components 来自既有的 systemRoutes 导出,非本次新增)
  • node scripts/check-control-bytes.mjs -> OK;node scripts/check-changeset-no-major.mjs -> OK

文件面

  • apps/console/src/AppContent.tsx —— 新增 SystemObjectRedirect + 四行路由声明
  • apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx —— 新增
  • .changeset/system-hub-routes-3655.md —— patch

⛔ 未触碰:SystemHubPage.tsx / AppSidebar.tsx / UnifiedSidebar.tsx(#3660 在途)、ShorthandRecordRedirect 的判定逻辑、packages/app-shell 的任何路由声明。

越界发现(只报不改,均已单独立单、未认领)


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt

…ositions} URLs as redirects (#3655)
SystemHubPage's cards and both sidebars' `sys-*` cluster emit five
`/apps/setup/system/…` targets whose host declares no route. They fell
through to app-shell's tail, where the failure depended on how long the
word was, because `looksLikeRecordId` treats any URL-safe segment of 6+
chars as a record id: `users`/`roles` reached `RouteNotFound`, while
`organizations`/`positions`/`permissions` were rewritten to
`…/system/record/<word>` and rendered a record detail page for an object
literally named `system`.
Four now forward in one hop to the object the framework's own Setup
navigation names — `sys_user`, `sys_organization`, and `sys_position`
for both `roles` and `positions` (ADR-0090 D3 renamed `sys_role` to
`sys_position`). Same shape as the `system/objects` / `system/metadata`
redirects beside them: the URL is translated, the page deleted in
cccdf84 is not resurrected, and no navigation producer is touched.
`system/permissions` is deliberately unchanged: the framework splits what
this console calls "Permissions" into `sys_capability` and
`sys_permission_set`, and picking one would silently commit every click
and bookmark to a surface nobody chose. Its landing is pinned so the gap
stays visible.
The new test renders app-shell's REAL AppContent with this host's REAL
`systemRoutes`, so the tail routes under test are the shipped ones rather
than a transcription; both pre-fix landings and the zero-app branch (no
`:objectName` route there, so all five reached `RouteNotFound` and the
length split does not exist) are measured rather than asserted away.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 7, 2026 6:33pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.1 KB350 KB
Entry fileindex-DJhxRrs5.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.66KB3.13KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)7.57KB2.97KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)22.10KB4.37KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)35.76KB9.11KB
auth (createAuthenticatedFetch.js)4.37KB1.69KB
auth (index.js)2.35KB1.07KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)4.91KB0.87KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)480.72KB105.64KB
core (index.js)2.96KB1.13KB
create-plugin (index.js)9.28KB2.98KB
data-objectstack (index.js)137.51KB35.11KB
fields (index.js)230.87KB56.83KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)2.65KB1.06KB
i18n (pickLocalized.js)1.70KB0.83KB
i18n (provider.js)9.48KB3.27KB
i18n (useObjectLabel.js)26.14KB6.07KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.53KB10.71KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)8.75KB3.06KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)3.67KB1.12KB
permissions (evaluator.js)4.41KB1.44KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.55KB0.71KB
plugin-ai (index.js)15.71KB3.79KB
plugin-calendar (index.js)44.98KB12.37KB
plugin-charts (index.js)61.04KB17.31KB
plugin-chatbot (index.js)180.09KB42.72KB
plugin-dashboard (index.js)115.50KB29.96KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)232.79KB57.42KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)112.10KB27.10KB
plugin-gantt (index.js)162.55KB39.57KB
plugin-grid (index.js)186.61KB49.34KB
plugin-kanban (index.js)48.30KB13.28KB
plugin-list (index.js)105.12KB25.48KB
plugin-map (index.js)16.81KB5.24KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)25.76KB7.33KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.03KB20.55KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.71KB3.53KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.67KB2.37KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)19.28KB6.38KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.02KB0.55KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.47KB2.03KB
sdui-parser (parse.js)10.04KB2.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)2.71KB1.34KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@yinlianghui
yinlianghui marked this pull request as ready for review August 7, 2026 18:45
@yinlianghui
yinlianghui added this pull request to the merge queueAug 7, 2026
Merged via the queue into main with commit 9961df2Aug 7, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3655-system-hub-routes branch August 7, 2026 18:45
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…jectstack-ai#3673/objectstack-ai#3699 reality (objectstack-ai#3700) (objectstack-ai#3705)
All nine [x] in "ObjectView-Driven System Pages (P1.12.2)" had gone false:
the shared component was deleted (PR objectstack-ai#3699), four URLs became redirects onto
framework objects (PR objectstack-ai#3673), /system/permissions is deliberately undeclared,
the audit page is a standalone REST page, and the 22-test suite is gone. The
Permission Management Page block above it drifted from the same source.
Rewritten per this file's own retirement convention (an italic _(Retired — …)_
annotation on the sub-header plus rewritten [x] items carrying a bare PR ref,
as at :152 ViewDesigner and :1046 tab-drag). The /system/permissions successor
stays neutral and unchecked — pending the maintainer's call in objectstack-ai#3655.
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…n_set (objectstack-ai#3655) (objectstack-ai#3728)
Fifth and last of the five `system/*` navigation targets. `system/permissions`
and the hub's Permissions count were both held back (PR objectstack-ai#3673 / objectstack-ai#3680) because
the framework splits this console's "Permissions" into `sys_capability`
(ADR-0066 layer 1, the definition registry) and `sys_permission_set` (layer 2,
the grant container the permissions docs call "the only capability container").
objectui#3655 decided it as `sys_permission_set`: the card reads "Manage
permission rules and assignments", and rules-and-assignments is layer 2.
- route: `system/permissions` -> `SystemObjectRedirect objectName="sys_permission_set"`
- count: `dataSource.find('sys_permission')` -> `'sys_permission_set'`
- the two MEASUREMENT pins written to be replaced when this landed are replaced;
the length-split fact they also carried is re-pinned on segments that are
still undeclared in both route tables.
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yinlianghui@claude