Skip to content

fix(console): an inaccessible landing app bounces to /home instead of stranding the user chrome-less (#4473) - #4483

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4473-landing-no-strand
Aug 12, 2026
Merged

fix(console): an inaccessible landing app bounces to /home instead of stranding the user chrome-less (#4473)#4483
yinlianghui merged 1 commit into
mainfrom
claude/issue-4473-landing-no-strand

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Closes#4473

The defect

Switching the active organization (and accepting an invitation, which switches as its last step) could land a member on /apps/setup rendering the bare "no apps configured" screen — document.querySelectorAll('header').length === 0: no top bar, no navigation, no workspace switcher, and no way back except editing the URL by hand.

Measured chain, end to end:

  1. WorkspaceSwitcher.handleSwitch full-page-navigates to the console root (window.location.href = resolveRootUrl()) — correct, and untouched here.
  2. MetadataProvider seeds the app list from sessionStorage (objectui:metadata:app, keyed by TYPE only, not by org) and clears loading before the org-scoped fetch lands, so RootLandingRedirect resolves the landing from the PREVIOUS workspace's list — its single visible app, setup — and rule 2 sends the user to /apps/setup.
  3. GET /api/v1/meta/apps is already filtered PER SESSION server-side (filterAppForUser, packages/rest/src/rest-server.ts — see meta apps by-name route: answer an explicit permission-denied envelope for an unauthorized session, instead of being indistinguishable from "not published" (backend half of objectui#4252) objectstack#8013), so for a member with no app access the settled list is empty.
  4. In AppContent, isSetupRoute suppresses requestedAppMissing, the pseudo-route fallback finds no launcherApps[0], and the !activeApp guard returns a bare divabove the single ConsoleLayout mount. That early return is why there is no chrome: it is not a route mounted outside the shell, it is a return that precedes the shell.

The invariant

Switching into an organization never strands the user. The guard lands at the app surface, so a hand-typed /_console/apps/:name URL is covered identically; the resolver (RootLandingRedirect) stays a metadata-only product decision and is not touched.

What changed

packages/app-shell/src/console/AppContent.tsx — when the surface has no app to enter and the viewer is not a workspace admin, it returns a Navigate element to /home with replace instead of the chrome-less empty state. Router-relative on purpose: Navigate resolves through the host's basename, so the /_console mount is preserved without building a URL by hand (resolveConsoleUrl is for full-page navigations that leave the router). RequireAiSurface in ConsoleShell.tsx already bounces the same way for a surface the runtime cannot serve.

The role-aware condition is not a stand-in for per-app access — it is what the empty state's own copy already presupposes. "No apps configured — create your first app, or go to system settings" asserts a WORKSPACE-level fact that a per-user-filtered list cannot establish, and offers two actions a non-admin cannot perform. For a workspace admin it stays the deliberate first-run surface (#3573 / #3590); for everyone else /home is the honest destination — it renders inside the shell chrome and already carries the role-aware copy for exactly this state ("No applications yet — your workspace is being set up…", console/home/HomePage.tsx), which the issue verified renders correctly in the same state.

The half that stays gated (dependency)

Telling "you may not enter THIS app" apart from "this app is not published" per app needs the backend permission-denied envelope, objectstack-ai/objectstack#8013 → console half #4252. Nothing here waits on it: the bounce reads only the per-user-filtered list that ships today, so /meta/apps returning an app is the whole "can enter" signal it needs. When #4252 lands, the requestedAppMissing branch ("App not available — it may still be publishing") is where the denied-vs-unpublished distinction belongs; that branch is deliberately unchanged here.

Red-first

New pin packages/app-shell/src/console/__tests__/AppContent.inaccessibleAppStrand.test.tsx. Against origin/main's AppContent.tsx (taken out with git checkout, restored after):

 ❯ AppContent.inaccessibleAppStrand.test.tsx (5 tests | 3 failed)
× bounces a member with no accessible app off /apps/setup instead of the chrome-less empty state
× covers a hand-typed /apps/:name URL at any depth, not just the resolved landing
× replaces the strand rather than pushing over it
Tests 3 failed | 2 passed (5)

The failure dump is the strand itself: the div.h-screen.flex.items-center.justify-center wrapper carrying empty.noAppsConfigured, create-first-app-btn, go-to-settings-btn — and zero header elements. After the fix, 5 passed (5).

The two cases that pass in BOTH directions are the must-not-change ones, and that is the point:

  • a user WITH access still enters the app normally (console-layout with data-active-app="crm", URL unchanged);
  • a workspace admin with zero apps still gets the first-run empty state.

The history assertion uses react-router's useNavigationType() rather than window.history.back(): under MemoryRouter the window history object is inert, so a history.back() there would have gone green whatever the bounce did.

Knock-on to existing pins

Four suites drove the zero-app empty state with useIsWorkspaceAdmin: () => false; that screen now renders for admins only, so their viewer mock is flipped to true (with the reason recorded in each file). The subjects those files measure — CTA target resolution (#3573 / #3590), component/metadata routes in the zero-app branch (#3610), pseudo-route segment matching (#3638), the legacy /system/* redirect targets (#3655) — are viewer-independent and their assertions are unchanged.

Verification

  • pnpm exec vitest run packages/app-shell/ apps/console/402 files / 3933 passed, 1 skipped.
  • pnpm --filter @object-ui/app-shell type-check (tsc --noEmit && tsc -p tsconfig.test.json) → exit 0; pnpm --filter @object-ui/console type-check (tsc --noEmit && tsc -b tsconfig.node.json --force) → exit 0. Dependency closures built first (--filter '@object-ui/app-shell^...' build).
  • Emitted declaration dist/console/AppContent.d.ts built from origin/main vs from this branch: byte-identical — no public type surface change, hence patch.
  • eslint on the six touched files: 0 errors (37 pre-existing any warnings).
  • node scripts/check-changeset-presence.mjs → OK (.changeset/landing-app-no-strand-4473.md, @object-ui/app-shell patch); node scripts/check-control-bytes.mjs → OK.

Generated by Claude Code

… stranding the user chrome-less (#4473)
Switching organization could land a member on `/apps/setup` rendering the bare
"no apps configured" screen: no header, no navigation, no workspace switcher,
and no way back except editing the URL by hand.
`GET /meta/apps` is filtered per session server-side (`filterAppForUser`), so an
empty list means "nothing here is yours to open", not "this workspace has no
apps". The app surface now redirects to `/home` — which renders inside the shell
chrome and already carries the role-aware copy for this state — whenever it has
no app to enter and the viewer is not a workspace admin. A workspace admin keeps
the first-run empty state whose CTAs only they can act on; a user with access to
the app enters it unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Qqyix2QcnpUC9XeYVDzx3
@vercel

vercelBot commented Aug 12, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 12, 2026 7:18pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-CcOZmQD7.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
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)489.32KB108.45KB
core (index.js)2.99KB1.14KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)153.42KB41.19KB
fields (index.js)228.99KB56.82KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.32KB1.77KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.33KB1.20KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.98KB10.85KB
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.75KB3.80KB
plugin-calendar (index.js)45.24KB12.46KB
plugin-charts (index.js)62.01KB17.63KB
plugin-chatbot (index.js)181.17KB43.03KB
plugin-dashboard (index.js)120.75KB31.38KB
plugin-designer (index.js)212.58KB42.83KB
plugin-detail (index.js)239.03KB59.77KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)114.58KB27.68KB
plugin-gantt (index.js)164.14KB39.98KB
plugin-grid (index.js)188.13KB50.00KB
plugin-kanban (index.js)48.62KB13.42KB
plugin-list (index.js)110.20KB26.79KB
plugin-map (index.js)18.16KB5.81KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)40.99KB10.74KB
plugin-timeline (index.js)26.21KB7.52KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.08KB20.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.73KB7.96KB
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 (dashboard-filter-alias.js)6.23KB2.74KB
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)3.05KB1.52KB
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

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — step-7 复核 by PM session session_017Qqyix2QcnpUC9XeYVDzx3 (focused review).

Flipping ready + arming auto-merge. Slot refills next; the admin-zero-apps chrome-less first-run screen is noted as a product question for the maintainer, not filed (deliberate design with working CTAs).


Generated by Claude Code


Generated by Claude Code

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: switching organization can land a member on a chrome-less "no apps" page — resolveLandingPath ignores per-user app access

2 participants

@yinlianghui@claude