From b08e0b96d2ed4cd22244fb9cc7d4742ab2c8a9f3 Mon Sep 17 00:00:00 2001 From: NubsCarson Date: Wed, 1 Jul 2026 22:15:53 +0000 Subject: [PATCH] fix(ui): elizacloud.ai apex lands authenticated users on the cloud console (credits/manage), not chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both elizacloud.ai (the cloud console) and app.elizacloud.ai (the agent app) serve the SAME packages/app bundle, and the shell kept 'chat is home' on every host — so when signed in, the two domains rendered the identical chat app and the apex never surfaced its reason to exist (add credits / manage the account). CloudRouterShell.AppCatchAllRoute now, on an apex control-plane host (elizacloud.ai / www / staging / dev), routes an authenticated visitor who lands on the bare root '/' to the console home ('/settings#billing' — the canonical credits/billing surface, whose tabbed shell of Billing · Developer/API keys · Connections · Organization is the account-management hub). Unauthenticated apex → Steward /login is unchanged; deeper apex paths (a shared agent, a deep link) still render the app so those links keep working; every non-apex host (per-agent subdomains, app.elizacloud.ai, localhost) is untouched — chat stays home. No redirect loop: '/settings' is not an apex-console route, so it re-enters the catch-all at a non-root path and renders the app, which reads '#billing'. Test: CloudRouterShell.test.tsx — apex+auth+root → console home, and apex+auth+deep-link → agent app (deep links preserved). 7/7 pass. --- .../src/cloud/shell/CloudRouterShell.test.tsx | 23 ++++++-- .../ui/src/cloud/shell/CloudRouterShell.tsx | 53 +++++++++++++++---- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/packages/ui/src/cloud/shell/CloudRouterShell.test.tsx b/packages/ui/src/cloud/shell/CloudRouterShell.test.tsx index 9dbe1ecf512e4..6ef1e37b8fdaf 100644 --- a/packages/ui/src/cloud/shell/CloudRouterShell.test.tsx +++ b/packages/ui/src/cloud/shell/CloudRouterShell.test.tsx @@ -40,11 +40,14 @@ function setHostname(hostname: string): void { }); } -function renderCatchAll(): void { +function renderCatchAll(initialPath = "/"): void { render( - + } /> + {/* The console home target. The real app renders billing here; the test + just needs a marker to prove the apex root redirected to it. */} + } /> { expect(screen.queryByTestId("agent-app")).toBeNull(); }); - it("renders the agent app on apex when a valid Steward session exists", () => { + it("redirects an authenticated apex ROOT visitor to the console home (credits/manage), not chat", () => { setHostname("elizacloud.ai"); localStorage.setItem(STEWARD_TOKEN_KEY, stewardToken(FUTURE_EXP)); - renderCatchAll(); + renderCatchAll("/"); + expect(screen.getByTestId("console-home")).toBeTruthy(); + expect(screen.queryByTestId("agent-app")).toBeNull(); + expect(screen.queryByTestId("login-page")).toBeNull(); + }); + + it("still renders the agent app for an authenticated apex DEEP link (not the bare root)", () => { + // A shared-agent / deep link on the apex must keep working — only the bare + // landing is rerouted to the console home. + setHostname("elizacloud.ai"); + localStorage.setItem(STEWARD_TOKEN_KEY, stewardToken(FUTURE_EXP)); + renderCatchAll("/some/agent/deep-link"); expect(screen.getByTestId("agent-app")).toBeTruthy(); + expect(screen.queryByTestId("console-home")).toBeNull(); expect(screen.queryByTestId("login-page")).toBeNull(); }); diff --git a/packages/ui/src/cloud/shell/CloudRouterShell.tsx b/packages/ui/src/cloud/shell/CloudRouterShell.tsx index b1286cb439bdd..9c69d1707d909 100644 --- a/packages/ui/src/cloud/shell/CloudRouterShell.tsx +++ b/packages/ui/src/cloud/shell/CloudRouterShell.tsx @@ -184,14 +184,35 @@ function isApexControlPlaneHost(): boolean { ); } +/** + * Where an authenticated visitor landing on the apex ROOT is sent. The apex + * (elizacloud.ai) is the cloud CONSOLE — its job is "add credits / manage your + * account", not chat (chat is the agent app's home, served from + * app.elizacloud.ai). `/settings#billing` is the canonical credits/billing + * surface (every in-app billing link resolves here, e.g. the + * `dashboard/billing → /settings#billing` compat redirect above) and its tabbed + * shell (Billing · Developer/API keys · Connections · Organization) is the + * account-management hub. Both domains serve the SAME packages/app bundle, so + * without this the apex and the app subdomain look identical when signed in. + */ +const APEX_AUTHENTICATED_HOME = "/settings#billing"; + /** * Catch-all element. Renders the agent app exactly as before, EXCEPT on an apex - * control-plane host where the Steward session is resolved-and-unauthenticated — - * there it redirects to the registered cloud `/login` page (`returnTo` - * preserved) instead of booting the agent shell that would 401-wall. The apex - * 401-wall was a router fall-through: no route registers the apex root `/`, so - * an unauthenticated apex visitor hit this catch-all and booted the agent - * runtime against a backend that isn't there. + * control-plane host, where it makes the two domains behave differently: + * + * - unauthenticated → the Steward `/login` page (`returnTo` preserved) instead + * of booting the agent shell that would 401-wall on `/api/*`. (The apex + * 401-wall was a router fall-through: no route registers the apex root `/`, + * so an unauthenticated apex visitor hit this catch-all and booted the agent + * runtime against a backend that isn't there.) + * - authenticated AND on the bare apex root (`/`) → the cloud console home + * ({@link APEX_AUTHENTICATED_HOME}) instead of chat, so elizacloud.ai lands + * on the credits/manage dashboard. Deeper apex paths (a shared agent, a deep + * link) still render the app so those links keep working. + * + * Every non-apex host (per-agent subdomains, app.elizacloud.ai, localhost) is + * untouched: chat stays home. */ export function AppCatchAllRoute({ appElement, @@ -200,11 +221,21 @@ export function AppCatchAllRoute({ }): React.JSX.Element { const { ready, authenticated } = useSessionAuth(); const location = useLocation(); - if (isApexControlPlaneHost() && ready && !authenticated) { - const returnTo = encodeURIComponent( - `${location.pathname}${location.search}`, - ); - return ; + if (isApexControlPlaneHost() && ready) { + if (!authenticated) { + const returnTo = encodeURIComponent( + `${location.pathname}${location.search}`, + ); + return ; + } + // Authenticated on the bare apex root → the console home. Guard on the root + // path (and no tab hash) so we redirect ONLY the landing, never a deep link + // the user navigated to on purpose. The target is `/settings`, which is not + // an apex-console route, so it re-enters this catch-all at a non-root path + // and renders the app (which reads `#billing`) — no redirect loop. + if (location.pathname === "/" && !location.hash) { + return ; + } } return <>{appElement}; }