Repository navigation
fix(auth): login pages say when the server cannot be reached; Sign up waits for the posture (objectui#11806) - #11882
Conversation
… for the posture before offering Sign up The console's own /login and app-shell's DefaultLoginPage held the public auth config as `null` both while it loaded and after a failed read, and `decideSignUpOffer` answers `null` as "offer the link". With the server down the page drew a live-looking form, offered "Sign up", and the user found out on submit. Each page now tracks the read as loading / failed / known. While the posture is unknown it renders no sign-up link; when the read fails it renders "Cannot connect to server" with a Retry that reads the config again, in place of the form. The exported sign-up decision is unchanged. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
…ture-unknown offer Both pages: a pending config read offers no "Sign up"; a failed read renders "Cannot connect to server" with Retry in place of the form and no "Sign up"; Retry reads again, keeps the unreachable state up while in flight, and an answer brings back the form with the offer it decides. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
The console's /register and app-shell's DefaultRegisterPage stored a
failed config read as `{ config: null }`, which `decideSignUpOffer`
answers as "form": with the server down they drew the full
registration form. They now track the read as loading / failed / known
like the login pages: nothing is offered until it answers, and a failed
read shows the same "Cannot connect to server" panel with Retry.
Each side's login page now exports its panel (`ServerUnreachable`, with
a `data-testid`) and its register page reuses it. The package entry is
unchanged.
Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU
Co-authored-by: Claude <noreply@anthropic.com>
Both register pages: a pending config read offers no form; a failed read shows "Cannot connect to server" with Retry in place of the form; Retry is busy while in flight, then an open answer brings the form and a disableSignUp: true answer bounces to /login. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
1 similar comment
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ames the register pages The README's sign-up table gains the pending and failed-read rows, and its useSignUpOffer host recipe asks decideSignUpOffer only once the read has answered, returning `unreachable` for a failed read. The decision's own answers are unchanged. The changeset now covers both register pages. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
With no backend the console's /login now ends on the "Cannot connect to server" panel (objectui#11806) instead of the password form. The closed set of recognised boot states matched it only while the auth-config spinner was still up; it now names the panel by its data-testid, so the assertion stays terminal-stable. Claude-Session: https://claude.ai/code/session_01MgfduSkFrfM3eorB3UGfAU Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Fixes #11806
Clause-②: no
Implemented by session
session_01MgfduSkFrfM3eorB3UGfAU(claim comment 6051369453, seatdomain:ui#2).What was wrong
With the API unreachable, the console's own
/loginand app-shell's exportedDefaultLoginPagedrew a normal-looking form and offered "Sign up". Both pages held the public auth config asnullwhile the read was pending and also after it failed, anddecideSignUpOfferanswersnullas "offer the link". The visitor found out that the server was down only when they submitted./registerandDefaultRegisterPageread a failed config as{ config: null }too, and drew the full registration form.What changed
Each page now tracks its config read as
loading,failedorknown:decideSignUpOffer, as before.The register pages on both sides (the console's
RegisterPage, app-shell'sDefaultRegisterPage) track the same three states:/loginbounce fordisableSignUp.Each side's
LoginPagemodule exports its panel asServerUnreachable(data-testid="auth-server-unreachable"), and itsRegisterPagereuses it. app-shell's package entry is unchanged: it exportsDefaultLoginPageandDefaultRegisterPageby name.packages/app-shell/README.md, "Default auth pages and the sign-up offer", gains rows for a pending and a failed config read and a Retry sentence. ItsuseSignUpOfferrecipe now tracksloading/failed/knownand asksdecideSignUpOfferonly once the read has answered.e2e/smoke.spec.ts's boot-state set names the unreachable panel, the sign-in screen's final state when there is no backend (objectui#4086's closed set).decideSignUpOffer,SignUpOfferContext,SignUpOfferandDefaultLoginPage's props are untouched. The pages consult the decision only once the read isknown. The panel is built from@object-ui/auth's exported form primitives (AuthFormHeader,AuthAlertIcon,AuthSpinner,AUTH_PRIMARY_BUTTON_CLASS), so it sits whereLoginFormwould, at the same width. Each page keeps its own copy of the panel: sharing one would mean a new@object-ui/app-shellexport, which this claim rules out.The mechanism, measured
getAuthConfig(), which isGET /api/v1/auth/config.createAuthClientretries that read three times, with 500, 1500 and 3500 ms backoff and 8 s per attempt, before it rejects. That rejection alone moves the page tofailed. The other boot reads the card lists decide nothing on this page:AuthProviderabsorbs a failedget-sessionand treats the visitor as signed out.runtime/config,auth/me/localizationand thei18ncatalogues are boot reads that the console'smain.tsxawaits and deliberately absorbs.auth/bootstrap-statusruns only once the posture is known and closed.enkeys, and no key is added:console.error.connectionFailed("Cannot connect to server"),console.error.checkServer,console.actions.retryandconsole.actions.retrying.console.error.serverUnreachableis not used, because it needs a{{url}}that the page does not hold.check:i18n-keysholds them equal to the pack.DefaultLoginPagepasses none. Its pins render the pack text through its ownt, which measures that these keys reach it.Browser check (the card's reproduction)
Setup: the console's Vite dev server on a private port, with
DEV_PROXY_TARGETpointing at a port nothing listens on./api/v1/auth/configthrough the proxy answers 502. The page load produced 502s on the seven paths the card describes:runtime/config,auth/me/localization,auth/config,i18n/translations/en,i18n/locales,auth/get-sessionanddev/metadata-events. Chromium at 1440x900,/login:/auth/configwith postureopenstarted on the proxy target, then RetryTests
New pins on the login pages, the same three on each side. The console's are in
LoginPage.server-unreachable-11806.test.tsxand app-shell's are indefaultLoginServerUnreachable-11806.test.tsx:openanswer brings the form and the link to/register, and adisableSignUp: trueanswer brings the form without the link.The client stub's
getConfigis scripted read by read. LikecreateAuthClient's, it shares an in-flight read and forgets a failed one, and a scripted rejection stands for the real client's final answer after its own retries.On the register pages, four pins on each side, in
RegisterPage.server-unreachable-11806.test.tsxanddefaultRegisterServerUnreachable-11806.test.tsx:openbrings the form;disableSignUp: truebounces to/login.Register ablation. With both register pages at their
cef0eeebytes, 6 pins fail and 2 pass. The 2 green ones are the pending pins: the base register pages already withheld the form while the read was pending.Ablation. The fix was committed first. Both page files were replaced by their
cef0eeebytes, then restored fromHEADby a trap; the restore is proven by blob hashes equal toHEADand an emptygit diff HEAD. All 8 pins went red. The pending pins failed for the reason they exist: the assertion that the "Sign up" anchor is absent found it present, on both pages.Gates, at
ce91f67, plus the e2e rows atba3624apnpm exec vitest run apps/console/src/pages/auth/ packages/app-shell/src/console/auth/plus the README's three readersTest Files 24 passed (24),Tests 175 passed (175)pnpm exec vitest run --maxWorkers=2 packages/app-shell/ apps/console/pnpm --filter @object-ui/app-shell type-check, afterturbo run build --filter='@object-ui/console^...'pnpm --filter @object-ui/console type-checktsc --listFileson both programsnode scripts/check-changeset-presence.mjs4 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-no-major.mjsNo changeset declares a major bumppnpm check:i18n-keysDefaultLoginPageand a drifted default in the console page each turned it red (missing-key,default-value-drift), restored fromHEADpnpm check:readme-exportscliandplugin-aiare unbuilt and not judged.pnpm exec eslint e2e/smoke.spec.ts, atba3624apnpm type-check:e2e, atba3624asmoke.spec.tsba3624a6 passed. Final-state control at 20 s: the old set matches 0 elements and the panel 1.pnpm check:new-line-citations0 new citation(s)pnpm check:control-bytesOKpnpm check:changeset-claims,check:pending-changeset-literals,check:unreferenced-sources,check:test-path-roots,check:phantom-deps,check:i18n-driftpnpm exec eslinton the eight touched source and test filesreact-hooks/set-state-in-effecton lines this diff does not touch; thecef0eeebytes carry the same onesNOT MEASURED locally:
check:eager-closure, because it needs a consolevite build, whichperformance-budget.ymlruns in CI. The page's new imports come from the@object-ui/authentry, which the page already imports forLoginForm.Acceptance notes
/registeris fixed on both sides in the patch round, under the seat's in-place amendment of the claim surface.LoginForm's own comment says a failed config read falls back to the password form, because "break-glass beats lock-out". On these two pages a failed read no longer reachesLoginForm. What is given up is a server whose/auth/configfails while/sign-in/emailworks, and Retry is the way back. The triage direction asked for exactly this replacement. An IdP outage does not reach this path: it does not fail/auth/config, which objectstack serves, andLoginForm's own break-glass under SSO enforcement is unchanged.Generated by Claude Code