Skip to content

fix(console): the first-run setup exits land inside the console mount - #4186

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4181-setup-exit-basename
Aug 10, 2026
Merged

fix(console): the first-run setup exits land inside the console mount#4186
yinlianghui merged 1 commit into
mainfrom
claude/issue-4181-setup-exit-basename

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4181

Premise, re-measured at this tip

Holds, both sites, unchanged in substance since filing (they sit at :90 and :149 now rather than :88/:148):

  • SetupPage.tsx:90 — the already-signed-in bounce: window.location.assign('/')
  • SetupPage.tsx:149 — the success path, after signUp() and the bootstrap-org rename: window.location.assign('/')

location.assign does not go through React Router, so its basename never applies and a root-relative / leaves the SPA. PR #4180 had already landed on main and deliberately did not touch SetupPage, so this branched off a tip that includes it; nothing needed rebasing.

One correction to the filing, and it changes the shape of the fix: the helper was not only module-private to LoginPage. RegisterPage.tsx:30 already carried a byte-identical copy. The drift the card predicted as "next" had already happened, so the lift covers three call sites, not two.

Why the exits stay full-page navigations

The card asked this first, so it was answered before anything was written — and the answer is that the reload is load-bearing. withConsoleBase() fixes where the exits land, not what kind of navigation they are.

