Skip to content

fix(console): /_console/setup is a stable deep link into platform administration - #4180

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-2794-setup-deep-link
Aug 10, 2026
Merged

fix(console): /_console/setup is a stable deep link into platform administration#4180
yinlianghui merged 1 commit into
mainfrom
claude/issue-2794-setup-deep-link

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Fixes#2794

Premise, re-measured at this tip

The card is 18 days old, so its premise was re-checked before anything was written. It holds — and the cause is not the one the card assumed.

/setup was never a missing route. It was occupied. apps/console/src/App.tsx:183 mounted the first-run owner-bootstrap wizard there (pages/auth/SetupPage, ported here when the Account SPA was retired), and that page evicts everyone it is not for:

  • SetupPage.tsx:88if (user && !submitting) window.location.assign('/')
  • SetupPage.tsx:78if (bootstrapped === true and no user) navigate('/login', ...)

So a signed-in admin opening the deep link was sent to /, which resolveLandingPath() resolves to /home on any multi-app deployment with no isDefault app. That is the card's exact observation, and being a full-page location.assign is why it read as "被重定向回 home" rather than as a routing error. The unauthenticated half was defective too, more quietly: it reached a bare /login, so signing in dropped the deep link entirely.

The card's literal target address needed correcting as well. apps/com.objectstack.setup/dashboard/system_overview — only the first segment pair is a fact about Setup; dashboard/system_overview is a fact about Setup's navigation on the day it was measured (it is SETUP_NAV_CONTRIBUTIONS' first group_overview child). The redirect therefore resolves the app root and lets the shell's existing landing resolution pick the page.

What changed

/setup now discriminates between its two meanings on the condition the wizard itself already probes — whether the deployment has an owner (GET /api/v1/auth/bootstrap-status). It is the honest discriminator because it is exactly the condition under which the wizard is the right page, and it is monotonic: a deployment crosses it once, forever.

  • no owner yet → the first-run wizard, unchanged;
  • otherwise → the platform-administration deep link.

A live session short-circuits the probe, because hasOwner: false cannot be true while somebody is signed in. That keeps the common case off the round-trip, and keeps a failed probe (which falls open to the wizard, matching SetupPage's own catch) from re-creating the bounce this card is about.

No new routing idiom.SetupRedirect is modelled on SystemRedirect sitting beside it in ConsoleShell.tsx — resolve a target, one Navigate replace, search and hash carried over. Its policy is the pure, router-free resolveSetupAppPath(), the same shape RootLandingRedirect's resolveLandingPath() uses for /.

The target is read from metadata, not spelled out.resolveSetupAppPath() finds the Setup app and builds its segment with appRouteSegment() — the same helper console/home/AppCard.tsx uses — so this alias and the home launcher's 「系统设置」 card cannot disagree about where Setup lives, and re-ordering Setup's nav cannot drift the alias.

The absent-app case is handled, not papered over.SETUP_APP.requiredPermissions = ['setup.access'], so "Setup is missing from metadata" is a normal permission outcome, not only a stripped build. The fallback is the canonical package-id URL, deliberately neither of the two tempting alternatives: /home is the defect being fixed, and the bare /apps/setup is AppContent's isSetupRoute pseudo-route, which resolves to the default app — it would silently render a different app. /apps/com.objectstack.setup matches no pseudo-route, so AppContent's own requestedAppMissing branch answers it with the "App not available" screen every other missing app gets, retry and one-shot metadata re-check included.

The auth-redirect contract is reused, not re-spelled. The settings branch goes through the console's existing ProtectedRoute, so an unauthenticated deep link becomes /login?redirect=%2Fsetup and returns here after signing in. LoginRedirect builds that param from the router's location, so it stays correct under a base href-mounted basename (#4168's lesson). ProtectedRoute / LoginRedirect were lifted out of App.tsx verbatim into components/ProtectedRoute.tsx — both were module-private, so no published surface widened; the move exists so the test exercises the real contract instead of a transcription that would be free to agree with itself.

No latch, and that is deliberate.signUp() flips the session to authenticated while the wizard is still renaming the bootstrap organization; re-deciding on that flip would unmount the wizard mid-submission and kill the rename — the failure SetupPage's own "not mid-submission" guard was written for. Rather than freeze the verdict, the probe is gated on being unauthenticated, so a fresh verdict can only come from a probe that ran with no session, and reading fresh first makes the decision immune to the flip. The ordering is the guard, and it is pinned as such.

/_console/studio — the consistency half

Measured, coherent, unchanged:

  • App.tsx:190 — bare /studio is a declared front door rendering BuilderLanding inside ProtectedRoute, not a fall-through to the catch-all;
  • App.tsx:210/studio/:packageId redirects to its data pillar;
  • it has a live in-product producer (console/home/HomePage.tsx:417, navigate('/studio')).

The card's /_console/studio// spelling no longer describes anything. Nothing was changed here, per the card's "align it only if it misbehaves".

Tests

New: packages/app-shell/src/console/__tests__/setupRedirectTarget.test.tsx (9) and apps/console/src/components/SetupRoute.test.tsx (14). The console file wires / to the realresolveLandingPath over a multi-app list, so /home is a destination the router can actually settle on — "never lands on home" is then an assertion about a reachable place rather than about an unwired stub. AuthGuard and LoginRedirect stay real; SetupPage and ConnectedShell are stubbed, because this measures routing rather than the wizard's form or the provider stack.

Reverse verification — direction predicted before running, then observed

Two independent reverts, both red as predicted:

  1. resolveSetupAppPath forced to '/home' + SetupRoute reduced to the pre-fix unconditional SetupPage11 reds / 11 passed, including THE FIX: /setup lands on the Setup app, not back on home and THE FIX: the unauthenticated deep link carries a redirect back to /setup. That reproduces the card's bounce and the dropped deep link.
  2. The fresh-first ordering moved below the session checks → exactly 2 reds: a fresh verdict outranks a session — that ordering IS the wizard guard and THE REGRESSION GUARD: signUp() flipping the session does not evict the wizard. Nothing else moved, which is what shows the ordering carries that property alone.

Local gates

pnpm exec vitest run --maxWorkers=2 packages/app-shell/
Test Files 323 passed (323) Tests 3030 passed | 1 skipped (3031)
pnpm exec vitest run --maxWorkers=2 apps/console/
Test Files 31 passed (31) Tests 322 passed (322)
pnpm --filter @object-ui/app-shell --filter @object-ui/console type-check → Done, Done
pnpm --filter @object-ui/app-shell --filter @object-ui/console lint → 0 errors
node scripts/check-control-bytes.mjs → OK (3851 tracked text files)

Build closure (--filter '@object-ui/console^...' build) was run first — the console's type-check reads workspace .d.ts and reports 21 phantom TS2307s without it.

Changeset: @object-ui/console + @object-ui/app-shell, both patch.


Generated by Claude Code

…inistration (#2794)
`/setup` bounced signed-in visitors to `/home`, so system settings had no
direct URL — unbookmarkable, unshareable, and asymmetric with Studio's
stable front door.
The route was never missing, it was occupied: `/setup` mounts the first-run
owner-bootstrap wizard, which evicts a signed-in visitor with
`window.location.assign('/')`, and the landing resolver turns `/` into
`/home` on any multi-app deployment.
`/setup` now discriminates on the condition the wizard itself probes —
whether the deployment has an owner. No owner: the wizard, unchanged.
Otherwise: `SetupRedirect`, a new app-shell alias beside `SystemRedirect`
that resolves the Setup app from metadata through the same
`appRouteSegment()` helper the home launcher's app cards use, and forwards
to the app root so `AppContent`'s existing landing resolution picks the page.
An unauthenticated deep link now travels the host's own auth-redirect
contract (`/login?redirect=%2Fsetup`, router-derived, basename-safe) and
returns here after signing in. A viewer whose metadata carries no Setup app
— usually a missing `setup.access` permission — gets the shell's ordinary
"App not available" screen rather than a silent landing on home.
`/_console/studio` was checked for the same asymmetry and needed no change.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 10, 2026 5:43pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)28.3 KB350 KB
Entry fileindex-D_vOA-KO.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)8.88KB3.25KB
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)485.06KB107.21KB
core (index.js)3.04KB1.15KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)140.66KB36.25KB
fields (index.js)229.40KB56.93KB
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)27.59KB6.63KB
i18n (useSafeTranslation.js)4.52KB1.96KB
layout (index.js)38.87KB10.80KB
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)45.23KB12.45KB
plugin-charts (index.js)61.52KB17.49KB
plugin-chatbot (index.js)180.33KB42.79KB
plugin-dashboard (index.js)118.52KB30.68KB
plugin-designer (index.js)210.51KB42.51KB
plugin-detail (index.js)237.80KB59.48KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)162.81KB39.67KB
plugin-grid (index.js)188.04KB49.91KB
plugin-kanban (index.js)48.60KB13.41KB
plugin-list (index.js)110.04KB26.67KB
plugin-map (index.js)17.00KB5.32KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.58KB10.58KB
plugin-timeline (index.js)26.21KB7.52KB
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)23.71KB7.95KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.23KB0.66KB
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

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.

console: /_console/setup 深链被重定向回 home,系统设置无直达路由

2 participants

@yinlianghui@claude