Found by the P4 authenticated walk (epic #307, #316) during the first credentialed run against a freshly-built master bundle.
Symptom
With a valid Keycloak session present, the SPA crashes on bootstrap and renders nothing — no toolbar, no shell, no route content. The console shows, repeatedly:
ERROR w: NG0200
at Wt.get (chunk-ZAANGBOU.js) …
at new t (main-*.js)
at …ɵfac [as factory]
NG0200 = Circular dependency in DI detected. It fires during service factory construction at bootstrap.
Reproduction
Serve a fresh pnpm build of master over the E2E origin (https://localhost:4200).
Anonymous browsing is fine — the full 60-spec anonymous suite passes, toolbar renders everywhere.
Log in through Keycloak (any valid spikersoft-realm user; verified with a freshly-seeded test account whose token/roles are valid via direct grant).
After the redirect back to the app: app-toolbar count = 0, nav-user-menu = 0, on /homeand on / — the whole app is dead. NG0200 in console.
The delta between the passing anonymous case and the crash is only the presence of an authenticated session — so the cycle is in a service that is constructed only on the authenticated path.
Not confirmed on live prod (SW caching)
Couldn't cleanly A/B against spikersoft.com: production has the service worker enabled, so it serves a cached older bundle (anonymous prod also reports app-toolbar=0 because the cached build predates the P2 data-testids). So live users may be on an older, working bundle — but the current master will ship this crash on the next fresh load.
Strong lead (not yet confirmed — needs a readable build)
Authenticated-only bootstrap DI cycle. Prime suspects in projects/spikersoft/src/main.ts: the HTTP_INTERCEPTORS chain (JwtInterceptor / AnonLessonInterceptor) — a classic NG0200 is an interceptor whose dependency graph pulls in HttpClient, which only closes the loop when an authenticated request actually exercises the token-attach path — or the keycloak-angular AutoRefreshTokenService / UserActivityService / provideActivityAuthGate factory that injects Keycloak. Pinning the exact cycle needs a dev/non-optimized build with class names (the prod build's development config points at dev API/Keycloak, so the repro harness needs adjusting).
Impact
Blocks all of P4/P5/P6 (every authenticated E2E phase) and — if it reaches users on a fresh bundle — is a sev-1: logged-in users get a blank page.
Found by the P4 authenticated walk (epic #307, #316) during the first credentialed run against a freshly-built `master` bundle.
## Symptom
With a valid Keycloak session present, the SPA crashes on bootstrap and renders **nothing** — no toolbar, no shell, no route content. The console shows, repeatedly:
```
ERROR w: NG0200
at Wt.get (chunk-ZAANGBOU.js) …
at new t (main-*.js)
at …ɵfac [as factory]
```
`NG0200` = *Circular dependency in DI detected*. It fires during service factory construction at bootstrap.
## Reproduction
1. Serve a fresh `pnpm build` of `master` over the E2E origin (`https://localhost:4200`).
2. Anonymous browsing is **fine** — the full 60-spec anonymous suite passes, toolbar renders everywhere.
3. Log in through Keycloak (any valid `spikersoft`-realm user; verified with a freshly-seeded test account whose token/roles are valid via direct grant).
4. After the redirect back to the app: `app-toolbar` count = 0, `nav-user-menu` = 0, on `/home` **and** on `/` — the whole app is dead. NG0200 in console.
The delta between the passing anonymous case and the crash is **only the presence of an authenticated session** — so the cycle is in a service that is constructed only on the authenticated path.
## Not confirmed on live prod (SW caching)
Couldn't cleanly A/B against spikersoft.com: production has the service worker enabled, so it serves a **cached older bundle** (anonymous prod also reports `app-toolbar`=0 because the cached build predates the P2 `data-testid`s). So live users may be on an older, working bundle — but the **current `master` will ship this crash** on the next fresh load.
## Strong lead (not yet confirmed — needs a readable build)
Authenticated-only bootstrap DI cycle. Prime suspects in `projects/spikersoft/src/main.ts`: the `HTTP_INTERCEPTORS` chain (`JwtInterceptor` / `AnonLessonInterceptor`) — a classic NG0200 is an interceptor whose dependency graph pulls in `HttpClient`, which only closes the loop when an authenticated request actually exercises the token-attach path — or the keycloak-angular `AutoRefreshTokenService` / `UserActivityService` / `provideActivityAuthGate` factory that injects `Keycloak`. Pinning the exact cycle needs a dev/non-optimized build with class names (the prod build's `development` config points at dev API/Keycloak, so the repro harness needs adjusting).
## Impact
Blocks all of P4/P5/P6 (every authenticated E2E phase) and — if it reaches users on a fresh bundle — is a sev-1: logged-in users get a blank page.
Part of epic #307.
spikerj
added the bug label 2026-07-04 01:56:23 +00:00
New evidence from the #359 browser validation (dev, non-minified build, local Mac API):
The NG0200 cycle is AnonLessonInterceptor → AnonSessionService → HttpClient → interceptor chain. A lazy-injection fix (resolve AnonSessionService via Injector inside intercept(), past the authenticated early-return) was found uncommitted in the worktree and is now committed on spikersoft-angular branch feature/mac-browser-validation (commit 6b3d947). With it, a fresh interactive Keycloak login renders and works fully — toolbar, user menu, Art Studio, SignalR.
The blank-shell symptom is NOT fully explained by that cycle. Even with the interceptor fix, booting the app with an existing SSO session (Playwright storage state, or any hard refresh while logged in) still renders an empty <app-root> — zero console errors, SystemNotificationService connects to the hub, Profile calls return 200, and the dev-mode diagnostic prints "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable". Fresh-login boot paints; silent-SSO/session-restore boot never does. Evidence: e2e/test-results/artstudio-live/debug-auth-home.png (blank) on that branch, versus 00-logged-in.png (fresh login, fully rendered) from the same bundle minutes apart.
So this ticket likely has two layers: the NG0200 DI cycle (fix in hand) and a session-restore bootstrap hang (unfixed, reproducible at will with a persisted storage state + page.goto). The #359 E2E spec works around it by logging in fresh inside the test.
New evidence from the #359 browser validation (dev, non-minified build, local Mac API):
1. **The NG0200 cycle is `AnonLessonInterceptor → AnonSessionService → HttpClient → interceptor chain`.** A lazy-injection fix (resolve `AnonSessionService` via `Injector` inside `intercept()`, past the authenticated early-return) was found uncommitted in the worktree and is now committed on `spikersoft-angular` branch `feature/mac-browser-validation` (commit `6b3d947`). With it, a **fresh interactive Keycloak login renders and works fully** — toolbar, user menu, Art Studio, SignalR.
2. **The blank-shell symptom is NOT fully explained by that cycle.** Even with the interceptor fix, booting the app with an *existing* SSO session (Playwright storage state, or any hard refresh while logged in) still renders an empty `<app-root>` — zero console errors, `SystemNotificationService` connects to the hub, Profile calls return 200, and the dev-mode diagnostic prints "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable". Fresh-login boot paints; silent-SSO/session-restore boot never does. Evidence: `e2e/test-results/artstudio-live/debug-auth-home.png` (blank) on that branch, versus `00-logged-in.png` (fresh login, fully rendered) from the same bundle minutes apart.
So this ticket likely has two layers: the NG0200 DI cycle (fix in hand) and a session-restore bootstrap hang (unfixed, reproducible at will with a persisted storage state + `page.goto`). The #359 E2E spec works around it by logging in fresh inside the test.
Layer 1 (the NG0200 DI cycle) fixed in spikersoft-angular PR #127 (merged to master). The exact cycle Angular emitted: AnonSessionService → ActivityTrackingService → HTTP_INTERCEPTORS → AnonSessionService — AnonLessonInterceptor field-injected AnonSessionService at construction, and ActivityTrackingService's ctor fires an auth-gated manifest GET that materializes the interceptor chain mid-construction. Fix: defer that one edge to a lazy injector.get(AnonSessionService) inside intercept(). Failing→passing unit repro added; mirrors the browser-validated 6b3d947.
Keeping this open, re-scoped to layer 2 — a separate session-restore bootstrap hang that survives the DI fix: booting with an existing SSO session (Playwright storage state, or hard-refresh while logged in) still renders an empty <app-root> with no console errors — "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable." This is the one that blanks the shell for returning logged-in users and blocks the authenticated e2e accounts (e2e-test-accounts). Next step: trace what keeps PendingTasks unstable during silent SSO restore (an APP_INITIALIZER/resolver awaiting a never-settling promise, or an interceptor stalling the manifest/auth round-trip).
**Layer 1 (the NG0200 DI cycle) fixed** in spikersoft-angular PR #127 (merged to `master`). The exact cycle Angular emitted: `AnonSessionService → ActivityTrackingService → HTTP_INTERCEPTORS → AnonSessionService` — `AnonLessonInterceptor` field-injected `AnonSessionService` at construction, and `ActivityTrackingService`'s ctor fires an auth-gated manifest GET that materializes the interceptor chain mid-construction. Fix: defer that one edge to a lazy `injector.get(AnonSessionService)` inside `intercept()`. Failing→passing unit repro added; mirrors the browser-validated `6b3d947`.
**Keeping this open, re-scoped to layer 2** — a *separate* session-restore bootstrap hang that survives the DI fix: booting with an existing SSO session (Playwright storage state, or hard-refresh while logged in) still renders an empty `<app-root>` with **no console errors** — "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable." This is the one that blanks the shell for returning logged-in users and blocks the authenticated e2e accounts ([[e2e-test-accounts]]). Next step: trace what keeps `PendingTasks` unstable during silent SSO restore (an APP_INITIALIZER/resolver awaiting a never-settling promise, or an interceptor stalling the manifest/auth round-trip).
Layer 2 root cause — pinned end-to-end (high confidence). The blank shell is a pre-bootstrap hang in keycloak.init():
Angular adds a bootstrap-level PendingTask (core.mjs:650) and gates appRef.bootstrap(rootComponent) behind ApplicationInitStatus.donePromise — so if an APP_INITIALIZER never settles, the root component is never created → truly empty <app-root> (dispositive: a post-bootstrap hang would still paint the skeleton).
provideKeycloak registers a blocking app initializer (keycloak-angular.mjs:1554) that awaits keycloak.init(initOptions).
With onLoad:'check-sso' and checkLoginIframe defaulting true (not set in main.ts), init awaits #checkLoginIframe() then #checkSsoSilently() (keycloak.js:948/974) — each a new Promise resolved only by a postMessage from a hidden cross-origin iframe at ids.spikersoft.com, with no internal timeout. If the iframe never posts back, init never settles.
Session-restore only: fresh login carries the auth-code fragment → init takes the callback.valid branch and resolves without the iframe. Hard-refresh / Playwright storage-state has no fragment → check-sso branch → hangs. Anonymous boot resolves (silent iframe reports "no session"). Zero console errors because a hang is neither resolve nor reject, and keycloak-js logging defaults off. The earlier "Profile 200 / hub connects" evidence was from the fresh-login run (AppComponent.ngOnInit never runs during the hang).
Ranked fixes (browser-verification required — not unit-testable):
checkLoginIframe: false in main.ts initOptions (keycloak's recommended modern-browser setting) — skips the hanging login-status round-trip; check-sso then uses the same-originassets/silent-check-sso.html. Least-invasive, most likely the real fix; pair with an explicit silentCheckSsoRedirectUri. Tradeoff: disables cross-tab SSO-logout detection (acceptable here).
Bound keycloak.init() with a timeout (safety net) — guarantees the shell never blanks again; on timeout boots anonymous (re-auth) rather than hanging. This part IS unit-testable.
If the shell paints but the session doesn't restore under the e2e origin, the true root is 3p-cookie/SameSite policy for ids.spikersoft.com cookies inside the localhost:4200 iframe — fix that to unblock authenticated e2e (#316/#319).
A fix PR combining #1 + #2 (with a stabilization unit test for the timeout safety net) is being prepared; it will need your ~5-min logged-in hard-refresh verification before merge — a unit test can't exercise the cross-origin iframe. Repro: build → serve -s dist/spikersoft/browser → log in → hard-refresh / → blank shell; apply #1 → paints + session restored.
**Layer 2 root cause — pinned end-to-end (high confidence).** The blank shell is a pre-bootstrap hang in `keycloak.init()`:
- Angular adds a bootstrap-level PendingTask (`core.mjs:650`) and gates `appRef.bootstrap(rootComponent)` behind `ApplicationInitStatus.donePromise` — so if an APP_INITIALIZER never settles, the root component is **never created** → truly empty `<app-root>` (dispositive: a post-bootstrap hang would still paint the skeleton).
- `provideKeycloak` registers a **blocking** app initializer (`keycloak-angular.mjs:1554`) that `await`s `keycloak.init(initOptions)`.
- With `onLoad:'check-sso'` and **`checkLoginIframe` defaulting true** (not set in `main.ts`), init awaits `#checkLoginIframe()` then `#checkSsoSilently()` (`keycloak.js:948/974`) — each a `new Promise` resolved **only** by a `postMessage` from a hidden **cross-origin** iframe at `ids.spikersoft.com`, **with no internal timeout**. If the iframe never posts back, init never settles.
- **Session-restore only:** fresh login carries the auth-code fragment → init takes the `callback.valid` branch and resolves without the iframe. Hard-refresh / Playwright storage-state has no fragment → check-sso branch → hangs. Anonymous boot resolves (silent iframe reports "no session"). **Zero console errors** because a hang is neither resolve nor reject, and keycloak-js logging defaults off. The earlier "Profile 200 / hub connects" evidence was from the *fresh-login* run (AppComponent.ngOnInit never runs during the hang).
**Ranked fixes (browser-verification required — not unit-testable):**
1. `checkLoginIframe: false` in `main.ts` initOptions (keycloak's recommended modern-browser setting) — skips the hanging login-status round-trip; check-sso then uses the **same-origin** `assets/silent-check-sso.html`. Least-invasive, most likely the real fix; pair with an explicit `silentCheckSsoRedirectUri`. Tradeoff: disables cross-tab SSO-logout detection (acceptable here).
2. Bound `keycloak.init()` with a timeout (safety net) — **guarantees the shell never blanks again**; on timeout boots anonymous (re-auth) rather than hanging. This part IS unit-testable.
3. If the shell paints but the session doesn't restore under the e2e origin, the true root is 3p-cookie/SameSite policy for `ids.spikersoft.com` cookies inside the `localhost:4200` iframe — fix that to unblock authenticated e2e (#316/#319).
A fix PR combining #1 + #2 (with a stabilization unit test for the timeout safety net) is being prepared; **it will need your ~5-min logged-in hard-refresh verification** before merge — a unit test can't exercise the cross-origin iframe. Repro: build → `serve -s dist/spikersoft/browser` → log in → hard-refresh `/` → blank shell; apply #1 → paints + session restored.
Fix ready for your browser check — spikersoft-angular PR #129 (NOT merged, held for you). Implements the ranked fix: (1) checkLoginIframe: false + silentCheckSsoFallback: true in main.ts (skips the hanging cross-origin login-status iframe; check-sso uses the same-origin silent html); (2) a bounded-init safety net (initKeycloakBounded, Promise.race([keycloak.init, 8s timeout]), always resolves) that guarantees the shell can never blank again even if an iframe still hangs — with a unit test proving it (never-resolving init → still settles at the deadline; unbounded await → test times out).
1857 angular tests pass, lint + build green. Your ~5-min step before merge:nx build → serve -s dist/spikersoft/browser → log in → hard-refresh / → confirm the shell paints AND you're still authenticated. If it paints but you're logged out, the residual is the 3p-cookie/SameSite iframe policy under the e2e origin (candidate #3) — I'll take that next to unblock authenticated e2e #316/#319.
**Fix ready for your browser check — spikersoft-angular PR #129 (NOT merged, held for you).** Implements the ranked fix: (1) `checkLoginIframe: false` + `silentCheckSsoFallback: true` in `main.ts` (skips the hanging cross-origin login-status iframe; check-sso uses the same-origin silent html); (2) a bounded-init safety net (`initKeycloakBounded`, `Promise.race([keycloak.init, 8s timeout])`, always resolves) that **guarantees the shell can never blank again** even if an iframe still hangs — with a unit test proving it (never-resolving init → still settles at the deadline; unbounded `await` → test times out).
1857 angular tests pass, lint + build green. **Your ~5-min step before merge:** `nx build` → `serve -s dist/spikersoft/browser` → log in → hard-refresh `/` → confirm the shell paints AND you're still authenticated. If it paints but you're logged out, the residual is the 3p-cookie/SameSite iframe policy under the e2e origin (candidate #3) — I'll take that next to unblock authenticated e2e #316/#319.
Layer-2 status: the shell-paint fix is already merged to master (PR #129 — keycloak-bounded-init, in origin/master HEAD 6374604). Independently re-verified the root cause + that the merged fix is correct (didn't add a duplicate).
Root cause (confirmed): the blocking keycloak.init() app-initializer with onLoad:'check-sso' + checkLoginIframe defaulting true awaits a hidden cross-origin login-status iframe at ids.spikersoft.com that never posts back on session-restore (no auth-code fragment) → ApplicationInitStatus.donePromise never resolves → empty <app-root>, "PendingTasks keeping application unstable", zero console errors. Fresh login takes the callback branch and resolves without the iframe — hence session-restore-only.
The merged fix:checkLoginIframe:false (falls back to the same-origin assets/silent-check-sso.html), omits initOptions from provideKeycloak (no second unbounded initializer — verified against lib source), and wraps init in Promise.race([init, 8s timeout]) so the shell can never blank again. Fresh-login + anonymous both settle well under 8s → fresh path unaffected. Unit test keycloak-bounded-init.spec.ts 3/3; nx build spikersoft green.
What still needs your browser-verify: the fix guarantees the shell paints; it cannot settle in code whether the SSO session actually restores on a real hard-refresh — that hinges on third-party-cookie / SameSite policy for ids.spikersoft.com cookies inside the same-origin silent-check-sso iframe (this ticket's candidate #3). Please boot with a restored SSO session (Playwright storage-state or a logged-in hard-refresh of /):
Shell paints and you're still authenticated → layer-2 fully resolved, close this and the E2E authenticated walks (#316/#319) are unblocked.
Shell paints but shows logged-out → residual is candidate #3 (cookie policy), a separate follow-up — not a regression of this fix.
Optional hardening not added (avoiding speculative change on a merged fix): set keycloak-js messageReceiveTimeout below the 8s bound so the silent iframe gives up cleanly first — say the word if you want it.
**Layer-2 status: the shell-paint fix is already merged to `master`** (PR #129 — `keycloak-bounded-init`, in `origin/master` HEAD `6374604`). Independently re-verified the root cause + that the merged fix is correct (didn't add a duplicate).
**Root cause (confirmed):** the blocking `keycloak.init()` app-initializer with `onLoad:'check-sso'` + `checkLoginIframe` defaulting `true` awaits a hidden **cross-origin** login-status iframe at `ids.spikersoft.com` that never posts back on session-restore (no auth-code fragment) → `ApplicationInitStatus.donePromise` never resolves → empty `<app-root>`, "PendingTasks keeping application unstable", zero console errors. Fresh login takes the callback branch and resolves without the iframe — hence session-restore-only.
**The merged fix:** `checkLoginIframe:false` (falls back to the same-origin `assets/silent-check-sso.html`), omits `initOptions` from `provideKeycloak` (no second unbounded initializer — verified against lib source), and wraps init in `Promise.race([init, 8s timeout])` so the shell can never blank again. Fresh-login + anonymous both settle well under 8s → fresh path unaffected. Unit test `keycloak-bounded-init.spec.ts` 3/3; `nx build spikersoft` green.
**What still needs your browser-verify:** the fix guarantees the shell **paints**; it cannot settle in code whether the **SSO session actually restores** on a real hard-refresh — that hinges on third-party-cookie / SameSite policy for `ids.spikersoft.com` cookies inside the same-origin silent-check-sso iframe (this ticket's **candidate #3**). Please boot with a restored SSO session (Playwright storage-state or a logged-in hard-refresh of `/`):
- Shell paints **and** you're still authenticated → layer-2 fully resolved, close this and the E2E authenticated walks (#316/#319) are unblocked.
- Shell paints but shows logged-out → residual is candidate #3 (cookie policy), a separate follow-up — not a regression of this fix.
Optional hardening not added (avoiding speculative change on a merged fix): set keycloak-js `messageReceiveTimeout` below the 8s bound so the silent iframe gives up cleanly first — say the word if you want it.
Closing with fresh evidence (2026-07-12). Fresh master production bundle, real Keycloak UI login, full authenticated walk (54 specs):
0 × NG0200 anywhere in the run (the original crash signature).
Shell mounted (app-root attached, toolbar visible) on every auth route; zero bounces to ids.spikersoft.com.
The walk failures that DID occur were unrelated console-error finds, now fixed or filed separately: the 401 /api/activity/track beacon bug (angular PR #169), e2e suite silent-skip + assertion traps (angular PR #168), and a handful of missing-endpoint 404s (/api/skills/my/by-interest, double-prefix /api/api/vault/* + /api/api/game/dungeon-crawler/keybindings) worth their own tickets.
Residual observation (not a crash, noting for the record): with a restored session, keycloak.init() still runs to the #1298s bounded-init timeout before an instant login round-trip completes auth — first paint on cold storage-state sessions is ~8s slower than it could be. The safety net works exactly as designed (boot always completes), but shaving that window is a candidate follow-up.
Closing with fresh evidence (2026-07-12). Fresh `master` production bundle, real Keycloak UI login, full authenticated walk (54 specs):
- **0 × NG0200** anywhere in the run (the original crash signature).
- Shell mounted (`app-root` attached, toolbar visible) on **every** auth route; **zero** bounces to ids.spikersoft.com.
- The walk failures that DID occur were unrelated console-error finds, now fixed or filed separately: the `401 /api/activity/track` beacon bug (angular PR #169), e2e suite silent-skip + assertion traps (angular PR #168), and a handful of missing-endpoint 404s (`/api/skills/my/by-interest`, double-prefix `/api/api/vault/*` + `/api/api/game/dungeon-crawler/keybindings`) worth their own tickets.
Residual observation (not a crash, noting for the record): with a restored session, `keycloak.init()` still runs to the #129 **8s bounded-init timeout** before an instant login round-trip completes auth — first paint on cold storage-state sessions is ~8s slower than it could be. The safety net works exactly as designed (boot always completes), but shaving that window is a candidate follow-up.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found by the P4 authenticated walk (epic #307, #316) during the first credentialed run against a freshly-built
masterbundle.Symptom
With a valid Keycloak session present, the SPA crashes on bootstrap and renders nothing — no toolbar, no shell, no route content. The console shows, repeatedly:
NG0200= Circular dependency in DI detected. It fires during service factory construction at bootstrap.Reproduction
pnpm buildofmasterover the E2E origin (https://localhost:4200).spikersoft-realm user; verified with a freshly-seeded test account whose token/roles are valid via direct grant).app-toolbarcount = 0,nav-user-menu= 0, on/homeand on/— the whole app is dead. NG0200 in console.The delta between the passing anonymous case and the crash is only the presence of an authenticated session — so the cycle is in a service that is constructed only on the authenticated path.
Not confirmed on live prod (SW caching)
Couldn't cleanly A/B against spikersoft.com: production has the service worker enabled, so it serves a cached older bundle (anonymous prod also reports
app-toolbar=0 because the cached build predates the P2data-testids). So live users may be on an older, working bundle — but the currentmasterwill ship this crash on the next fresh load.Strong lead (not yet confirmed — needs a readable build)
Authenticated-only bootstrap DI cycle. Prime suspects in
projects/spikersoft/src/main.ts: theHTTP_INTERCEPTORSchain (JwtInterceptor/AnonLessonInterceptor) — a classic NG0200 is an interceptor whose dependency graph pulls inHttpClient, which only closes the loop when an authenticated request actually exercises the token-attach path — or the keycloak-angularAutoRefreshTokenService/UserActivityService/provideActivityAuthGatefactory that injectsKeycloak. Pinning the exact cycle needs a dev/non-optimized build with class names (the prod build'sdevelopmentconfig points at dev API/Keycloak, so the repro harness needs adjusting).Impact
Blocks all of P4/P5/P6 (every authenticated E2E phase) and — if it reaches users on a fresh bundle — is a sev-1: logged-in users get a blank page.
Part of epic #307.
New evidence from the #359 browser validation (dev, non-minified build, local Mac API):
The NG0200 cycle is
AnonLessonInterceptor → AnonSessionService → HttpClient → interceptor chain. A lazy-injection fix (resolveAnonSessionServiceviaInjectorinsideintercept(), past the authenticated early-return) was found uncommitted in the worktree and is now committed onspikersoft-angularbranchfeature/mac-browser-validation(commit6b3d947). With it, a fresh interactive Keycloak login renders and works fully — toolbar, user menu, Art Studio, SignalR.The blank-shell symptom is NOT fully explained by that cycle. Even with the interceptor fix, booting the app with an existing SSO session (Playwright storage state, or any hard refresh while logged in) still renders an empty
<app-root>— zero console errors,SystemNotificationServiceconnects to the hub, Profile calls return 200, and the dev-mode diagnostic prints "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable". Fresh-login boot paints; silent-SSO/session-restore boot never does. Evidence:e2e/test-results/artstudio-live/debug-auth-home.png(blank) on that branch, versus00-logged-in.png(fresh login, fully rendered) from the same bundle minutes apart.So this ticket likely has two layers: the NG0200 DI cycle (fix in hand) and a session-restore bootstrap hang (unfixed, reproducible at will with a persisted storage state +
page.goto). The #359 E2E spec works around it by logging in fresh inside the test.Layer 1 (the NG0200 DI cycle) fixed in spikersoft-angular PR #127 (merged to
master). The exact cycle Angular emitted:AnonSessionService → ActivityTrackingService → HTTP_INTERCEPTORS → AnonSessionService—AnonLessonInterceptorfield-injectedAnonSessionServiceat construction, andActivityTrackingService's ctor fires an auth-gated manifest GET that materializes the interceptor chain mid-construction. Fix: defer that one edge to a lazyinjector.get(AnonSessionService)insideintercept(). Failing→passing unit repro added; mirrors the browser-validated6b3d947.Keeping this open, re-scoped to layer 2 — a separate session-restore bootstrap hang that survives the DI fix: booting with an existing SSO session (Playwright storage state, or hard-refresh while logged in) still renders an empty
<app-root>with no console errors — "Application did not stabilize within 9 seconds / PendingTasks keeping application unstable." This is the one that blanks the shell for returning logged-in users and blocks the authenticated e2e accounts (e2e-test-accounts). Next step: trace what keepsPendingTasksunstable during silent SSO restore (an APP_INITIALIZER/resolver awaiting a never-settling promise, or an interceptor stalling the manifest/auth round-trip).Layer 2 root cause — pinned end-to-end (high confidence). The blank shell is a pre-bootstrap hang in
keycloak.init():core.mjs:650) and gatesappRef.bootstrap(rootComponent)behindApplicationInitStatus.donePromise— so if an APP_INITIALIZER never settles, the root component is never created → truly empty<app-root>(dispositive: a post-bootstrap hang would still paint the skeleton).provideKeycloakregisters a blocking app initializer (keycloak-angular.mjs:1554) thatawaitskeycloak.init(initOptions).onLoad:'check-sso'andcheckLoginIframedefaulting true (not set inmain.ts), init awaits#checkLoginIframe()then#checkSsoSilently()(keycloak.js:948/974) — each anew Promiseresolved only by apostMessagefrom a hidden cross-origin iframe atids.spikersoft.com, with no internal timeout. If the iframe never posts back, init never settles.callback.validbranch and resolves without the iframe. Hard-refresh / Playwright storage-state has no fragment → check-sso branch → hangs. Anonymous boot resolves (silent iframe reports "no session"). Zero console errors because a hang is neither resolve nor reject, and keycloak-js logging defaults off. The earlier "Profile 200 / hub connects" evidence was from the fresh-login run (AppComponent.ngOnInit never runs during the hang).Ranked fixes (browser-verification required — not unit-testable):
checkLoginIframe: falseinmain.tsinitOptions (keycloak's recommended modern-browser setting) — skips the hanging login-status round-trip; check-sso then uses the same-originassets/silent-check-sso.html. Least-invasive, most likely the real fix; pair with an explicitsilentCheckSsoRedirectUri. Tradeoff: disables cross-tab SSO-logout detection (acceptable here).keycloak.init()with a timeout (safety net) — guarantees the shell never blanks again; on timeout boots anonymous (re-auth) rather than hanging. This part IS unit-testable.ids.spikersoft.comcookies inside thelocalhost:4200iframe — fix that to unblock authenticated e2e (#316/#319).A fix PR combining #1 + #2 (with a stabilization unit test for the timeout safety net) is being prepared; it will need your ~5-min logged-in hard-refresh verification before merge — a unit test can't exercise the cross-origin iframe. Repro: build →
serve -s dist/spikersoft/browser→ log in → hard-refresh/→ blank shell; apply #1 → paints + session restored.Fix ready for your browser check — spikersoft-angular PR #129 (NOT merged, held for you). Implements the ranked fix: (1)
checkLoginIframe: false+silentCheckSsoFallback: trueinmain.ts(skips the hanging cross-origin login-status iframe; check-sso uses the same-origin silent html); (2) a bounded-init safety net (initKeycloakBounded,Promise.race([keycloak.init, 8s timeout]), always resolves) that guarantees the shell can never blank again even if an iframe still hangs — with a unit test proving it (never-resolving init → still settles at the deadline; unboundedawait→ test times out).1857 angular tests pass, lint + build green. Your ~5-min step before merge:
nx build→serve -s dist/spikersoft/browser→ log in → hard-refresh/→ confirm the shell paints AND you're still authenticated. If it paints but you're logged out, the residual is the 3p-cookie/SameSite iframe policy under the e2e origin (candidate #3) — I'll take that next to unblock authenticated e2e #316/#319.Layer-2 status: the shell-paint fix is already merged to
master(PR #129 —keycloak-bounded-init, inorigin/masterHEAD6374604). Independently re-verified the root cause + that the merged fix is correct (didn't add a duplicate).Root cause (confirmed): the blocking
keycloak.init()app-initializer withonLoad:'check-sso'+checkLoginIframedefaultingtrueawaits a hidden cross-origin login-status iframe atids.spikersoft.comthat never posts back on session-restore (no auth-code fragment) →ApplicationInitStatus.donePromisenever resolves → empty<app-root>, "PendingTasks keeping application unstable", zero console errors. Fresh login takes the callback branch and resolves without the iframe — hence session-restore-only.The merged fix:
checkLoginIframe:false(falls back to the same-originassets/silent-check-sso.html), omitsinitOptionsfromprovideKeycloak(no second unbounded initializer — verified against lib source), and wraps init inPromise.race([init, 8s timeout])so the shell can never blank again. Fresh-login + anonymous both settle well under 8s → fresh path unaffected. Unit testkeycloak-bounded-init.spec.ts3/3;nx build spikersoftgreen.What still needs your browser-verify: the fix guarantees the shell paints; it cannot settle in code whether the SSO session actually restores on a real hard-refresh — that hinges on third-party-cookie / SameSite policy for
ids.spikersoft.comcookies inside the same-origin silent-check-sso iframe (this ticket's candidate #3). Please boot with a restored SSO session (Playwright storage-state or a logged-in hard-refresh of/):Optional hardening not added (avoiding speculative change on a merged fix): set keycloak-js
messageReceiveTimeoutbelow the 8s bound so the silent iframe gives up cleanly first — say the word if you want it.Closing with fresh evidence (2026-07-12). Fresh
masterproduction bundle, real Keycloak UI login, full authenticated walk (54 specs):app-rootattached, toolbar visible) on every auth route; zero bounces to ids.spikersoft.com.401 /api/activity/trackbeacon bug (angular PR #169), e2e suite silent-skip + assertion traps (angular PR #168), and a handful of missing-endpoint 404s (/api/skills/my/by-interest, double-prefix/api/api/vault/*+/api/api/game/dungeon-crawler/keybindings) worth their own tickets.Residual observation (not a crash, noting for the record): with a restored session,
keycloak.init()still runs to the #129 8s bounded-init timeout before an instant login round-trip completes auth — first paint on cold storage-state sessions is ~8s slower than it could be. The safety net works exactly as designed (boot always completes), but shaving that window is a candidate follow-up.