Uh oh!
There was an error while loading. Please reload this page.
fix(console): /_console/setup is a stable deep link into platform administration - #4180
Merged
Merged
Conversation
…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
The latest updates on your projects. Learn more about Vercel for GitHub. |
Contributor
✅ Console Performance Budget
📦 Bundle Size Report
Size Limits
|
yinlianghui
marked this pull request as ready for review
August 10, 2026 17:54
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
/setupwas never a missing route. It was occupied.apps/console/src/App.tsx:183mounted 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:88—if (user && !submitting) window.location.assign('/')SetupPage.tsx:78—if (bootstrapped === true and no user) navigate('/login', ...)So a signed-in admin opening the deep link was sent to
/, whichresolveLandingPath()resolves to/homeon any multi-app deployment with noisDefaultapp. That is the card's exact observation, and being a full-pagelocation.assignis 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_overviewis a fact about Setup's navigation on the day it was measured (it isSETUP_NAV_CONTRIBUTIONS' firstgroup_overviewchild). The redirect therefore resolves the app root and lets the shell's existing landing resolution pick the page.What changed
/setupnow 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.A live session short-circuits the probe, because
hasOwner: falsecannot 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, matchingSetupPage's owncatch) from re-creating the bounce this card is about.No new routing idiom.
SetupRedirectis modelled onSystemRedirectsitting beside it inConsoleShell.tsx— resolve a target, oneNavigate replace, search and hash carried over. Its policy is the pure, router-freeresolveSetupAppPath(), the same shapeRootLandingRedirect'sresolveLandingPath()uses for/.The target is read from metadata, not spelled out.
resolveSetupAppPath()finds the Setup app and builds its segment withappRouteSegment()— the same helperconsole/home/AppCard.tsxuses — 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:/homeis the defect being fixed, and the bare/apps/setupisAppContent'sisSetupRoutepseudo-route, which resolves to the default app — it would silently render a different app./apps/com.objectstack.setupmatches no pseudo-route, soAppContent's ownrequestedAppMissingbranch 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=%2Fsetupand returns here after signing in.LoginRedirectbuilds that param from the router's location, so it stays correct under abase href-mounted basename (#4168's lesson).ProtectedRoute/LoginRedirectwere lifted out ofApp.tsxverbatim intocomponents/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 failureSetupPage's own "not mid-submission" guard was written for. Rather than freeze the verdict, the probe is gated on being unauthenticated, so afreshverdict can only come from a probe that ran with no session, and readingfreshfirst 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/studiois a declared front door renderingBuilderLandinginsideProtectedRoute, not a fall-through to the catch-all;App.tsx:210—/studio/:packageIdredirects to itsdatapillar;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) andapps/console/src/components/SetupRoute.test.tsx(14). The console file wires/to the realresolveLandingPathover a multi-app list, so/homeis 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.AuthGuardandLoginRedirectstay real;SetupPageandConnectedShellare 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:
resolveSetupAppPathforced to'/home'+SetupRoutereduced to the pre-fix unconditionalSetupPage→ 11 reds / 11 passed, includingTHE FIX: /setup lands on the Setup app, not back on homeandTHE FIX: the unauthenticated deep link carries a redirect back to /setup. That reproduces the card's bounce and the dropped deep link.fresh-first ordering moved below the session checks → exactly 2 reds:a fresh verdict outranks a session — that ordering IS the wizard guardandTHE 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
Build closure (
--filter '@object-ui/console^...' build) was run first — the console'stype-checkreads workspace.d.tsand reports 21 phantomTS2307s without it.Changeset:
@object-ui/console+@object-ui/app-shell, both patch.Generated by Claude Code