Skip to content

Keep sign-in and sign-out redirects on one dark background - #6961

Merged
habdelra merged 8 commits into
mainfrom
cs-14357-signing-in-and-out-through-the-secure-origin-flashes-white
Oct 26, 2026
Merged

habdelra merged 8 commits into
mainfrom
cs-14357-signing-in-and-out-through-the-secure-origin-flashes-white

Conversation

@habdelra

@habdelra habdelra commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Signing in or out through the secure origin is a chain of full-page redirects between the app and the secure origin, and every page in the chain now paints the same dark sign-in background from its first frame until the workspace chooser takes over. The flashes came from four places:

  • The app's page before Ember renders. The body was boxel-ui's reset white until the first render, which on a cold load is a couple of seconds. An inline script in packages/host/index.html paints the boot in the colour the Loading screen is about to use: #191624 when no session is stored, the operator-mode purple when one is. It does this only on the app itself, never on a published realm's page, and only until the app has rendered something.
  • The browser's own canvas before either page's first paint. Chrome doesn't always hold the previous page's frame across these redirects (it didn't on the way back from Google), and until the new page paints it shows its default canvas, which is white in a light browser theme. A <meta name="color-scheme" content="dark"> darkens that canvas. The secure origin's shell declares it statically. The app's boot script adds it and takes it off again once the app renders, so the running app keeps its own colour scheme.
  • The empty interact submode's operator-mode purple. Once the login token is redeemed, the session is up before the index route has opened the workspace chooser (the route first awaits the billing fetch). SecureSignIn now opens the chooser in memory before booting the session when the page lands on it (the app's root with no card or operator-mode state), and the route writes the same state to the URL once it has booted.
  • Loading indicators that didn't match. "Loading…", the app's "Signing you in…" and the secure origin's "Signing you in…" were three different markups with two different spinners. They are now one BusyMessage component in packages/secure-origin/src/components, so every busy screen in the chain shows the same title above the same spinner in the same place.

The loopback hand-off confirmation also reads in active voice: "A program on this computer at … wants to sign in to Boxel as you. Continue only if you started this sign-in from that program."

Coming back to the app from the secure origin, Chrome can still show its default canvas for about 130 ms before it has parsed any of the page. That canvas is white in a light browser theme. The secure origin's Cross-Origin-Opener-Policy: same-origin puts the app in a new browsing context, so Chrome doesn't hold the previous frame, and nothing the page declares can reach that frame. In the Google flow, one more white flash isn't ours: Google's own accounts.youtube.com/accounts/SetSID "Redirecting" page paints white for about 130 ms.

Recordings

Recorded from a Chrome window on staging, signed in as a staging test account, with the browser's address bar cropped off. "After" is a production build of this branch's host, served locally against staging. The secure origin there is still staging's own, so its loopback confirmation still shows the copy this PR replaces.

Sign in — before (white, then purple, then the chooser)

sign in before

Sign in — after

sign in after

Sign out — before (white while the app boots)

sign out before

Sign out — after

sign out after

Testing

  • Host integration: matrix/secure-sign-in (11, two new: the chooser is open before the session boots on a root landing, and left alone when the page names its own state), matrix/login (10), acceptance host mode tests (37, which include "Loading screen is never shown on host mode") and basic (4) all pass locally.
  • Secure origin: 59 unit and 117 component tests pass, including the new copy assertion on the loopback confirmation. lint:build passes against the regenerated build-record.json.
  • The workspace-chooser acceptance module was cut short locally when the shared test stack timed out indexing the catalog and tore itself down. CI runs it in full.
  • Manually, against staging: password sign-in, sign-out, and Google sign-out and sign-in, recorded window-level at 30 fps. No white or purple frame appears anywhere in the chain on this branch.

🤖 Generated with Claude Code

habdelra and others added 3 commits October 24, 2026 18:27
Signing in or out through the secure origin goes through several full-page
redirects, and the pages along the way painted different backgrounds:
the host page was the reset's white until Ember rendered, the browser's
default canvas showed before either page's first paint, and the empty
interact submode's operator-mode purple showed between redeeming the login
token and the index route opening the workspace chooser.

- The host's index.html paints the app's boot in the colour its Loading
  screen is about to be, and in the dark colour scheme, until the app
  renders (published realms keep the plain page).
- The secure origin's shell declares the dark colour scheme, so its
  pre-stylesheet frame is dark too.
- SecureSignIn opens the workspace chooser before booting the session when
  the page will land on it.
- "Loading…", the host's "Signing you in…" and the secure origin's
  "Signing you in…" all draw one BusyMessage component.