ConsoleShell.ConnectedShellInner mounts MetadataProvider as soon as auth resolves — it deliberately does not gate on isAuthenticated (objectui#4042) — and keys it on language alone. On a first-run deployment that means the metadata tree is already mounted and populated by reads that ran with no session, and nothing in its effect deps changes when signUp() creates one. The landing resolution the exit hands off to (RootLandingRedirect.resolveLandingPath) reads that app list, so a router navigate('/') would drop the brand-new owner into an appless console — a second dead end in place of the first.

The permission grant compounds it: the bootstrap runs off a permission-grant middleware that "may land moments after signUp() resolves" (SetupPage.handleSubmit's own comment), so even a re-fetch raced at exit time is not reliably the owner's world. Tearing the document down is what guarantees the console rebuilds with the session cookie present.

It is also what the two sibling auth surfaces already do for the same reason — LoginPage and RegisterPage both exit through window.location.assign(withConsoleBase(...)). So this keeps one idiom across all three, rather than inventing a fourth behaviour for the least-exercised page.

The rationale is recorded on POST_BOOTSTRAP_EXIT in the source and pinned by a test, so the next reader does not have to re-derive it.

The lift

withConsoleBase() moves to apps/console/src/utils/consoleBase.ts, byte-identical in body. LoginPage, RegisterPage and SetupPage all import it.

While documenting it I checked something the card did not ask about but which decides whether this fix works at all: the helper reads import.meta.env.BASE_URL, while the router reads the injected base href (App.tsx:resolveBasename). Two different sources — they agree for a reason rather than by construction, and that reason is now written down:

  • dev / default / mount — Vite resolves a relative base to / in serve mode, so the prefix is a no-op and both spellings coincide. This is exactly why a standalone os dev run can never reveal the bug.
  • the shipped embeddable build (vite.config.ts base: './', no VITE_BASE_PATH) — Vite 8 bakes BASE_URL as the literal ./, so the helper returns a relative url. location.assign resolves that against the document base URL, which is precisely the base href the framework CLI injects — the same element the router reads. The two sources meet in the browser.
  • pinned absolute base (VITE_BASE_PATH=/_console/) — BASE_URL is /_console/ and the helper returns an absolute path.

All three are now covered by tests, so the embedded case is asserted rather than assumed.

Tests

New: apps/console/src/utils/consoleBase.test.ts (8) and apps/console/src/pages/auth/__tests__/authExitBasename.test.tsx (13).

Landing is not asserted by string equality against the assign argument — that would only restate the implementation, and it cannot express the embedded build at all, where the correct target is the relative ./. Each assertion resolves the target against the document's base URL (the same resolution location.assign performs) and asks whether the result is inside the mount. The mount is configured the way a real deployment configures it: an injected base href plus the BASE_URL Vite baked into that build.

Beyond the two exits, the file pins that the success path still performs the org rename before exiting (the redirect change must not skip the work the page exists for), and that the exit remains a full-page navigation rather than a router one.

Reverse verification — direction predicted before running, then observed

Predicted: reverting only SetupPage.tsx to origin/main turns the four basename pins red, leaves the default-mount pins green, and leaves every LoginPage/RegisterPage pin green — the last being what shows the lift is behaviour-neutral rather than merely untested.

Observed, exactly that — 4 failed | 17 passed (21):

× SetupPage > the success path > THE FIX: lands inside the mount on an embedded console
× SetupPage > the success path > THE FIX: lands inside the mount on a pinned-base console
× SetupPage > the already-signed-in bounce > THE FIX: lands inside the mount on an embedded console
× SetupPage > the already-signed-in bounce > THE FIX: lands inside the mount on a pinned-base console

The four reds reproduce the filing's escape (/ instead of /_console/). consoleBase.test.ts stayed fully green — the helper was untouched by that revert — as did all seven LoginPage/RegisterPage pins and both default-/-mount pins. Restored, re-run: 21/21 green.

Local gates

pnpm exec vitest run --maxWorkers=2 apps/console/
  Test Files  33 passed (33)      Tests  343 passed (343)

pnpm --filter @object-ui/console type-check   → clean (tsc --noEmit && tsc -b)
pnpm --filter @object-ui/console lint         → 0 errors (193 pre-existing warnings)
eslint (the 6 touched files)                  → 0 errors, 4 warnings, all pre-existing
                                                 react-hooks warnings in effects not touched here
node scripts/check-control-bytes.mjs          → OK (3862 tracked text files)

Build closure (--filter '@object-ui/console^...' build) was run first, before type-check.

Changeset: @object-ui/console patch — user-visible on deployed consoles.


Generated by Claude Code

…#4181)

SetupPage finished the first-run owner bootstrap with window.location.assign('/')
at both of its exits — the success path after signUp() plus the bootstrap-org
rename, and the already-signed-in bounce. location.assign bypasses React Router's
basename, so on a console served under an injected `base href` (the framework CLI
injects one for every embedded deployment) a root-relative '/' resolves to the
ORIGIN root and drops a brand-new owner outside the SPA, on the first screen after
creating their account.

Both exits now route through withConsoleBase(). They stay FULL-PAGE navigations on
purpose: ConsoleShell mounts MetadataProvider once auth resolves rather than once
it authenticates (objectui#4042) and re-keys it on `language` alone, so the app
list read while nobody was signed in would survive a router navigation and land
the new owner in an appless console.

withConsoleBase was module-private to LoginPage and had ALREADY been copied
verbatim into RegisterPage, so the lift covers three call sites rather than the
two the card assumed. LoginPage/RegisterPage behaviour is unchanged and pinned as
unchanged across all three mount configurations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercel Bot commented Aug 10, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 10, 2026 7:02pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.3 KB 350 KB
Entry file index-DsvTPFTf.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.88KB 3.25KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 485.06KB 107.21KB
core (index.js) 3.04KB 1.15KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 140.66KB 36.25KB
fields (index.js) 229.40KB 56.93KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.87KB 10.80KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 45.23KB 12.45KB
plugin-charts (index.js) 61.52KB 17.49KB
plugin-chatbot (index.js) 180.33KB 42.79KB
plugin-dashboard (index.js) 118.52KB 30.68KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 237.80KB 59.48KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 114.58KB 27.68KB
plugin-gantt (index.js) 162.81KB 39.67KB
plugin-grid (index.js) 188.04KB 49.91KB
plugin-kanban (index.js) 48.60KB 13.41KB
plugin-list (index.js) 110.04KB 26.67KB
plugin-map (index.js) 17.00KB 5.32KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 26.21KB 7.52KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 23.71KB 7.95KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.23KB 0.66KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: the first-run setup wizard's completion redirect bypasses the console basename

2 participants