- The loopback confirmation reads in active voice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-24T22:43:00.479887Z 4b8fd22 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b8fd22616

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/host/app/lib/secure-sign-in.ts Outdated
Comment on lines +51 to +55
let params = new URLSearchParams(search);
return (
pathname === ENV.rootURL &&
!params.has('operatorModeState') &&
!params.has('cardPath')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match the route’s empty-state chooser predicate

When the root URL already carries a serialized state such as { stacks: [], submode: 'interact', workspaceChooserOpened: false }, routes/index.gts deliberately replaces it with a state that opens the chooser, but this helper returns false solely because operatorModeState is present. That state can be produced by closing the chooser with no cards and can survive an expired session, so the first post-login render still shows the empty purple interact view before the route opens the chooser—the flash this change is intended to remove. Decode the state and mirror the route’s empty-state condition.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 5e05cec861. landsOnWorkspaceChooser decodes operatorModeState and mirrors the index route: a root page whose state is an interact submode with no stacks lands on the chooser, whatever its chooser flag says. A new integration test covers { stacks: [], submode: 'interact', workspaceChooserOpened: false }.

Comment on lines +33 to +34
<div id='host-loading' class='session-loading' data-test-host-loading>
<BusyMessage @title='Loading…' />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep signed-in loading text on the dark palette

When a persisted session exists, this branch renders BusyMessage outside AuthContainer or any dark-theme scope. Its color: var(--foreground, var(--boxel-light)) therefore resolves the root light-theme --foreground to black, so the white fallback is never used; this replaces the previous explicit white loading text and makes returning users see a dark title on the purple #686283 loading screen while the other shared busy screens remain white. Give this wrapper the intended foreground token or dark theme scope.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 5e05cec861. The signed-in Loading wrapper sets --foreground: var(--boxel-light), so the title stays white outside AuthContainer.

@habdelra habdelra left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] This review checked two things: whether every page in the sign-in and sign-out chain stays on one dark background, and whether the boot script and the shared BusyMessage change anything else that renders packages/host/index.html (published realms in host mode, the prerender /render and /connect pages, and tests). It did not run the host acceptance or matrix suites, which need the shared test stack, so those results are unverified here.

One blocking issue: when a session is stored, the "Loading…" title renders black on the purple instead of white. Apart from that, the change does what it sets out to do.

  1. Set --foreground on .session-loading so BusyMessage's title stays light outside AuthContainer. See the comment on .session-loading in templates/loading.gts. Blocking.
  2. Add a comment on HostModeService#isActive that points to the boot script's copy of the same decision. See the comment on isApp in index.html. Non-blocking.

Comment on lines +39 to 40
.session-loading {
background-color: #686283;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] When a session is stored, "Loading…" now renders black on the purple, where it used to be white. BusyMessage colours its title var(--foreground, var(--boxel-light)). Outside AuthContainer, nothing between :root and this wrapper sets --foreground, so the title inherits boxel-ui theme.css's :root value, var(--boxel-dark) (#000). The host never puts data-theme on <html>. I loaded variables.css, theme.css and global.css with this rule and .busy-title in headless Chrome, and the title computed to rgb(0, 0, 0). The old .loading-text was #fff.

Set the token on the wrapper:

Suggested change
.session-loading {
background-color: #686283;
.session-loading {
--foreground: var(--boxel-light);
background-color: #686283;

Regression, blocking: this is the first screen of every cold load for a signed-in user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in 5e05cec861. The signed-in Loading wrapper sets --foreground: var(--boxel-light), so BusyMessage's title is white on the purple.

Comment thread packages/host/index.html
Comment on lines +63 to +68
var isApp =
!simulatingHostMode &&
(config.hostsOwnAssets ||
(realmServerHost !== undefined &&
host.endsWith(realmServerHost) &&
!host.endsWith('.localhost')));

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] isApp copies HostModeService#isActive (hostsOwnAssets, isRealmServerDomain, simulatingHostMode), but only this file points to the other. If someone changes how the service tells a published realm from the app, this copy will keep the old rule. A published realm's page would then get the dark boot paint and the color-scheme: dark meta. No test would catch it: the host test runner's tests/index.html doesn't include this script, and no assertion checks a host-mode page's boot. Can you add a comment on isActive / isRealmServerDomain that names this script, so the next change updates both copies?

This PR adds the duplicate, so the pointer belongs here. Non-blocking.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Done in 5e05cec861. HostModeService#isActive now names the boot script in index.html that makes the same published-realm decision, so a change to one is made in both.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   2h 15m 51s ⏱️ + 7m 46s
5 941 tests +2  5 930 ✅ +2  11 💤 ±0  0 ❌ ±0 
5 956 runs  +2  5 945 ✅ +2  11 💤 ±0  0 ❌ ±0 

Results for commit e132ca1. ± Comparison against earlier commit 87868ac.

Realm Server Test Results

     1 files  ±    0     366 suites  ±0   2h 6m 48s ⏱️ + 4m 14s
 6 107 tests ±    0   6 106 ✅  -     1  0 💤 ±0  1 ❌ +1 
12 340 runs  +6 170  12 339 ✅ +6 169  0 💤 ±0  1 ❌ +1 

Results for commit e132ca1. ± Comparison against earlier commit 87868ac.

For more details on these errors, see this check.

… interact state

- The signed-in Loading screen sits outside AuthContainer, so BusyMessage's
  `--foreground` fell back to boxel-ui's root black; the wrapper sets it to
  the light colour.
- landsOnWorkspaceChooser mirrors the index route: a root page whose
  operator-mode state is an interact submode with no stacks lands on the
  chooser too, whatever its chooser flag says.
- HostModeService#isActive names the boot script in index.html that makes
  the same published-realm decision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@habdelra
habdelra requested a review from a team October 24, 2026 23:00
…nd-out-through-the-secure-origin-flashes-white

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd-out-through-the-secure-origin-flashes-white

build-record.json is regenerated from the merged source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@habdelra

Copy link
Copy Markdown
Contributor Author

[Claude Code 🤖] The red Matrix Client Tests (3, 3) shard is broken-linksto.spec.ts:170 ("a linksToMany element deleted while the page is open shows the placeholder live"). The same test fails in that shard on main, and the fix is in flight in #6936. This PR doesn't touch linked cards, so the shard is being rerun.

habdelra and others added 2 commits October 26, 2026 07:52
…nd-out-through-the-secure-origin-flashes-white

build-record.json is regenerated from the merged source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd-out-through-the-secure-origin-flashes-white

build-record.json is regenerated from the merged source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@habdelra
habdelra merged commit e2b6f99 into main Oct 26, 2026
125 of 127 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants